mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
163 lines
6.9 KiB
PHP
163 lines
6.9 KiB
PHP
<?php
|
|
|
|
use App\Exceptions\DeploymentException;
|
|
use App\Jobs\ApplicationDeploymentJob;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationDeploymentQueue;
|
|
use App\Rules\DockerImageFormat;
|
|
use App\Support\ValidationPatterns;
|
|
|
|
it('accepts valid docker registry image names', function (string $imageName) {
|
|
expect(ValidationPatterns::isValidDockerImageName($imageName))->toBeTrue();
|
|
})->with([
|
|
'single component' => 'nginx',
|
|
'namespace image' => 'library/nginx',
|
|
'ghcr image' => 'ghcr.io/coollabsio/coolify',
|
|
'repository component with repeated hyphens' => 'ghcr.io/acme/my--service',
|
|
'registry with port' => 'registry.example.com:5000/team/app',
|
|
'IPv6 registry' => '[2a01:4f8::1]/team/app',
|
|
'IPv6 registry with port' => '[2a01:4f8::1]:5000/app',
|
|
'IPv4-mapped IPv6 registry' => '[::ffff:192.0.2.1]:5000/app',
|
|
'digest marker used by existing dockerimage records' => 'nginx@sha256',
|
|
]);
|
|
|
|
it('rejects docker registry image names with shell metacharacters', function (string $imageName) {
|
|
expect(ValidationPatterns::isValidDockerImageName($imageName))->toBeFalse();
|
|
})->with([
|
|
'command substitution' => 'coolify/poc$(touch /tmp/pwned)',
|
|
'semicolon' => 'coolify/poc;id',
|
|
'backticks' => 'coolify/poc`id`',
|
|
'pipe' => 'coolify/poc|id',
|
|
'logical and' => 'coolify/poc&&id',
|
|
'newline' => "coolify/poc\nid",
|
|
'space' => 'coolify/poc image',
|
|
'tag in image-name-only field' => 'coolify/poc:latest',
|
|
'IPv6 registry with command substitution' => '[2a01:4f8::1$(id)]:5000/app',
|
|
'IPv6 registry with letters outside hex' => '[2a01:4f8::zz]/app',
|
|
'unclosed IPv6 registry' => '[2a01:4f8::1:5000/app',
|
|
'bare IPv6 registry' => '2a01:4f8::1/app',
|
|
]);
|
|
|
|
it('accepts valid docker registry image tags', function (string $tag) {
|
|
expect(ValidationPatterns::isValidDockerImageTag($tag))->toBeTrue();
|
|
})->with([
|
|
'latest' => 'latest',
|
|
'version' => 'v1.2.3',
|
|
'uppercase and underscore' => 'PR_123',
|
|
'sha256 hash' => '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
'legacy sha256 prefixed hash' => 'sha256-1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
'environment variable reference' => '$API_VERSION',
|
|
'braced environment variable reference' => '${API_VERSION}',
|
|
'environment variable reference with literal parts' => 'v${API_VERSION}-alpine',
|
|
'environment variable reference with default for empty or unset' => '${API_VERSION:-latest}',
|
|
'environment variable reference with default for unset' => '${API_VERSION-1.27-alpine}',
|
|
]);
|
|
|
|
it('rejects docker registry image tags with shell metacharacters', function (string $tag) {
|
|
expect(ValidationPatterns::isValidDockerImageTag($tag))->toBeFalse();
|
|
})->with([
|
|
'command substitution' => 'latest$(touch /tmp/pwned)',
|
|
'semicolon' => 'latest;id',
|
|
'backticks' => 'latest`id`',
|
|
'pipe' => 'latest|id',
|
|
'logical and' => 'latest&&id',
|
|
'newline' => "latest\nid",
|
|
'bare dollar' => '$',
|
|
'positional parameter' => '$1',
|
|
'special parameter' => '$$',
|
|
'unclosed braced reference' => '${API_VERSION',
|
|
'parameter expansion with command substitution default' => '${API_VERSION:-$(id)}',
|
|
'parameter expansion with nested reference default' => '${API_VERSION:-${OTHER}}',
|
|
'parameter expansion with required error' => '${API_VERSION:?missing}',
|
|
'parameter expansion with assignment' => '${API_VERSION:=latest}',
|
|
]);
|
|
|
|
it('accepts supported full docker image reference formats', function (string $imageReference) {
|
|
$failures = [];
|
|
|
|
(new DockerImageFormat)->validate('image', $imageReference, function (string $message) use (&$failures): void {
|
|
$failures[] = $message;
|
|
});
|
|
|
|
expect($failures)->toBeEmpty();
|
|
})->with([
|
|
'image with tag' => 'nginx:latest',
|
|
'registry image with tag' => 'ghcr.io/user/app:v1.2.3',
|
|
'image with sha256 digest' => 'nginx@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
'registry image with sha256 digest' => 'ghcr.io/user/app@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
'registry port image with tag' => 'localhost:5000/app:latest',
|
|
'IPv6 registry port image with tag' => '[2a01:4f8::1]:5000/app:latest',
|
|
]);
|
|
|
|
it('accepts an environment variable tag in a full docker image reference only when variable tags are allowed', function () {
|
|
$validate = function (DockerImageFormat $rule): array {
|
|
$failures = [];
|
|
|
|
$rule->validate('image', 'ghcr.io/user/app:${API_VERSION:-latest}', function (string $message) use (&$failures): void {
|
|
$failures[] = $message;
|
|
});
|
|
|
|
return $failures;
|
|
};
|
|
|
|
expect($validate(new DockerImageFormat(allowVariableTag: true)))->toBeEmpty()
|
|
->and($validate(new DockerImageFormat))->not->toBeEmpty();
|
|
});
|
|
|
|
it('rejects unsupported full docker image reference formats', function (string $imageReference) {
|
|
$failures = [];
|
|
|
|
(new DockerImageFormat)->validate('image', $imageReference, function (string $message) use (&$failures): void {
|
|
$failures[] = $message;
|
|
});
|
|
|
|
expect($failures)->not->toBeEmpty();
|
|
})->with([
|
|
'colon sha256 marker' => 'nginx:sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
'command substitution' => 'nginx:latest$(touch /tmp/pwned)',
|
|
'newline' => "nginx:latest\nid",
|
|
]);
|
|
|
|
it('stops deployments when a stored docker registry image value is unsafe', function () {
|
|
$job = (new ReflectionClass(ApplicationDeploymentJob::class))->newInstanceWithoutConstructor();
|
|
|
|
$application = new Application([
|
|
'docker_registry_image_name' => 'coolify/poc$(touch /tmp/pwned)',
|
|
'docker_registry_image_tag' => 'latest',
|
|
]);
|
|
$deploymentQueue = new ApplicationDeploymentQueue([
|
|
'docker_registry_image_tag' => null,
|
|
]);
|
|
|
|
$jobReflection = new ReflectionClass($job);
|
|
foreach ([
|
|
'application' => $application,
|
|
'application_deployment_queue' => $deploymentQueue,
|
|
'dockerImagePreviewTag' => null,
|
|
] as $property => $value) {
|
|
$reflectionProperty = $jobReflection->getProperty($property);
|
|
$reflectionProperty->setValue($job, $value);
|
|
}
|
|
|
|
$method = $jobReflection->getMethod('validateDockerRegistryImageConfiguration');
|
|
|
|
expect(fn () => $method->invoke($job))->toThrow(DeploymentException::class);
|
|
});
|
|
|
|
it('allows deployments when the stored docker registry image tag is an environment variable reference', function () {
|
|
$job = (new ReflectionClass(ApplicationDeploymentJob::class))->newInstanceWithoutConstructor();
|
|
|
|
$jobReflection = new ReflectionClass($job);
|
|
foreach ([
|
|
'application' => new Application([
|
|
'docker_registry_image_name' => 'ghcr.io/user/app',
|
|
'docker_registry_image_tag' => '$API_VERSION',
|
|
]),
|
|
'dockerImagePreviewTag' => null,
|
|
] as $property => $value) {
|
|
$jobReflection->getProperty($property)->setValue($job, $value);
|
|
}
|
|
|
|
$jobReflection->getMethod('validateDockerRegistryImageConfiguration')->invoke($job);
|
|
})->throwsNoExceptions();
|