diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 440d28266..e5f7dc9ee 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -52,8 +52,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private Application $application; - private DockerRegistry $dockerRegistry; - private string $deployment_uuid; private int $pull_request_id; @@ -176,10 +174,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->build_pack = data_get($this->application, 'build_pack'); $this->build_args = collect([]); - if ($this->application->docker_registry_id) { - $this->dockerRegistry = DockerRegistry::find($this->application->docker_registry_id); - } - $this->application_deployment_queue_id = $application_deployment_queue_id; $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid; $this->pull_request_id = $this->application_deployment_queue->pull_request_id; @@ -396,7 +390,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function deploy_dockerimage_buildpack() { - $useCustomRegistry = $this->application->docker_use_custom_registry; try { // setup $this->dockerImage = $this->application->docker_registry_image_name; @@ -408,7 +401,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); // login if use custom registry - if ($useCustomRegistry) { + if ($this->application->docker_use_custom_registry) { $this->handleRegistryAuth(); } @@ -419,7 +412,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } catch (Exception $e) { throw $e; } finally { - if ($useCustomRegistry) { + if ($this->application->docker_use_custom_registry) { $this->application_deployment_queue->addLogEntry('Logging out from registry...'); $this->execute_remote_command([ 'docker logout', @@ -2459,35 +2452,35 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function handleRegistryAuth() { - if (!$this->dockerRegistry) { - throw new Exception('Registry not found.'); + $registries = $this->application->registries; + + if ($registries->isEmpty()) { + throw new Exception('No registries found.'); } - $token = escapeshellarg($this->dockerRegistry->token); - $username = escapeshellarg($this->dockerRegistry->username); + foreach ($registries as $registry) { + $token = escapeshellarg($registry->token); + $username = escapeshellarg($registry->username); - // Handle different registry types - $url = match ($this->dockerRegistry->type) { - 'docker_hub' => '', // Docker Hub doesn't need URL specified - 'custom' => escapeshellarg($this->dockerRegistry->url), - default => escapeshellarg($this->dockerRegistry->url) - }; + $url = match ($registry->type) { + 'docker_hub' => '', + 'custom' => escapeshellarg($registry->url), + default => escapeshellarg($registry->url) + }; - $this->application_deployment_queue->addLogEntry('Attempting to log into registry...'); + $this->application_deployment_queue->addLogEntry("Attempting to log into registry {$registry->name}"); - // Build login command based on registry type - $command = $this->dockerRegistry->type === 'docker_hub' - ? "echo {{secrets.token}} | docker login -u {$username} --password-stdin" - : "echo {{secrets.token}} | docker login {$url} -u {$username} --password-stdin"; + $command = $registry->type === 'docker_hub' + ? "echo {{secrets.token}} | docker login -u {$username} --password-stdin" + : "echo {{secrets.token}} | docker login {$url} -u {$username} --password-stdin"; - $this->execute_remote_command( - [ + $this->execute_remote_command([ 'command' => $command, 'secrets' => [ 'token' => $token, ], 'hidden' => true, - ] - ); + ]); + } } } diff --git a/app/Livewire/Images/Registry/Create.php b/app/Livewire/Images/Registry/Create.php index 144ceb76e..3b13613d8 100644 --- a/app/Livewire/Images/Registry/Create.php +++ b/app/Livewire/Images/Registry/Create.php @@ -8,7 +8,7 @@ use Livewire\Component; class Create extends Component { - #[Validate('required|string|max:255')] + #[Validate('required|string|max:255|unique:docker_registries,name')] public string $name = ''; #[Validate('required|string')] diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 08758ba14..ba2ef48e4 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -30,6 +30,8 @@ class General extends Component public string $build_pack; + public array $selectedRegistries = []; + public ?string $ports_exposes = null; public bool $is_preserve_repository_enabled = false; @@ -73,7 +75,6 @@ class General extends Component 'application.docker_registry_image_name' => 'nullable', 'application.docker_registry_image_tag' => 'nullable', 'application.docker_use_custom_registry' => 'boolean', - 'application.docker_registry_id' => 'nullable|required_if:application.docker_use_custom_registry,true', 'application.dockerfile_location' => 'nullable', 'application.docker_compose_location' => 'nullable', 'application.docker_compose' => 'nullable', @@ -95,6 +96,8 @@ class General extends Component 'application.settings.is_preserve_repository_enabled' => 'boolean|required', 'application.watch_paths' => 'nullable', 'application.redirect' => 'string|required', + 'selectedRegistries' => 'required_if:application.docker_use_custom_registry,true|array', + 'selectedRegistries.*' => 'exists:docker_registries,id', ]; protected $validationAttributes = [ @@ -117,7 +120,6 @@ class General extends Component 'application.docker_registry_image_name' => 'Docker registry image name', 'application.docker_registry_image_tag' => 'Docker registry image tag', 'application.docker_use_custom_registry' => 'Use private registry', - 'application.docker_registry_id' => 'Registry', 'application.dockerfile_location' => 'Dockerfile location', 'application.docker_compose_location' => 'Docker compose location', 'application.docker_compose' => 'Docker compose', @@ -135,6 +137,7 @@ class General extends Component 'application.settings.is_preserve_repository_enabled' => 'Is preserve repository enabled', 'application.watch_paths' => 'Watch paths', 'application.redirect' => 'Redirect', + 'selectedRegistries' => 'Registries', ]; public function mount() @@ -154,6 +157,9 @@ class General extends Component $this->application->settings->save(); } + if ($this->application->docker_use_custom_registry) { + $this->selectedRegistries = $this->application->registries->pluck('id')->toArray(); + } $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; $this->ports_exposes = $this->application->ports_exposes; $this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled; @@ -395,7 +401,8 @@ class General extends Component if (data_get($this->application, 'build_pack') === 'dockerimage') { $this->validate([ 'application.docker_registry_image_name' => 'required', - 'application.docker_registry_id' => 'required_if:application.docker_use_custom_registry,true', + 'selectedRegistries' => 'required_if:application.docker_use_custom_registry,true|array', + 'selectedRegistries.*' => 'exists:docker_registries,id', ]); } @@ -431,6 +438,12 @@ class General extends Component } } $this->application->custom_labels = base64_encode($this->customLabels); + if ($this->application->docker_use_custom_registry) { + $this->application->registries()->sync($this->selectedRegistries); + } else { + $this->application->registries()->detach(); + } + $this->application->save(); $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 7833a11b9..2205ce610 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -14,13 +14,14 @@ class DockerImage extends Component { public string $dockerImage = ''; public bool $useCustomRegistry = false; - public ?int $selectedRegistry = null; + public array $selectedRegistries = []; public array $parameters; public array $query; protected $rules = [ 'dockerImage' => 'required|string', - 'selectedRegistry' => 'required_if:useCustomRegistry,true|nullable|exists:docker_registries,id' + 'selectedRegistries' => 'required_if:useCustomRegistry,true|array', + 'selectedRegistries.*' => 'exists:docker_registries,id' ]; public function mount() @@ -31,54 +32,70 @@ class DockerImage extends Component public function submit() { - $this->validate(['dockerImage' => 'required',]); + $this->validate([ + 'dockerImage' => 'required', + 'selectedRegistries' => 'required_if:useCustomRegistry,true|array', + 'selectedRegistries.*' => 'exists:docker_registries,id' + ]); - $image = str($this->dockerImage)->before(':'); - $tag = str($this->dockerImage)->contains(':') ? - str($this->dockerImage)->after(':') : - 'latest'; + try { + $image = str($this->dockerImage)->before(':'); + if (str($this->dockerImage)->contains(':')) { + $tag = str($this->dockerImage)->after(':'); + } else { + $tag = 'latest'; + } - $destination_uuid = $this->query['destination']; - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (! $destination) { - $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); + $destination_uuid = $this->query['destination']; + $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + if (! $destination) { + $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); + } + if (! $destination) { + throw new \Exception('Destination not found. What?!'); + } + $destination_class = $destination->getMorphClass(); + + $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); + $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); + + $name = 'docker-image-' . new Cuid2; + error_log($name); + $application = Application::create([ + 'name' => $name, + 'repository_project_id' => 0, + 'git_repository' => 'coollabsio/coolify', + 'git_branch' => 'main', + 'build_pack' => 'dockerimage', + 'ports_exposes' => 80, + 'docker_registry_image_name' => $image, + 'docker_registry_image_tag' => $tag, + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination_class, + 'health_check_enabled' => false, + 'docker_use_custom_registry' => $this->useCustomRegistry, + ]); + + if ($this->useCustomRegistry && !empty($this->selectedRegistries)) { + $application->registries()->sync($this->selectedRegistries); + } + + error_log($application->uuid); + $fqdn = generateFqdn($destination->server, $application->uuid); + $application->update([ + 'name' => 'docker-image-' . $application->uuid, + 'fqdn' => $fqdn, + ]); + + return redirect()->route('project.application.configuration', [ + 'application_uuid' => $application->uuid, + 'environment_name' => $environment->name, + 'project_uuid' => $project->uuid, + ]); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); } - if (! $destination) { - throw new \Exception('Destination not found. What?!'); - } - $destination_class = $destination->getMorphClass(); - - $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); - $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); - - $application = Application::create([ - 'name' => 'docker-image-' . new Cuid2, - 'repository_project_id' => 0, - 'git_repository' => 'coollabsio/coolify', - 'git_branch' => 'main', - 'build_pack' => 'dockerimage', - 'ports_exposes' => 80, - 'docker_registry_image_name' => $image, - 'docker_registry_image_tag' => $tag, - 'docker_use_custom_registry' => $this->useCustomRegistry, - 'docker_registry_id' => $this->selectedRegistry ?? null, - 'environment_id' => $environment->id, - 'destination_id' => $destination->id, - 'destination_type' => $destination_class, - 'health_check_enabled' => false, - ]); - - $fqdn = generateFqdn($destination->server, $application->uuid); - $application->update([ - 'name' => 'docker-image-' . $application->uuid, - 'fqdn' => $fqdn, - ]); - - return redirect()->route('project.application.configuration', [ - 'application_uuid' => $application->uuid, - 'environment_name' => $environment->name, - 'project_uuid' => $project->uuid, - ]); } public function render() diff --git a/app/Models/Application.php b/app/Models/Application.php index d45e0892e..34aeee17b 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -110,6 +110,15 @@ class Application extends BaseModel private static $parserVersion = '4'; + protected $fillable = [ + 'docker_use_custom_registry', + ]; + + protected $casts = [ + 'docker_use_custom_registry' => 'boolean', + 'selectedRegistries' => 'array', + ]; + protected $guarded = []; protected $appends = ['server_status']; @@ -1653,8 +1662,8 @@ class Application extends BaseModel } } - public function registry() + public function registries() { - return $this->belongsTo(DockerRegistry::class); + return $this->belongsToMany(DockerRegistry::class, 'application_docker_registry', 'application_id'); } } diff --git a/app/Models/DockerRegistry.php b/app/Models/DockerRegistry.php index c3638d0ad..3c0ac3a6f 100644 --- a/app/Models/DockerRegistry.php +++ b/app/Models/DockerRegistry.php @@ -25,9 +25,4 @@ class DockerRegistry extends Model 'custom' => 'Custom Registry' ]; } - - public function applications(): HasMany - { - return $this->hasMany(Application::class, 'docker_registry_id', 'id'); - } } diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php index dd5ba66b7..e5a3ab19a 100644 --- a/app/View/Components/Forms/Select.php +++ b/app/View/Components/Forms/Select.php @@ -19,6 +19,7 @@ class Select extends Component public ?string $label = null, public ?string $helper = null, public bool $required = false, + public bool $multiple = false, public string $defaultClass = 'select' ) { // @@ -33,11 +34,13 @@ class Select extends Component $this->id = new Cuid2; } if (is_null($this->name)) { - $this->name = $this->id; + $this->name = $this->multiple ? "{$this->id}[]" : $this->id; } $this->label = Str::title($this->label); - return view('components.forms.select'); + return view('components.forms.select', [ + 'multiple' => $this->multiple, + ]); } } diff --git a/database/migrations/2024_11_08_084443_add_registry_to_applications_table.php b/database/migrations/2024_11_08_084443_add_registry_to_applications_table.php index c01657efe..c84bd56cb 100644 --- a/database/migrations/2024_11_08_084443_add_registry_to_applications_table.php +++ b/database/migrations/2024_11_08_084443_add_registry_to_applications_table.php @@ -8,7 +8,6 @@ return new class extends Migration { public function up(): void { Schema::table('applications', function (Blueprint $table) { - $table->foreignId('docker_registry_id')->nullable(); $table->boolean('docker_use_custom_registry')->default(false); }); } @@ -16,7 +15,6 @@ return new class extends Migration { public function down(): void { Schema::table('applications', function (Blueprint $table) { - $table->dropColumn('docker_registry_id'); $table->dropColumn('docker_use_custom_registry'); }); } diff --git a/database/migrations/2024_11_10_203115_create_registries_table.php b/database/migrations/2024_11_7_203115_create_registries_table.php similarity index 60% rename from database/migrations/2024_11_10_203115_create_registries_table.php rename to database/migrations/2024_11_7_203115_create_registries_table.php index cc71abc06..0cfa8874e 100644 --- a/database/migrations/2024_11_10_203115_create_registries_table.php +++ b/database/migrations/2024_11_7_203115_create_registries_table.php @@ -9,29 +9,17 @@ return new class extends Migration { { Schema::create('docker_registries', function (Blueprint $table) { $table->id(); - $table->string('name'); + $table->string('name')->unique(); $table->string('type'); // docker_hub, gcr, ghcr, quay, custom $table->string('url')->nullable(); $table->string('username')->nullable(); $table->text('token')->nullable(); $table->timestamps(); }); - - // Add foreign key constraint - Schema::table('applications', function (Blueprint $table) { - $table->foreign('docker_registry_id') - ->references('id') - ->on('docker_registries') - ->nullOnDelete(); - }); } public function down(): void { - Schema::table('applications', function (Blueprint $table) { - $table->dropForeign(['docker_registry_id']); - }); - Schema::dropIfExists('docker_registries'); } }; diff --git a/database/migrations/2024_12_03_184606_create_application_docker_registry_table.php b/database/migrations/2024_12_03_184606_create_application_docker_registry_table.php new file mode 100644 index 000000000..dedae7067 --- /dev/null +++ b/database/migrations/2024_12_03_184606_create_application_docker_registry_table.php @@ -0,0 +1,24 @@ +id(); + $table->unsignedBigInteger('application_id'); + $table->unsignedBigInteger('docker_registry_id'); + $table->foreign('application_id')->references('id')->on('applications')->cascadeOnDelete(); + $table->foreign('docker_registry_id')->references('id')->on('docker_registries')->cascadeOnDelete(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('application_docker_registry'); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index 32d476c1a..59ff0f5cc 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -13,6 +13,10 @@ body { @apply text-sm antialiased scrollbar; } +option[selected] { + @apply bg-coollabs; +} + .apexcharts-tooltip { @apply dark:text-white dark:border-coolgray-300 dark:bg-coolgray-200 shadow-none !important; } diff --git a/resources/views/components/forms/select.blade.php b/resources/views/components/forms/select.blade.php index 4da9eca1b..f57280295 100644 --- a/resources/views/components/forms/select.blade.php +++ b/resources/views/components/forms/select.blade.php @@ -1,6 +1,7 @@