Merge remote-tracking branch 'origin/next' into chore/update-deployment-validation

This commit is contained in:
Andras Bacsai committed 2026-06-02 11:28:42 +02:00
commit 1bad82cf43
10 files changed
+379 -22

No files matched your search

@@ -17,6 +17,7 @@ use App\Models\LocalPersistentVolume;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Rules\DockerImageFormat;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
use App\Services\DockerImageParser;
@@ -1790,8 +1791,8 @@ class ApplicationsController extends Controller
]))->setStatusCode(201);
} elseif ($type === 'dockerimage') {
$validationRules = [
'docker_registry_image_name' => 'string|required',
'docker_registry_image_tag' => 'string',
'docker_registry_image_name' => ['required', 'string', 'max:255', new DockerImageFormat],
'docker_registry_image_tag' => ValidationPatterns::dockerImageTagRules(),
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
];
$validationRules = array_merge(sharedDataApplications(), $validationRules);
+16
View File
@@ -220,6 +220,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
$this->dockerImagePreviewTag = $this->application_deployment_queue->docker_registry_image_tag;
$this->validateDockerRegistryImageConfiguration();
$this->git_type = data_get($this->application_deployment_queue, 'git_type');
@@ -1139,6 +1140,21 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return $this->pull_request_id === 0;
}
private function validateDockerRegistryImageConfiguration(): void
{
if (! ValidationPatterns::isValidDockerImageName($this->application->docker_registry_image_name)) {
throw new DeploymentException('Docker registry image name contains invalid characters.');
}
if (! ValidationPatterns::isValidDockerImageTag($this->application->docker_registry_image_tag)) {
throw new DeploymentException('Docker registry image tag contains invalid characters.');
}
if (! ValidationPatterns::isValidDockerImageTag($this->dockerImagePreviewTag)) {
throw new DeploymentException('Docker registry preview image tag contains invalid characters.');
}
}
private function generate_image_names()
{
if ($this->application->dockerfile) {
+3 -3
View File
@@ -158,8 +158,8 @@ class General extends Component
'portsMappings' => ValidationPatterns::portMappingRules(),
'customNetworkAliases' => 'nullable',
'dockerfile' => 'nullable',
'dockerRegistryImageName' => 'nullable',
'dockerRegistryImageTag' => 'nullable',
'dockerRegistryImageName' => ValidationPatterns::dockerImageNameRules(),
'dockerRegistryImageTag' => ValidationPatterns::dockerImageTagRules(),
'dockerfileLocation' => ValidationPatterns::filePathRules(),
'dockerComposeLocation' => ValidationPatterns::filePathRules(),
'dockerCompose' => 'nullable',
@@ -849,7 +849,7 @@ class General extends Component
}
if ($this->buildPack === 'dockerimage') {
$this->validate([
'dockerRegistryImageName' => 'required',
'dockerRegistryImageName' => ValidationPatterns::dockerImageNameRules(required: true),
]);
}
+3 -2
View File
@@ -5,6 +5,7 @@ namespace App\Livewire\Project\New;
use App\Models\Application;
use App\Models\Project;
use App\Services\DockerImageParser;
use App\Support\ValidationPatterns;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -81,8 +82,8 @@ class DockerImage extends Component
public function submit()
{
$this->validate([
'imageName' => ['required', 'string'],
'imageTag' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9._-]*$/i'],
'imageName' => ValidationPatterns::dockerImageNameRules(required: true),
'imageTag' => ValidationPatterns::dockerImageTagRules(),
'imageSha256' => ['nullable', 'string', 'regex:/^[a-f0-9]{64}$/i'],
]);
+22 -13
View File
@@ -2,18 +2,26 @@
namespace App\Rules;
use App\Support\ValidationPatterns;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;
class DockerImageFormat implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value)) {
$fail('The :attribute format is invalid. Use image:tag or image@sha256:hash format.');
return;
}
// Check if the value contains ":sha256:" or ":sha" which is incorrect format
if (preg_match('/:sha256?:/i', $value)) {
$fail('The :attribute must use @ before sha256 digest (e.g., image@sha256:hash, not image:sha256:hash).');
@@ -21,20 +29,21 @@ class DockerImageFormat implements ValidationRule
return;
}
// Valid formats:
// 1. image:tag (e.g., nginx:latest)
// 2. registry/image:tag (e.g., ghcr.io/user/app:v1.2.3)
// 3. image@sha256:hash (e.g., nginx@sha256:abc123...)
// 4. registry/image@sha256:hash
// 5. registry:port/image:tag (e.g., localhost:5000/app:latest)
$imageName = $value;
$tag = null;
$pattern = '/^
(?:[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[0-9]+)?\/)? # Optional registry with optional port
[a-z0-9]+(?:[._\/-][a-z0-9]+)* # Image name (required)
(?::[a-z0-9][a-z0-9._-]*|@sha256:[a-f0-9]{64})? # Optional :tag or @sha256:hash
$/ix';
if (preg_match('/\A(.+)@sha256:([a-f0-9]{64})\z/i', $value, $matches) === 1) {
$imageName = $matches[1];
} else {
$lastColon = strrpos($value, ':');
$lastSlash = strrpos($value, '/');
if ($lastColon !== false && ($lastSlash === false || $lastColon > $lastSlash)) {
$imageName = substr($value, 0, $lastColon);
$tag = substr($value, $lastColon + 1);
}
}
if (! preg_match($pattern, $value)) {
if (! ValidationPatterns::isValidDockerImageName($imageName) || ! ValidationPatterns::isValidDockerImageTag($tag)) {
$fail('The :attribute format is invalid. Use image:tag or image@sha256:hash format.');
}
}
+92
View File
@@ -102,6 +102,23 @@ class ValidationPatterns
*/
public const DB_PASSWORD_PATTERN = '/^[A-Za-z0-9!@#%^*()_+\-=\[\]{}:,.?\/~]+$/';
/**
* Pattern for Docker image repository names without a tag.
*
* Allows an optional registry host/port followed by lowercase repository
* path components. A trailing @sha256 marker is accepted for existing
* digest-based dockerimage records that store the digest hash separately.
*/
public const DOCKER_IMAGE_NAME_PATTERN = '/\A(?=.{1,255}\z)(?:(?:[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]+)?\/)?[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*)*)(?:@sha256)?\z/';
/**
* Pattern for Docker image tags.
*
* Docker tags may contain letters, digits, underscores, dots, and hyphens,
* must start with an alphanumeric/underscore, and are limited to 128 chars.
*/
public const DOCKER_IMAGE_TAG_PATTERN = '/\A[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}\z/';
/**
* Normalize environment variable keys before validation and storage.
*/
@@ -163,6 +180,81 @@ class ValidationPatterns
return $key;
}
/**
* Get validation rules for Docker image repository names without tags.
*/
public static function dockerImageNameRules(bool $required = false, int $maxLength = 255): array
{
$rules = [];
if ($required) {
$rules[] = 'required';
} else {
$rules[] = 'nullable';
}
$rules[] = 'string';
$rules[] = "max:$maxLength";
$rules[] = 'regex:'.self::DOCKER_IMAGE_NAME_PATTERN;
return $rules;
}
/**
* Get validation rules for Docker image tags.
*/
public static function dockerImageTagRules(bool $required = false, int $maxLength = 128): array
{
$rules = [];
if ($required) {
$rules[] = 'required';
} else {
$rules[] = 'nullable';
}
$rules[] = 'string';
$rules[] = "max:$maxLength";
$rules[] = 'regex:'.self::DOCKER_IMAGE_TAG_PATTERN;
return $rules;
}
/**
* Get validation messages for Docker image fields.
*/
public static function dockerImageMessages(string $nameField = 'docker_registry_image_name', string $tagField = 'docker_registry_image_tag'): array
{
return [
"{$nameField}.regex" => 'The Docker registry image name must be a valid image repository without a tag and may not contain shell metacharacters.',
"{$tagField}.regex" => 'The Docker registry image tag must be a valid Docker tag and may not contain shell metacharacters.',
];
}
/**
* Check if a string is a valid Docker image repository name without a tag.
*/
public static function isValidDockerImageName(?string $value): bool
{
if (blank($value)) {
return true;
}
return preg_match(self::DOCKER_IMAGE_NAME_PATTERN, $value) === 1;
}
/**
* Check if a string is a valid Docker image tag.
*/
public static function isValidDockerImageTag(?string $value): bool
{
if (blank($value)) {
return true;
}
return preg_match(self::DOCKER_IMAGE_TAG_PATTERN, $value) === 1;
}
/**
* Get validation rules for database identifier fields (username, database name).
*