From b57a439b766fc1b178fb6d34cd9a86cd98f2fad3 Mon Sep 17 00:00:00 2001 From: David Buday Date: Fri, 8 Nov 2024 13:31:30 +0100 Subject: [PATCH 01/39] chore: add registry auth to docker image apps --- app/Jobs/ApplicationDeploymentJob.php | 63 ++++++++++++++++--- app/Livewire/Project/New/DockerImage.php | 41 +++++++----- ...dd_registry_auth_to_applications_table.php | 23 +++++++ .../project/new/docker-image.blade.php | 14 ++++- 4 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 5ceed332a9..bab7925c0a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -381,17 +381,35 @@ private function deploy_simple_dockerfile() private function deploy_dockerimage_buildpack() { - $this->dockerImage = $this->application->docker_registry_image_name; - if (str($this->application->docker_registry_image_tag)->isEmpty()) { - $this->dockerImageTag = 'latest'; - } else { - $this->dockerImageTag = $this->application->docker_registry_image_tag; + try { + $didLogin = $this->handleRegistryAuth(); + + // Pull the image + $this->execute_remote_command([ + "docker pull {$this->application->image}", + ]); + + // Logout if we logged in + if ($didLogin) { + $this->application_deployment_queue->addLogEntry('Logging out from registry...'); + $this->execute_remote_command([ + 'docker logout', + 'hidden' => true + ]); + } + + // Continue with the rest of the deployment... + } catch (Exception $e) { + // Make sure to logout even if pull fails + if ($didLogin ?? false) { + $this->execute_remote_command([ + 'docker logout', + 'hidden' => true + ]); + } + $this->application_deployment_queue->addLogEntry('Deployment error: ' . $e->getMessage(), 'stderr'); + throw $e; } - $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); - $this->generate_image_names(); - $this->prepare_builder_image(); - $this->generate_compose_file(); - $this->rolling_update(); } private function deploy_docker_compose_buildpack() @@ -2444,4 +2462,29 @@ public function failed(Throwable $exception): void } } } + + private function handleRegistryAuth() +{ + if ($this->application->registry_username && $this->application->registry_token) { + try { + $username = escapeshellarg($this->application->registry_username); + $token = escapeshellarg(decrypt($this->application->registry_token)); + + $this->application_deployment_queue->addLogEntry('Attempting to log into registry...'); + + $command = "echo {$token} | docker login -u {$username} --password-stdin > /dev/null"; + + $this->execute_remote_command([ + $command, + 'hidden' => true, + ]); + + return true; + } catch (Exception $e) { + $this->application_deployment_queue->addLogEntry('Registry authentication error: ' . $e->getMessage(), 'stderr'); + throw $e; + } + } + return false; +} } diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 417fb2ea02..af7c62dd88 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -12,11 +12,17 @@ class DockerImage extends Component { public string $dockerImage = ''; - + public ?string $registryUsername = null; + public ?string $registryToken = null; public array $parameters; - public array $query; + protected $rules = [ + 'dockerImage' => 'required|string', + 'registryUsername' => 'nullable|string', + 'registryToken' => 'nullable|string', + ]; + public function mount() { $this->parameters = get_route_parameters(); @@ -27,25 +33,30 @@ public function submit() { $this->validate([ 'dockerImage' => 'required', + 'registryUsername' => 'required_with:registryToken', + 'registryToken' => 'required_with:registryUsername', ]); + $image = str($this->dockerImage)->before(':'); - if (str($this->dockerImage)->contains(':')) { - $tag = str($this->dockerImage)->after(':'); - } else { - $tag = 'latest'; - } + $tag = str($this->dockerImage)->contains(':') ? + str($this->dockerImage)->after(':') : + 'latest'; + $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 = StandaloneDocker::where('uuid', $destination_uuid)->first() + ?? SwarmDocker::where('uuid', $destination_uuid)->first(); + + if (!$destination) { + throw new \Exception('Destination not found.'); } $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(); + $environment = $project->load(['environments']) + ->environments + ->where('name', $this->parameters['environment_name']) + ->first(); + $application = Application::create([ 'name' => 'docker-image-'.new Cuid2, 'repository_project_id' => 0, @@ -59,6 +70,8 @@ public function submit() 'destination_id' => $destination->id, 'destination_type' => $destination_class, 'health_check_enabled' => false, + 'registry_username' => $this->registryUsername, + 'registry_token' => $this->registryToken ? encrypt($this->registryToken) : null, ]); $fqdn = generateFqdn($destination->server, $application->uuid); diff --git a/database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php b/database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php new file mode 100644 index 0000000000..d3800b4a90 --- /dev/null +++ b/database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php @@ -0,0 +1,23 @@ +string('registry_username')->nullable(); + $table->text('registry_token')->nullable(); + }); + } + + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('registry_username'); + $table->dropColumn('registry_token'); + }); + } +}; \ No newline at end of file diff --git a/resources/views/livewire/project/new/docker-image.blade.php b/resources/views/livewire/project/new/docker-image.blade.php index 4cc86710a3..5bc7df060f 100644 --- a/resources/views/livewire/project/new/docker-image.blade.php +++ b/resources/views/livewire/project/new/docker-image.blade.php @@ -6,6 +6,18 @@

Docker Image

Save - + + +

Registry Authentication

+
+ + + +
From 15653e645ca7458063fcc8acbb813ee4efcfdf3c Mon Sep 17 00:00:00 2001 From: David Buday Date: Fri, 8 Nov 2024 17:53:47 +0100 Subject: [PATCH 02/39] chore: added registry url with default plus fixed functionality --- app/Jobs/ApplicationDeploymentJob.php | 62 ++++++++++------ app/Livewire/Project/Application/General.php | 8 ++ app/Livewire/Project/New/DockerImage.php | 26 +++++-- app/Models/Application.php | 4 + ...dd_registry_auth_to_applications_table.php | 12 ++- .../project/application/general.blade.php | 74 ++++++++++++------- .../project/new/docker-image.blade.php | 29 +++++--- 7 files changed, 148 insertions(+), 67 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index bab7925c0a..10bb9a0f83 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -363,7 +363,9 @@ private function deploy_simple_dockerfile() $this->server = $this->build_server; } $dockerfile_base64 = base64_encode($this->application->dockerfile); + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}."); + $didLogin = $this->handleRegistryAuth(); $this->prepare_builder_image(); $this->execute_remote_command( [ @@ -382,12 +384,24 @@ private function deploy_simple_dockerfile() private function deploy_dockerimage_buildpack() { try { + // setup + $this->dockerImage = $this->application->docker_registry_image_name; + if (str($this->application->docker_registry_image_tag)->isEmpty()) { + $this->dockerImageTag = 'latest'; + } else { + $this->dockerImageTag = $this->application->docker_registry_image_tag; + } + + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); + // login $didLogin = $this->handleRegistryAuth(); - // Pull the image - $this->execute_remote_command([ - "docker pull {$this->application->image}", - ]); + $this->generate_image_names(); + $this->prepare_builder_image(); + $this->generate_compose_file(); + $this->rolling_update(); + + // Logout if we logged in if ($didLogin) { @@ -591,6 +605,8 @@ private function deploy_docker_compose_buildpack() private function deploy_dockerfile_buildpack() { + // login + $didLogin = $this->handleRegistryAuth(); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}."); if ($this->use_build_server) { $this->server = $this->build_server; @@ -1970,7 +1986,7 @@ private function pull_latest_image($image) $this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry."); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "docker pull {$image}"), + executeInDocker($this->deployment_uuid, "docker pull {$this->application->registry_url}/{$image}"), 'hidden' => true, ] ); @@ -2464,27 +2480,31 @@ public function failed(Throwable $exception): void } private function handleRegistryAuth() -{ - if ($this->application->registry_username && $this->application->registry_token) { - try { - $username = escapeshellarg($this->application->registry_username); - $token = escapeshellarg(decrypt($this->application->registry_token)); + { + if ($this->application->docker_use_custom_registry) { + try { + $username = escapeshellarg($this->application->docker_registry_username); + $token = escapeshellarg(decrypt($this->application->docker_registry_token)); + + $registry = $this->application->docker_registry_url ?: 'docker.io'; // Default to docker.io + $registry = escapeshellarg($registry); - $this->application_deployment_queue->addLogEntry('Attempting to log into registry...'); + $this->application_deployment_queue->addLogEntry('Attempting to log into registry...'); - $command = "echo {$token} | docker login -u {$username} --password-stdin > /dev/null"; + $command = "echo {$token} | docker login {$registry} -u {$username} --password-stdin"; + $this->application_deployment_queue->addLogEntry($command); - $this->execute_remote_command([ + $this->execute_remote_command([ $command, 'hidden' => true, - ]); - - return true; - } catch (Exception $e) { - $this->application_deployment_queue->addLogEntry('Registry authentication error: ' . $e->getMessage(), 'stderr'); - throw $e; + ]); + + return true; + } catch (Exception $e) { + $this->application_deployment_queue->addLogEntry('Registry authentication error: ' . $e->getMessage(), 'stderr'); + throw $e; + } } + return false; } - return false; -} } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index f1575a01f0..4ad57d2a7a 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -71,6 +71,10 @@ class General extends Component 'application.dockerfile' => 'nullable', 'application.docker_registry_image_name' => 'nullable', 'application.docker_registry_image_tag' => 'nullable', + 'application.docker_use_custom_registry' => 'boolean', + 'application.docker_registry_url' => 'nullable', + 'application.docker_registry_username' => 'required_with:application.docker_use_custom_registry', + 'application.docker_registry_token' => 'required_with:application.docker_use_custom_registry', 'application.dockerfile_location' => 'nullable', 'application.docker_compose_location' => 'nullable', 'application.docker_compose' => 'nullable', @@ -112,6 +116,10 @@ class General extends Component 'application.dockerfile' => 'Dockerfile', 'application.docker_registry_image_name' => 'Docker registry image name', 'application.docker_registry_image_tag' => 'Docker registry image tag', + 'application.docker_use_custom_registry' => 'Docker use custom registry', + 'application.docker_registry_url' => 'Registry URL', + 'application.docker_registry_username' => 'Registry Username', + 'application.docker_registry_token' => 'Registry Token', 'application.dockerfile_location' => 'Dockerfile location', 'application.docker_compose_location' => 'Docker compose location', 'application.docker_compose' => 'Docker compose', diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index af7c62dd88..e201c203d6 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -14,29 +14,41 @@ class DockerImage extends Component public string $dockerImage = ''; public ?string $registryUsername = null; public ?string $registryToken = null; + public ?string $registryUrl = 'docker.io'; + public bool $useCustomRegistry = false; public array $parameters; public array $query; protected $rules = [ 'dockerImage' => 'required|string', - 'registryUsername' => 'nullable|string', - 'registryToken' => 'nullable|string', + 'registryUsername' => 'required_with:useCustomRegistry|string', + 'registryToken' => 'required_with:useCustomRegistry|string', + 'registryUrl' => 'nullable|string', + 'useCustomRegistry' => 'boolean' ]; public function mount() { $this->parameters = get_route_parameters(); $this->query = request()->query(); + $this->registryUrl = 'docker.io'; } public function submit() { $this->validate([ 'dockerImage' => 'required', - 'registryUsername' => 'required_with:registryToken', - 'registryToken' => 'required_with:registryUsername', + 'registryUsername' => 'required_with:useCustomRegistry', + 'registryToken' => 'required_with:useCustomRegistry', ]); + // Only save registry settings if useCustomRegistry is true + if (!$this->useCustomRegistry) { + $this->registryUsername = null; + $this->registryToken = null; + $this->registryUrl = 'docker.io'; + } + $image = str($this->dockerImage)->before(':'); $tag = str($this->dockerImage)->contains(':') ? str($this->dockerImage)->after(':') : @@ -70,8 +82,10 @@ public function submit() 'destination_id' => $destination->id, 'destination_type' => $destination_class, 'health_check_enabled' => false, - 'registry_username' => $this->registryUsername, - 'registry_token' => $this->registryToken ? encrypt($this->registryToken) : null, + 'docker_use_custom_registry' => $this->useCustomRegistry, + 'docker_registry_url' => $this->registryUrl, + 'docker_registry_username' => $this->registryUsername, + 'docker_registry_token' => $this->registryToken ? encrypt($this->registryToken) : null, ]); $fqdn = generateFqdn($destination->server, $application->uuid); diff --git a/app/Models/Application.php b/app/Models/Application.php index 0ef787b2e1..2180b78a37 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -35,6 +35,10 @@ 'git_full_url' => ['type' => 'string', 'nullable' => true, 'description' => 'Git full URL.'], 'docker_registry_image_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image tag.'], + 'docker_use_custom_registry' => ['type' => 'boolean', 'description' => 'Use custom registry.'], + 'docker_registry_url' => ['type' => 'string', 'nullable' => true, 'description' => 'Registry URL.'], + 'docker_registry_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Registry username.'], + 'docker_registry_token' => ['type' => 'string', 'nullable' => true, 'description' => 'Registry token.'], 'build_pack' => ['type' => 'string', 'description' => 'Build pack.', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose']], 'static_image' => ['type' => 'string', 'description' => 'Static image used when static site is deployed.'], 'install_command' => ['type' => 'string', 'description' => 'Install command.'], diff --git a/database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php b/database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php index d3800b4a90..422503bfcc 100644 --- a/database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php +++ b/database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php @@ -8,16 +8,20 @@ public function up(): void { Schema::table('applications', function (Blueprint $table) { - $table->string('registry_username')->nullable(); - $table->text('registry_token')->nullable(); + $table->string('docker_registry_username')->nullable(); + $table->text('docker_registry_token')->nullable(); + $table->string('docker_registry_url')->nullable(); + $table->boolean('docker_use_custom_registry')->default(false); }); } public function down(): void { Schema::table('applications', function (Blueprint $table) { - $table->dropColumn('registry_username'); - $table->dropColumn('registry_token'); + $table->dropColumn('docker_registry_username'); + $table->dropColumn('docker_registry_token'); + $table->dropColumn('docker_registry_url'); + $table->dropColumn('docker_use_custom_registry'); }); } }; \ No newline at end of file diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index ed50fd2308..2fa49c059f 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -101,34 +101,56 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" target="_blank">here. @endif @endif -
- @if ($application->build_pack === 'dockerimage') - @if ($application->destination->server->isSwarm()) - - +
+
+ @if ($application->build_pack === 'dockerimage') + @if ($application->destination->server->isSwarm()) + + + @else + + + @endif @else - - + @if ( + $application->destination->server->isSwarm() || + $application->additional_servers->count() > 0 || + $application->settings->is_build_server_enabled) + + + @else + + + @endif @endif - @else - @if ( - $application->destination->server->isSwarm() || - $application->additional_servers->count() > 0 || - $application->settings->is_build_server_enabled) - - - @else - - +
+ + @if ($application->build_pack === 'dockerimage') +
+ +
+ + @if ($application->docker_use_custom_registry) +
+ + + +
@endif @endif
diff --git a/resources/views/livewire/project/new/docker-image.blade.php b/resources/views/livewire/project/new/docker-image.blade.php index 5bc7df060f..c1bbbb1d95 100644 --- a/resources/views/livewire/project/new/docker-image.blade.php +++ b/resources/views/livewire/project/new/docker-image.blade.php @@ -8,16 +8,25 @@
-

Registry Authentication

-
- - - +
+
+ + @if ($useCustomRegistry) +

Registry Authentication

+
+ + + + + +
+ @endif
From 5a1431cf848695e13c5ad0c2fe018a74d158fadb Mon Sep 17 00:00:00 2001 From: David Buday Date: Fri, 8 Nov 2024 18:09:01 +0100 Subject: [PATCH 03/39] chore: remove encrypting token since it should be editable and visible like envs --- app/Jobs/ApplicationDeploymentJob.php | 52 ++++++++---------------- app/Livewire/Project/New/DockerImage.php | 2 +- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 10bb9a0f83..cc0d5b01fd 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -365,7 +365,7 @@ private function deploy_simple_dockerfile() $dockerfile_base64 = base64_encode($this->application->dockerfile); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}."); - $didLogin = $this->handleRegistryAuth(); + $this->prepare_builder_image(); $this->execute_remote_command( [ @@ -393,29 +393,27 @@ private function deploy_dockerimage_buildpack() } $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); - // login - $didLogin = $this->handleRegistryAuth(); + // login if use custom registry + if ($this->application->docker_use_custom_registry) { + $this->handleRegistryAuth(); + } $this->generate_image_names(); $this->prepare_builder_image(); $this->generate_compose_file(); $this->rolling_update(); - - - // Logout if we logged in - if ($didLogin) { + // Logout if use custom registry + if ($this->application->docker_use_custom_registry) { $this->application_deployment_queue->addLogEntry('Logging out from registry...'); $this->execute_remote_command([ 'docker logout', 'hidden' => true ]); } - - // Continue with the rest of the deployment... } catch (Exception $e) { - // Make sure to logout even if pull fails - if ($didLogin ?? false) { + // Make sure to logout even if build/pull fails + if ($this->application->docker_use_custom_registry) { $this->execute_remote_command([ 'docker logout', 'hidden' => true @@ -605,8 +603,6 @@ private function deploy_docker_compose_buildpack() private function deploy_dockerfile_buildpack() { - // login - $didLogin = $this->handleRegistryAuth(); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}."); if ($this->use_build_server) { $this->server = $this->build_server; @@ -2481,30 +2477,18 @@ public function failed(Throwable $exception): void private function handleRegistryAuth() { - if ($this->application->docker_use_custom_registry) { - try { - $username = escapeshellarg($this->application->docker_registry_username); - $token = escapeshellarg(decrypt($this->application->docker_registry_token)); + $username = escapeshellarg($this->application->docker_registry_username); + $token = escapeshellarg($this->application->docker_registry_token); - $registry = $this->application->docker_registry_url ?: 'docker.io'; // Default to docker.io - $registry = escapeshellarg($registry); + $registry = escapeshellarg($this->application->docker_registry_url ?: 'docker.io'); // Default to docker.io - $this->application_deployment_queue->addLogEntry('Attempting to log into registry...'); + $this->application_deployment_queue->addLogEntry('Attempting to log into registry...'); - $command = "echo {$token} | docker login {$registry} -u {$username} --password-stdin"; - $this->application_deployment_queue->addLogEntry($command); + $command = "echo {$token} | docker login {$registry} -u {$username} --password-stdin"; - $this->execute_remote_command([ - $command, - 'hidden' => true, - ]); - - return true; - } catch (Exception $e) { - $this->application_deployment_queue->addLogEntry('Registry authentication error: ' . $e->getMessage(), 'stderr'); - throw $e; - } - } - return false; + $this->execute_remote_command([ + $command, + 'hidden' => true, + ]); } } diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index e201c203d6..016ba1e698 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -85,7 +85,7 @@ public function submit() 'docker_use_custom_registry' => $this->useCustomRegistry, 'docker_registry_url' => $this->registryUrl, 'docker_registry_username' => $this->registryUsername, - 'docker_registry_token' => $this->registryToken ? encrypt($this->registryToken) : null, + 'docker_registry_token' => $this->registryToken, ]); $fqdn = generateFqdn($destination->server, $application->uuid); From 5187324b03e191643f6e6cafb622bc8433763a51 Mon Sep 17 00:00:00 2001 From: David Buday Date: Fri, 8 Nov 2024 18:10:25 +0100 Subject: [PATCH 04/39] fix: revert change --- app/Jobs/ApplicationDeploymentJob.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index cc0d5b01fd..ac295e9aaa 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1982,7 +1982,7 @@ private function pull_latest_image($image) $this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry."); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "docker pull {$this->application->registry_url}/{$image}"), + executeInDocker($this->deployment_uuid, "docker pull {$image}"), 'hidden' => true, ] ); From 3130b44340011e060fa7774789c34ee158ef91de Mon Sep 17 00:00:00 2001 From: David Buday Date: Fri, 8 Nov 2024 18:12:28 +0100 Subject: [PATCH 05/39] fix: revert some more for less confusion --- app/Livewire/Project/New/DockerImage.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 016ba1e698..ae7f385863 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -55,19 +55,17 @@ public function submit() 'latest'; $destination_uuid = $this->query['destination']; - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first() - ?? SwarmDocker::where('uuid', $destination_uuid)->first(); - - if (!$destination) { - throw new \Exception('Destination not found.'); + $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(); + $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $application = Application::create([ 'name' => 'docker-image-'.new Cuid2, From 02d29d93b47211294e164aa4777d2b87ed0fb7b9 Mon Sep 17 00:00:00 2001 From: David Buday Date: Fri, 8 Nov 2024 19:20:21 +0100 Subject: [PATCH 06/39] chore: info tooltip --- resources/views/livewire/project/application/general.blade.php | 1 + resources/views/livewire/project/new/docker-image.blade.php | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index be7f35c1a4..5beeb90e01 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -139,6 +139,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
diff --git a/resources/views/livewire/project/new/docker-image.blade.php b/resources/views/livewire/project/new/docker-image.blade.php index c1bbbb1d95..96cd84075a 100644 --- a/resources/views/livewire/project/new/docker-image.blade.php +++ b/resources/views/livewire/project/new/docker-image.blade.php @@ -10,6 +10,7 @@
From ebf207296b558a669e901593087556bd8c9cb142 Mon Sep 17 00:00:00 2001 From: David Buday Date: Sat, 9 Nov 2024 19:08:53 +0100 Subject: [PATCH 07/39] chore: encrypt with casts + masking and interpolation + fixes --- app/Jobs/ApplicationDeploymentJob.php | 22 ++++++----- app/Livewire/Project/Application/General.php | 5 ++- app/Livewire/Project/New/DockerImage.php | 8 ++-- app/Models/Application.php | 4 ++ app/Traits/ExecuteRemoteCommand.php | 39 +++++++++++++++++-- .../project/new/docker-image.blade.php | 4 +- 6 files changed, 61 insertions(+), 21 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index ac295e9aaa..66fcb8ee5a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2477,18 +2477,20 @@ public function failed(Throwable $exception): void private function handleRegistryAuth() { - $username = escapeshellarg($this->application->docker_registry_username); + $username = $this->application->docker_registry_username; + $registry = $this->application->docker_registry_url ?: 'docker.io'; $token = escapeshellarg($this->application->docker_registry_token); - - $registry = escapeshellarg($this->application->docker_registry_url ?: 'docker.io'); // Default to docker.io - $this->application_deployment_queue->addLogEntry('Attempting to log into registry...'); + $command = "echo {{secrets.token}} | docker login {$registry} -u {$username} --password-stdin"; - $command = "echo {$token} | docker login {$registry} -u {$username} --password-stdin"; - - $this->execute_remote_command([ - $command, - 'hidden' => true, - ]); + $this->execute_remote_command( + [ + 'command' => $command, + 'secrets' => [ + 'token' => $token, + ], + 'hidden' => true, + ] + ); } } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 4ad57d2a7a..6d9cd8765e 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -73,8 +73,8 @@ class General extends Component 'application.docker_registry_image_tag' => 'nullable', 'application.docker_use_custom_registry' => 'boolean', 'application.docker_registry_url' => 'nullable', - 'application.docker_registry_username' => 'required_with:application.docker_use_custom_registry', - 'application.docker_registry_token' => 'required_with:application.docker_use_custom_registry', + 'application.docker_registry_username' => 'nullable|required_if:application.docker_use_custom_registry,true', + 'application.docker_registry_token' => 'nullable|required_if:application.docker_use_custom_registry,true', 'application.dockerfile_location' => 'nullable', 'application.docker_compose_location' => 'nullable', 'application.docker_compose' => 'nullable', @@ -154,6 +154,7 @@ public function mount() $this->application->fqdn = null; $this->application->settings->save(); } + $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; diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index ae7f385863..4e70a7e4bc 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -21,8 +21,8 @@ class DockerImage extends Component protected $rules = [ 'dockerImage' => 'required|string', - 'registryUsername' => 'required_with:useCustomRegistry|string', - 'registryToken' => 'required_with:useCustomRegistry|string', + 'registryUsername' => 'required_if:useCustomRegistry,true|string|nullable', + 'registryToken' => 'required_if:useCustomRegistry,true|string|nullable', 'registryUrl' => 'nullable|string', 'useCustomRegistry' => 'boolean' ]; @@ -38,8 +38,8 @@ public function submit() { $this->validate([ 'dockerImage' => 'required', - 'registryUsername' => 'required_with:useCustomRegistry', - 'registryToken' => 'required_with:useCustomRegistry', + 'registryUsername' => 'required_if:useCustomRegistry,true', + 'registryToken' => 'required_if:useCustomRegistry,true', ]); // Only save registry settings if useCustomRegistry is true diff --git a/app/Models/Application.php b/app/Models/Application.php index 2180b78a37..c377d29511 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -113,6 +113,10 @@ class Application extends BaseModel protected $guarded = []; + protected $casts = [ + 'docker_registry_token' => 'encrypted', + ]; + protected $appends = ['server_status']; protected static function booted() diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index f8ccee9db7..509a898d10 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -36,6 +36,10 @@ public function execute_remote_command(...$commands) $ignore_errors = data_get($single_command, 'ignore_errors', false); $append = data_get($single_command, 'append', true); $this->save = data_get($single_command, 'save'); + $secrets = data_get($single_command, 'secrets', []); // Secrets for interpolation and masking + if (count($secrets) > 0) { + $command = $this->interpolateCommand($command, $secrets); + } if ($this->server->isNonRoot()) { if (str($command)->startsWith('docker exec')) { $command = str($command)->replace('docker exec', 'sudo docker exec'); @@ -44,10 +48,14 @@ public function execute_remote_command(...$commands) } } $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command); - $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { + $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $secrets, $hidden, $customType, $append) { $output = str($output)->trim(); - if ($output->startsWith('╔')) { - $output = "\n".$output; + if (count($secrets) > 0) { + $output = $this->maskSecrets($output, $secrets); + $command = $this->maskSecrets($command, $secrets); + } + if (str($output)->startsWith('╔')) { + $output = "\n" . $output; } $new_log_entry = [ 'command' => remove_iip($command), @@ -93,4 +101,29 @@ public function execute_remote_command(...$commands) } }); } + + private function interpolateCommand(string $command, array $secrets): string + { + foreach ($secrets as $key => $value) { + // Define the placeholder format + $placeholder = "{{secrets.$key}}"; + // Replace placeholder with actual value + $command = str_replace($placeholder, $value, $command); + } + return $command; + } + + private function maskSecrets(string $text, array $secrets): string + { + // Sort secrets by length descending to prevent partial masking + usort($secrets, function ($a, $b) { + return strlen($b) - strlen($a); + }); + + foreach ($secrets as $value) { + // Replace each secret value with '*****' + $text = str_replace($value, '*****', $text); + } + return $text; + } } diff --git a/resources/views/livewire/project/new/docker-image.blade.php b/resources/views/livewire/project/new/docker-image.blade.php index 96cd84075a..7af0ffcfdd 100644 --- a/resources/views/livewire/project/new/docker-image.blade.php +++ b/resources/views/livewire/project/new/docker-image.blade.php @@ -21,11 +21,11 @@ helper="Leave empty for Docker Hub" /> @endif From 9d08eed7f68033259e1994b6f93761f6a5091798 Mon Sep 17 00:00:00 2001 From: David Buday Date: Sun, 10 Nov 2024 17:00:38 +0100 Subject: [PATCH 08/39] chore: small improvement --- app/Jobs/ApplicationDeploymentJob.php | 80 +++++++++++++-------------- app/Traits/ExecuteRemoteCommand.php | 44 +++++++-------- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 66fcb8ee5a..ffc0a5051e 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -196,8 +196,8 @@ public function __construct(int $application_deployment_queue_id) $this->preserveRepository = $this->application->settings->is_preserve_repository_enabled; $this->basedir = $this->application->generateBaseDir($this->deployment_uuid); - $this->workdir = "{$this->basedir}".rtrim($this->application->base_directory, '/'); - $this->configuration_dir = application_configuration_dir()."/{$this->application->uuid}"; + $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/'); + $this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}"; $this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); @@ -397,12 +397,12 @@ private function deploy_dockerimage_buildpack() if ($this->application->docker_use_custom_registry) { $this->handleRegistryAuth(); } - + $this->generate_image_names(); $this->prepare_builder_image(); $this->generate_compose_file(); $this->rolling_update(); - + // Logout if use custom registry if ($this->application->docker_use_custom_registry) { $this->application_deployment_queue->addLogEntry('Logging out from registry...'); @@ -419,7 +419,7 @@ private function deploy_dockerimage_buildpack() 'hidden' => true ]); } - $this->application_deployment_queue->addLogEntry('Deployment error: ' . $e->getMessage(), 'stderr'); + //$this->application_deployment_queue->addLogEntry('Deployment error: ' . $e->getMessage(), 'stderr'); throw $e; } } @@ -432,13 +432,13 @@ private function deploy_docker_compose_buildpack() if (data_get($this->application, 'docker_compose_custom_start_command')) { $this->docker_compose_custom_start_command = $this->application->docker_compose_custom_start_command; if (! str($this->docker_compose_custom_start_command)->contains('--project-directory')) { - $this->docker_compose_custom_start_command = str($this->docker_compose_custom_start_command)->replaceFirst('compose', 'compose --project-directory '.$this->workdir)->value(); + $this->docker_compose_custom_start_command = str($this->docker_compose_custom_start_command)->replaceFirst('compose', 'compose --project-directory ' . $this->workdir)->value(); } } if (data_get($this->application, 'docker_compose_custom_build_command')) { $this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command; if (! str($this->docker_compose_custom_build_command)->contains('--project-directory')) { - $this->docker_compose_custom_build_command = str($this->docker_compose_custom_build_command)->replaceFirst('compose', 'compose --project-directory '.$this->workdir)->value(); + $this->docker_compose_custom_build_command = str($this->docker_compose_custom_build_command)->replaceFirst('compose', 'compose --project-directory ' . $this->workdir)->value(); } } if ($this->pull_request_id === 0) { @@ -452,7 +452,7 @@ private function deploy_docker_compose_buildpack() if ($this->preserveRepository) { foreach ($this->application->fileStorages as $fileStorage) { $path = $fileStorage->fs_path; - $saveName = 'file_stat_'.$fileStorage->id; + $saveName = 'file_stat_' . $fileStorage->id; $realPathInGit = str($path)->replace($this->application->workdir(), $this->workdir)->value(); // check if the file is a directory or a file inside the repository $this->execute_remote_command( @@ -947,12 +947,12 @@ private function save_environment_variables() $real_value = $env->real_value; } else { if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; + $real_value = '\'' . $real_value . '\''; } else { $real_value = escapeEnvVariables($env->real_value); } } - $envs->push($env->key.'='.$real_value); + $envs->push($env->key . '=' . $real_value); } // Add PORT if not exists, use the first port as default if ($this->build_pack !== 'dockercompose') { @@ -1006,12 +1006,12 @@ private function save_environment_variables() $real_value = $env->real_value; } else { if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; + $real_value = '\'' . $real_value . '\''; } else { $real_value = escapeEnvVariables($env->real_value); } } - $envs->push($env->key.'='.$real_value); + $envs->push($env->key . '=' . $real_value); } // Add PORT if not exists, use the first port as default if ($this->build_pack !== 'dockercompose') { @@ -1419,7 +1419,7 @@ private function deploy_to_additional_destinations() destination: $destination, no_questions_asked: true, ); - $this->application_deployment_queue->addLogEntry("Deployment to {$server->name}. Logs: ".route('project.application.deployment.show', [ + $this->application_deployment_queue->addLogEntry("Deployment to {$server->name}. Logs: " . route('project.application.deployment.show', [ 'project_uuid' => data_get($this->application, 'environment.project.uuid'), 'application_uuid' => data_get($this->application, 'uuid'), 'deployment_uuid' => $deployment_uuid, @@ -1759,27 +1759,27 @@ private function generate_compose_file() 'CMD-SHELL', $this->generate_healthcheck_commands(), ], - 'interval' => $this->application->health_check_interval.'s', - 'timeout' => $this->application->health_check_timeout.'s', + 'interval' => $this->application->health_check_interval . 's', + 'timeout' => $this->application->health_check_timeout . 's', 'retries' => $this->application->health_check_retries, - 'start_period' => $this->application->health_check_start_period.'s', + 'start_period' => $this->application->health_check_start_period . 's', ]; if (! is_null($this->application->limits_cpuset)) { - data_set($docker_compose, 'services.'.$this->container_name.'.cpuset', $this->application->limits_cpuset); + data_set($docker_compose, 'services.' . $this->container_name . '.cpuset', $this->application->limits_cpuset); } if ($this->server->isSwarm()) { - data_forget($docker_compose, 'services.'.$this->container_name.'.container_name'); - data_forget($docker_compose, 'services.'.$this->container_name.'.expose'); - data_forget($docker_compose, 'services.'.$this->container_name.'.restart'); - - data_forget($docker_compose, 'services.'.$this->container_name.'.mem_limit'); - data_forget($docker_compose, 'services.'.$this->container_name.'.memswap_limit'); - data_forget($docker_compose, 'services.'.$this->container_name.'.mem_swappiness'); - data_forget($docker_compose, 'services.'.$this->container_name.'.mem_reservation'); - data_forget($docker_compose, 'services.'.$this->container_name.'.cpus'); - data_forget($docker_compose, 'services.'.$this->container_name.'.cpuset'); - data_forget($docker_compose, 'services.'.$this->container_name.'.cpu_shares'); + data_forget($docker_compose, 'services.' . $this->container_name . '.container_name'); + data_forget($docker_compose, 'services.' . $this->container_name . '.expose'); + data_forget($docker_compose, 'services.' . $this->container_name . '.restart'); + + data_forget($docker_compose, 'services.' . $this->container_name . '.mem_limit'); + data_forget($docker_compose, 'services.' . $this->container_name . '.memswap_limit'); + data_forget($docker_compose, 'services.' . $this->container_name . '.mem_swappiness'); + data_forget($docker_compose, 'services.' . $this->container_name . '.mem_reservation'); + data_forget($docker_compose, 'services.' . $this->container_name . '.cpus'); + data_forget($docker_compose, 'services.' . $this->container_name . '.cpuset'); + data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares'); $docker_compose['services'][$this->container_name]['deploy'] = [ 'mode' => 'replicated', @@ -1841,20 +1841,20 @@ private function generate_compose_file() } } if ($this->application->isHealthcheckDisabled()) { - data_forget($docker_compose, 'services.'.$this->container_name.'.healthcheck'); + data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck'); } if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) { $docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array; } if (count($persistent_storages) > 0) { - if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) { + if (! data_get($docker_compose, 'services.' . $this->container_name . '.volumes')) { $docker_compose['services'][$this->container_name]['volumes'] = []; } $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_storages); } if (count($persistent_file_volumes) > 0) { - if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) { + if (! data_get($docker_compose, 'services.' . $this->container_name . '.volumes')) { $docker_compose['services'][$this->container_name]['volumes'] = []; } $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_file_volumes->map(function ($item) { @@ -1922,9 +1922,9 @@ private function generate_local_persistent_volumes() $volume_name = $persistentStorage->name; } if ($this->pull_request_id !== 0) { - $volume_name = $volume_name.'-pr-'.$this->pull_request_id; + $volume_name = $volume_name . '-pr-' . $this->pull_request_id; } - $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; } return $local_persistent_volumes; @@ -1940,7 +1940,7 @@ private function generate_local_persistent_volumes_only_volume_names() $name = $persistentStorage->name; if ($this->pull_request_id !== 0) { - $name = $name.'-pr-'.$this->pull_request_id; + $name = $name . '-pr-' . $this->pull_request_id; } $local_persistent_volumes_names[$name] = [ @@ -2248,7 +2248,7 @@ private function graceful_shutdown_container(string $containerName, int $timeout ); } } catch (\Exception $error) { - $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr'); + $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: " . $error->getMessage(), 'stderr'); } $this->remove_container($containerName); @@ -2271,7 +2271,7 @@ private function stop_running_container(bool $force = false) $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); if ($this->pull_request_id === 0) { $containers = $containers->filter(function ($container) { - return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id; + return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name . '-pr-' . $this->pull_request_id; }); } $containers->each(function ($container) { @@ -2375,8 +2375,8 @@ private function run_pre_deployment_command() foreach ($containers as $container) { $containerName = data_get($container, 'Names'); - if ($containers->count() == 1 || str_starts_with($containerName, $this->application->pre_deployment_command_container.'-'.$this->application->uuid)) { - $cmd = "sh -c '".str_replace("'", "'\''", $this->application->pre_deployment_command)."'"; + if ($containers->count() == 1 || str_starts_with($containerName, $this->application->pre_deployment_command_container . '-' . $this->application->uuid)) { + $cmd = "sh -c '" . str_replace("'", "'\''", $this->application->pre_deployment_command) . "'"; $exec = "docker exec {$containerName} {$cmd}"; $this->execute_remote_command( [ @@ -2402,8 +2402,8 @@ private function run_post_deployment_command() $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); foreach ($containers as $container) { $containerName = data_get($container, 'Names'); - if ($containers->count() == 1 || str_starts_with($containerName, $this->application->post_deployment_command_container.'-'.$this->application->uuid)) { - $cmd = "sh -c '".str_replace("'", "'\''", $this->application->post_deployment_command)."'"; + if ($containers->count() == 1 || str_starts_with($containerName, $this->application->post_deployment_command_container . '-' . $this->application->uuid)) { + $cmd = "sh -c '" . str_replace("'", "'\''", $this->application->post_deployment_command) . "'"; $exec = "docker exec {$containerName} {$cmd}"; try { $this->execute_remote_command( diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 509a898d10..2bebec587b 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -23,7 +23,7 @@ public function execute_remote_command(...$commands) } else { $commandsText = collect($commands); } - if ($this->server instanceof Server === false) { + if (! $this->server instanceof Server) { throw new \RuntimeException('Server is not set or is not an instance of Server model'); } $commandsText->each(function ($single_command) { @@ -36,9 +36,9 @@ public function execute_remote_command(...$commands) $ignore_errors = data_get($single_command, 'ignore_errors', false); $append = data_get($single_command, 'append', true); $this->save = data_get($single_command, 'save'); - $secrets = data_get($single_command, 'secrets', []); // Secrets for interpolation and masking - if (count($secrets) > 0) { - $command = $this->interpolateCommand($command, $secrets); + $secrets = data_get($single_command, 'secrets', []); + if (!empty($secrets)) { + $command = $this->replaceSecrets($command, $secrets); } if ($this->server->isNonRoot()) { if (str($command)->startsWith('docker exec')) { @@ -50,7 +50,7 @@ public function execute_remote_command(...$commands) $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command); $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $secrets, $hidden, $customType, $append) { $output = str($output)->trim(); - if (count($secrets) > 0) { + if (!empty($secrets)) { $output = $this->maskSecrets($output, $secrets); $command = $this->maskSecrets($command, $secrets); } @@ -68,7 +68,11 @@ public function execute_remote_command(...$commands) if (! $this->application_deployment_queue->logs) { $new_log_entry['order'] = 1; } else { - $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); + $previous_logs = json_decode( + $this->application_deployment_queue->logs, + associative: true, + flags: JSON_THROW_ON_ERROR + ); $new_log_entry['order'] = count($previous_logs) + 1; } $previous_logs[] = $new_log_entry; @@ -102,27 +106,23 @@ public function execute_remote_command(...$commands) }); } - private function interpolateCommand(string $command, array $secrets): string + private function replaceSecrets(string $text, array $secrets): string { - foreach ($secrets as $key => $value) { - // Define the placeholder format - $placeholder = "{{secrets.$key}}"; - // Replace placeholder with actual value - $command = str_replace($placeholder, $value, $command); - } - return $command; + return preg_replace_callback( + '/\{\{secrets\.(\w+)\}\}/', + fn($match) => $secrets[$match[1]] ?? $match[0], + $text + ); } private function maskSecrets(string $text, array $secrets): string { - // Sort secrets by length descending to prevent partial masking - usort($secrets, function ($a, $b) { - return strlen($b) - strlen($a); - }); - - foreach ($secrets as $value) { - // Replace each secret value with '*****' - $text = str_replace($value, '*****', $text); + // Sort by length to prevent partial matches + $sortedSecrets = collect($secrets)->sortByDesc(fn($value) => strlen($value)); + foreach ($sortedSecrets as $value) { + if (!empty($value)) { + $text = str_replace($value, '******', $text); + } } return $text; } From 3cd185aa789cd191fee6ab599df71dfb5563af20 Mon Sep 17 00:00:00 2001 From: David Buday Date: Mon, 11 Nov 2024 10:26:24 +0100 Subject: [PATCH 09/39] chore: changed migrations, created registries, initial progress --- app/Jobs/ApplicationDeploymentJob.php | 10 ++-- app/Livewire/Project/Application/General.php | 23 +++++--- app/Livewire/Project/New/DockerImage.php | 43 +++++--------- app/Models/Application.php | 57 ++++++++++--------- app/Models/Registry.php | 39 +++++++++++++ ...43_add_registry_to_applications_table.php} | 10 +--- ...4_11_10_203115_create_registries_table.php | 38 +++++++++++++ .../project/application/general.blade.php | 20 +++---- 8 files changed, 152 insertions(+), 88 deletions(-) create mode 100644 app/Models/Registry.php rename database/migrations/{2024_11_08_084443_add_registry_auth_to_applications_table.php => 2024_11_08_084443_add_registry_to_applications_table.php} (60%) create mode 100644 database/migrations/2024_11_10_203115_create_registries_table.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index ffc0a5051e..e480a6fc7f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2477,11 +2477,13 @@ public function failed(Throwable $exception): void private function handleRegistryAuth() { - $username = $this->application->docker_registry_username; - $registry = $this->application->docker_registry_url ?: 'docker.io'; - $token = escapeshellarg($this->application->docker_registry_token); + //$registry = $this->$application->registry; ?? + //$token = escapeshellarg($registry->token); ... + $token = escapeshellarg('test'); + $url = escapeshellarg('test'); + $username = escapeshellarg('test'); $this->application_deployment_queue->addLogEntry('Attempting to log into registry...'); - $command = "echo {{secrets.token}} | docker login {$registry} -u {$username} --password-stdin"; + $command = "echo {{secrets.token}} | docker login {$url} -u {$username} --password-stdin"; $this->execute_remote_command( [ diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 6d9cd8765e..d1e905dd9b 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -4,6 +4,7 @@ use App\Actions\Application\GenerateConfig; use App\Models\Application; +use App\Models\Registry; use Illuminate\Support\Collection; use Livewire\Component; use Spatie\Url\Url; @@ -72,9 +73,7 @@ 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_url' => 'nullable', - 'application.docker_registry_username' => 'nullable|required_if:application.docker_use_custom_registry,true', - 'application.docker_registry_token' => 'nullable|required_if:application.docker_use_custom_registry,true', + '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', @@ -116,10 +115,8 @@ class General extends Component 'application.dockerfile' => 'Dockerfile', 'application.docker_registry_image_name' => 'Docker registry image name', 'application.docker_registry_image_tag' => 'Docker registry image tag', - 'application.docker_use_custom_registry' => 'Docker use custom registry', - 'application.docker_registry_url' => 'Registry URL', - 'application.docker_registry_username' => 'Registry Username', - 'application.docker_registry_token' => 'Registry Token', + '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', @@ -322,7 +319,7 @@ public function checkFqdns($showToaster = true) public function set_redirect() { try { - $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count(); + $has_www = collect($this->application->fqdns)->filter(fn($fqdn) => str($fqdn)->contains('www.'))->count(); if ($has_www === 0 && $this->application->redirect === 'www') { $this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.

Please add www to your domain list and as an A DNS record (if applicable).'); @@ -381,6 +378,7 @@ public function submit($showToaster = true) 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', ]); } @@ -439,7 +437,14 @@ public function downloadConfig() echo $config; }, $fileName, [ 'Content-Type' => 'application/json', - 'Content-Disposition' => 'attachment; filename='.$fileName, + 'Content-Disposition' => 'attachment; filename=' . $fileName, + ]); + } + + public function render() + { + return view('livewire.project.application.general', [ + 'registries' => Registry::all(), ]); } } diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 4e70a7e4bc..936e22b5e9 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\New; use App\Models\Application; +use App\Models\Registry; use App\Models\Project; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; @@ -12,18 +13,14 @@ class DockerImage extends Component { public string $dockerImage = ''; - public ?string $registryUsername = null; - public ?string $registryToken = null; - public ?string $registryUrl = 'docker.io'; public bool $useCustomRegistry = false; + public ?int $selectedRegistry = null; public array $parameters; public array $query; protected $rules = [ 'dockerImage' => 'required|string', - 'registryUsername' => 'required_if:useCustomRegistry,true|string|nullable', - 'registryToken' => 'required_if:useCustomRegistry,true|string|nullable', - 'registryUrl' => 'nullable|string', + 'selectedRegistry' => 'nullable|required_if:useCustomRegistry,true', 'useCustomRegistry' => 'boolean' ]; @@ -31,27 +28,15 @@ public function mount() { $this->parameters = get_route_parameters(); $this->query = request()->query(); - $this->registryUrl = 'docker.io'; } public function submit() { - $this->validate([ - 'dockerImage' => 'required', - 'registryUsername' => 'required_if:useCustomRegistry,true', - 'registryToken' => 'required_if:useCustomRegistry,true', - ]); - - // Only save registry settings if useCustomRegistry is true - if (!$this->useCustomRegistry) { - $this->registryUsername = null; - $this->registryToken = null; - $this->registryUrl = 'docker.io'; - } - + $this->validate(['dockerImage' => 'required',]); + $image = str($this->dockerImage)->before(':'); - $tag = str($this->dockerImage)->contains(':') ? - str($this->dockerImage)->after(':') : + $tag = str($this->dockerImage)->contains(':') ? + str($this->dockerImage)->after(':') : 'latest'; $destination_uuid = $this->query['destination']; @@ -68,7 +53,7 @@ public function submit() $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $application = Application::create([ - 'name' => 'docker-image-'.new Cuid2, + 'name' => 'docker-image-' . new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', @@ -76,19 +61,17 @@ public function submit() 'ports_exposes' => 80, 'docker_registry_image_name' => $image, 'docker_registry_image_tag' => $tag, + 'docker_use_custom_registry' => $this->useCustomRegistry, + 'docker_registry_id' => $this->selectedRegistry, 'environment_id' => $environment->id, 'destination_id' => $destination->id, 'destination_type' => $destination_class, 'health_check_enabled' => false, - 'docker_use_custom_registry' => $this->useCustomRegistry, - 'docker_registry_url' => $this->registryUrl, - 'docker_registry_username' => $this->registryUsername, - 'docker_registry_token' => $this->registryToken, ]); $fqdn = generateFqdn($destination->server, $application->uuid); $application->update([ - 'name' => 'docker-image-'.$application->uuid, + 'name' => 'docker-image-' . $application->uuid, 'fqdn' => $fqdn, ]); @@ -101,6 +84,8 @@ public function submit() public function render() { - return view('livewire.project.new.docker-image'); + return view('livewire.project.new.docker-image', [ + 'registries' => Registry::all() + ]); } } diff --git a/app/Models/Application.php b/app/Models/Application.php index c377d29511..5cd1eb99b4 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -36,9 +36,7 @@ 'docker_registry_image_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image tag.'], 'docker_use_custom_registry' => ['type' => 'boolean', 'description' => 'Use custom registry.'], - 'docker_registry_url' => ['type' => 'string', 'nullable' => true, 'description' => 'Registry URL.'], - 'docker_registry_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Registry username.'], - 'docker_registry_token' => ['type' => 'string', 'nullable' => true, 'description' => 'Registry token.'], + 'docker_registry_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Docker registry identifier.'], 'build_pack' => ['type' => 'string', 'description' => 'Build pack.', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose']], 'static_image' => ['type' => 'string', 'description' => 'Static image used when static site is deployed.'], 'install_command' => ['type' => 'string', 'description' => 'Install command.'], @@ -113,12 +111,10 @@ class Application extends BaseModel protected $guarded = []; - protected $casts = [ - 'docker_registry_token' => 'encrypted', - ]; - protected $appends = ['server_status']; + protected $with = ['registry']; + protected static function booted() { static::saving(function ($application) { @@ -243,7 +239,7 @@ public function delete_configurations() $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(['rm -rf '.$this->workdir()], $server, false); + instant_remote_process(['rm -rf ' . $this->workdir()], $server, false); } } @@ -377,7 +373,7 @@ public function type() public function publishDirectory(): Attribute { return Attribute::make( - set: fn ($value) => $value ? '/'.ltrim($value, '/') : null, + set: fn($value) => $value ? '/' . ltrim($value, '/') : null, ); } @@ -459,7 +455,7 @@ public function gitCommitLink($link): string $git_repository = str_replace('.git', '', $this->git_repository); $url = Url::fromString($git_repository); $url = $url->withUserInfo(''); - $url = $url->withPath($url->getPath().'/commits/'.$link); + $url = $url->withPath($url->getPath() . '/commits/' . $link); return $url->__toString(); } @@ -512,21 +508,21 @@ public function dockerComposeLocation(): Attribute public function baseDirectory(): Attribute { return Attribute::make( - set: fn ($value) => '/'.ltrim($value, '/'), + set: fn($value) => '/' . ltrim($value, '/'), ); } public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === '' ? null : $value, + set: fn($value) => $value === '' ? null : $value, ); } public function portsMappingsArray(): Attribute { return Attribute::make( - get: fn () => is_null($this->ports_mappings) + get: fn() => is_null($this->ports_mappings) ? [] : explode(',', $this->ports_mappings), @@ -643,7 +639,7 @@ public function status(): Attribute public function portsExposesArray(): Attribute { return Attribute::make( - get: fn () => is_null($this->ports_exposes) + get: fn() => is_null($this->ports_exposes) ? [] : explode(',', $this->ports_exposes) ); @@ -860,7 +856,7 @@ public function isHealthcheckDisabled(): bool public function workdir() { - return application_configuration_dir()."/{$this->uuid}"; + return application_configuration_dir() . "/{$this->uuid}"; } public function isLogDrainEnabled() @@ -870,7 +866,7 @@ public function isLogDrainEnabled() public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect; + $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build . $this->redirect; if ($this->pull_request_id === 0 || $this->pull_request_id === null) { $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); } else { @@ -924,7 +920,7 @@ public function generateBaseDir(string $uuid) public function dirOnServer() { - return application_configuration_dir()."/{$this->uuid}"; + return application_configuration_dir() . "/{$this->uuid}"; } public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false) @@ -1048,7 +1044,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1056,14 +1052,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit); } } @@ -1092,7 +1088,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1100,14 +1096,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit); } } @@ -1160,7 +1156,7 @@ public function oldRawParser() } if ($source->startsWith('.')) { $source = $source->after('.'); - $source = $workdir.$source; + $source = $workdir . $source; } $commands->push("mkdir -p $source > /dev/null 2>&1 || true"); } @@ -1171,7 +1167,7 @@ public function oldRawParser() $labels->push('coolify.managed=true'); } if (! $labels->contains('coolify.applicationId')) { - $labels->push('coolify.applicationId='.$this->id); + $labels->push('coolify.applicationId=' . $this->id); } if (! $labels->contains('coolify.type')) { $labels->push('coolify.type=application'); @@ -1291,7 +1287,7 @@ public function parseContainerLabels(?ApplicationPreview $preview = null) public function fqdns(): Attribute { return Attribute::make( - get: fn () => is_null($this->fqdn) + get: fn() => is_null($this->fqdn) ? [] : explode(',', $this->fqdn), ); @@ -1352,10 +1348,10 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false continue; } if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) { - $healthcheckCommand .= ' '.trim($trimmedLine, '\\ '); + $healthcheckCommand .= ' ' . trim($trimmedLine, '\\ '); } if (isset($healthcheckCommand) && ! str_contains($trimmedLine, '\\') && ! empty($healthcheckCommand)) { - $healthcheckCommand .= ' '.$trimmedLine; + $healthcheckCommand .= ' ' . $trimmedLine; break; } } @@ -1537,4 +1533,9 @@ public function setConfig($config) throw new \Exception('Failed to update application settings'); } } + + public function registry() + { + return $this->belongsTo(Registry::class); + } } diff --git a/app/Models/Registry.php b/app/Models/Registry.php new file mode 100644 index 0000000000..8793815a2c --- /dev/null +++ b/app/Models/Registry.php @@ -0,0 +1,39 @@ + 'boolean', + 'token' => 'encrypted', + ]; + + public static function getTypes(): array + { + return [ + 'docker_hub' => 'Docker Hub', + 'gcr' => 'Google Container Registry', + 'ghcr' => 'GitHub Container Registry', + 'quay' => 'Quay.io', + 'custom' => 'Custom Registry' + ]; + } + + public function applications(): HasMany + { + return $this->hasMany(Application::class); + } +} diff --git a/database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php b/database/migrations/2024_11_08_084443_add_registry_to_applications_table.php similarity index 60% rename from database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php rename to database/migrations/2024_11_08_084443_add_registry_to_applications_table.php index 422503bfcc..c01657efe9 100644 --- a/database/migrations/2024_11_08_084443_add_registry_auth_to_applications_table.php +++ b/database/migrations/2024_11_08_084443_add_registry_to_applications_table.php @@ -8,9 +8,7 @@ public function up(): void { Schema::table('applications', function (Blueprint $table) { - $table->string('docker_registry_username')->nullable(); - $table->text('docker_registry_token')->nullable(); - $table->string('docker_registry_url')->nullable(); + $table->foreignId('docker_registry_id')->nullable(); $table->boolean('docker_use_custom_registry')->default(false); }); } @@ -18,10 +16,8 @@ public function up(): void public function down(): void { Schema::table('applications', function (Blueprint $table) { - $table->dropColumn('docker_registry_username'); - $table->dropColumn('docker_registry_token'); - $table->dropColumn('docker_registry_url'); + $table->dropColumn('docker_registry_id'); $table->dropColumn('docker_use_custom_registry'); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2024_11_10_203115_create_registries_table.php b/database/migrations/2024_11_10_203115_create_registries_table.php new file mode 100644 index 0000000000..01cb3e6542 --- /dev/null +++ b/database/migrations/2024_11_10_203115_create_registries_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('name'); + $table->string('type'); // docker_hub, gcr, ghcr, quay, custom + $table->string('url')->nullable(); + $table->string('username')->nullable(); + $table->text('token')->nullable(); + $table->boolean('is_default')->default(false); + $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/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 5beeb90e01..145c5575a5 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -137,20 +137,18 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @if ($application->build_pack === 'dockerimage')
- +
@if ($application->docker_use_custom_registry) -
- - - +
+ + @foreach ($registries as $registry) + + @endforeach +
@endif @endif From 42de7ec0f03da4d9d7c5840e00366e0b73dbadb3 Mon Sep 17 00:00:00 2001 From: David Buday Date: Mon, 11 Nov 2024 15:35:25 +0100 Subject: [PATCH 10/39] chore: database, navigation images --- app/Jobs/ApplicationDeploymentJob.php | 47 ++++++----- app/Livewire/Images/Images/Create.php | 0 app/Livewire/Images/Images/Index.php | 13 +++ app/Livewire/Images/Registry/Create.php | 50 +++++++++++ app/Livewire/Images/Registry/Index.php | 18 ++++ app/Livewire/Images/Registry/Show.php | 82 +++++++++++++++++++ app/Livewire/Project/Application/General.php | 4 +- app/Livewire/Project/New/DockerImage.php | 9 +- app/Models/Application.php | 2 +- .../{Registry.php => DockerRegistry.php} | 14 +--- ...4_11_10_203115_create_registries_table.php | 1 - .../views/components/images/navbar.blade.php | 14 ++++ resources/views/components/navbar.blade.php | 14 +++- .../livewire/images/images/index.blade.php | 16 ++++ .../livewire/images/registry/create.blade.php | 23 ++++++ .../livewire/images/registry/index.blade.php | 18 ++++ .../livewire/images/registry/show.blade.php | 32 ++++++++ .../project/application/general.blade.php | 4 +- .../project/new/docker-image.blade.php | 22 ++--- routes/web.php | 7 +- 20 files changed, 332 insertions(+), 58 deletions(-) create mode 100644 app/Livewire/Images/Images/Create.php create mode 100644 app/Livewire/Images/Images/Index.php create mode 100644 app/Livewire/Images/Registry/Create.php create mode 100644 app/Livewire/Images/Registry/Index.php create mode 100644 app/Livewire/Images/Registry/Show.php rename app/Models/{Registry.php => DockerRegistry.php} (72%) create mode 100644 resources/views/components/images/navbar.blade.php create mode 100644 resources/views/livewire/images/images/index.blade.php create mode 100644 resources/views/livewire/images/registry/create.blade.php create mode 100644 resources/views/livewire/images/registry/index.blade.php create mode 100644 resources/views/livewire/images/registry/show.blade.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index e480a6fc7f..957817813e 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -9,6 +9,7 @@ use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; +use App\Models\DockerRegistry; use App\Models\EnvironmentVariable; use App\Models\GithubApp; use App\Models\GitlabApp; @@ -383,6 +384,7 @@ private function deploy_simple_dockerfile() private function deploy_dockerimage_buildpack() { + $useCustomRegistry = $this->application->docker_use_custom_registry; try { // setup $this->dockerImage = $this->application->docker_registry_image_name; @@ -394,7 +396,7 @@ private function deploy_dockerimage_buildpack() $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); // login if use custom registry - if ($this->application->docker_use_custom_registry) { + if ($useCustomRegistry) { $this->handleRegistryAuth(); } @@ -402,25 +404,16 @@ private function deploy_dockerimage_buildpack() $this->prepare_builder_image(); $this->generate_compose_file(); $this->rolling_update(); - - // Logout if use custom registry - if ($this->application->docker_use_custom_registry) { - $this->application_deployment_queue->addLogEntry('Logging out from registry...'); - $this->execute_remote_command([ - 'docker logout', - 'hidden' => true - ]); - } } catch (Exception $e) { - // Make sure to logout even if build/pull fails - if ($this->application->docker_use_custom_registry) { + throw $e; + } finally { + if ($useCustomRegistry) { + $this->application_deployment_queue->addLogEntry('Logging out from registry...'); $this->execute_remote_command([ 'docker logout', 'hidden' => true ]); } - //$this->application_deployment_queue->addLogEntry('Deployment error: ' . $e->getMessage(), 'stderr'); - throw $e; } } @@ -2477,13 +2470,27 @@ public function failed(Throwable $exception): void private function handleRegistryAuth() { - //$registry = $this->$application->registry; ?? - //$token = escapeshellarg($registry->token); ... - $token = escapeshellarg('test'); - $url = escapeshellarg('test'); - $username = escapeshellarg('test'); + $registry = DockerRegistry::find($this->application->docker_registry_id); + if (!$registry) { + throw new Exception('Registry not found.'); + } + + $token = escapeshellarg($registry->token); + $username = escapeshellarg($registry->username); + + // Handle different registry types + $url = match ($registry->type) { + 'docker_hub' => '', // Docker Hub doesn't need URL specified + 'custom' => escapeshellarg($registry->url), + default => escapeshellarg($registry->url) + }; + $this->application_deployment_queue->addLogEntry('Attempting to log into registry...'); - $command = "echo {{secrets.token}} | docker login {$url} -u {$username} --password-stdin"; + + // Build login command based on registry type + $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( [ diff --git a/app/Livewire/Images/Images/Create.php b/app/Livewire/Images/Images/Create.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/Livewire/Images/Images/Index.php b/app/Livewire/Images/Images/Index.php new file mode 100644 index 0000000000..4126ebddf7 --- /dev/null +++ b/app/Livewire/Images/Images/Index.php @@ -0,0 +1,13 @@ + 'required|string|max:255', + 'type' => 'required|string', + 'url' => 'nullable|string|max:255', + 'username' => 'nullable|string|max:255', + 'token' => 'nullable|string', + ]; + + public function getRegistryTypesProperty() + { + return DockerRegistry::getTypes(); + } + + public function submit() + { + $this->validate(); + + DockerRegistry::create([ + 'name' => $this->name, + 'type' => $this->type, + 'url' => $this->type === 'custom' ? $this->url : 'docker.io', + 'username' => $this->username, + 'token' => $this->token, + ]); + + $this->dispatch('registry-added'); + $this->dispatch('success', 'Registry added successfully.'); + $this->dispatch('close-modal'); + } + + public function render() + { + return view('livewire.images.registry.create'); + } +} diff --git a/app/Livewire/Images/Registry/Index.php b/app/Livewire/Images/Registry/Index.php new file mode 100644 index 0000000000..f612ebee20 --- /dev/null +++ b/app/Livewire/Images/Registry/Index.php @@ -0,0 +1,18 @@ + '$refresh']; + + public function render() + { + return view('livewire.images.registry.index', [ + 'registries' => DockerRegistry::all() + ]); + } +} diff --git a/app/Livewire/Images/Registry/Show.php b/app/Livewire/Images/Registry/Show.php new file mode 100644 index 0000000000..90069f5718 --- /dev/null +++ b/app/Livewire/Images/Registry/Show.php @@ -0,0 +1,82 @@ + 'required|string|max:255', + 'type' => 'required|string', + 'url' => 'nullable|string|max:255', + 'username' => 'nullable|string|max:255', + 'token' => 'nullable|string', + ]; + + public function mount(DockerRegistry $registry) + { + $this->registry = $registry; + $this->name = $registry->name; + $this->type = $registry->type; + $this->url = $registry->url; + $this->username = $registry->username; + $this->token = $registry->token; + } + + public function getRegistryTypesProperty() + { + return DockerRegistry::getTypes(); + } + + public function updateRegistry() + { + $this->validate(); + + $this->registry->update([ + 'name' => $this->name, + 'type' => $this->type, + 'url' => $this->type === 'custom' ? $this->url : 'docker.io', + 'username' => $this->username, + 'token' => $this->token, + ]); + + $this->dispatch('success', 'Registry updated successfully.'); + } + + public function delete() + { + // Update all applications using this registry + $this->registry->applications() + ->update([ + 'docker_registry_id' => null, + 'docker_use_custom_registry' => false + ]); + + $this->registry->delete(); + $this->dispatch('registry-added'); + $this->dispatch('success', 'Registry deleted successfully.'); + } + + public function render() + { + return view('livewire.images.registry.show'); + } + + public function getIsFormDirtyProperty(): bool + { + return $this->name !== $this->registry->name + || $this->type !== $this->registry->type + || $this->url !== $this->registry->url + || $this->username !== $this->registry->username + || $this->token !== $this->registry->token; + } +} diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index d1e905dd9b..72829c8de0 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -4,7 +4,7 @@ use App\Actions\Application\GenerateConfig; use App\Models\Application; -use App\Models\Registry; +use App\Models\DockerRegistry; use Illuminate\Support\Collection; use Livewire\Component; use Spatie\Url\Url; @@ -444,7 +444,7 @@ public function downloadConfig() public function render() { return view('livewire.project.application.general', [ - 'registries' => Registry::all(), + 'registries' => DockerRegistry::all(), ]); } } diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 936e22b5e9..7833a11b92 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -3,7 +3,7 @@ namespace App\Livewire\Project\New; use App\Models\Application; -use App\Models\Registry; +use App\Models\DockerRegistry; use App\Models\Project; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; @@ -20,8 +20,7 @@ class DockerImage extends Component protected $rules = [ 'dockerImage' => 'required|string', - 'selectedRegistry' => 'nullable|required_if:useCustomRegistry,true', - 'useCustomRegistry' => 'boolean' + 'selectedRegistry' => 'required_if:useCustomRegistry,true|nullable|exists:docker_registries,id' ]; public function mount() @@ -62,7 +61,7 @@ public function submit() 'docker_registry_image_name' => $image, 'docker_registry_image_tag' => $tag, 'docker_use_custom_registry' => $this->useCustomRegistry, - 'docker_registry_id' => $this->selectedRegistry, + 'docker_registry_id' => $this->selectedRegistry ?? null, 'environment_id' => $environment->id, 'destination_id' => $destination->id, 'destination_type' => $destination_class, @@ -85,7 +84,7 @@ public function submit() public function render() { return view('livewire.project.new.docker-image', [ - 'registries' => Registry::all() + 'registries' => DockerRegistry::all() ]); } } diff --git a/app/Models/Application.php b/app/Models/Application.php index 5cd1eb99b4..6ecc5f202f 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1536,6 +1536,6 @@ public function setConfig($config) public function registry() { - return $this->belongsTo(Registry::class); + return $this->belongsTo(DockerRegistry::class); } } diff --git a/app/Models/Registry.php b/app/Models/DockerRegistry.php similarity index 72% rename from app/Models/Registry.php rename to app/Models/DockerRegistry.php index 8793815a2c..c3638d0ad5 100644 --- a/app/Models/Registry.php +++ b/app/Models/DockerRegistry.php @@ -5,16 +5,10 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; -class Registry extends Model +// #[OA\Schema( +class DockerRegistry extends Model { - protected $fillable = [ - 'name', - 'type', - 'url', - 'username', - 'token', - 'is_default' - ]; + protected $guarded = []; protected $casts = [ 'is_default' => 'boolean', @@ -34,6 +28,6 @@ public static function getTypes(): array public function applications(): HasMany { - return $this->hasMany(Application::class); + return $this->hasMany(Application::class, 'docker_registry_id', 'id'); } } diff --git a/database/migrations/2024_11_10_203115_create_registries_table.php b/database/migrations/2024_11_10_203115_create_registries_table.php index 01cb3e6542..cc71abc064 100644 --- a/database/migrations/2024_11_10_203115_create_registries_table.php +++ b/database/migrations/2024_11_10_203115_create_registries_table.php @@ -14,7 +14,6 @@ public function up(): void $table->string('url')->nullable(); $table->string('username')->nullable(); $table->text('token')->nullable(); - $table->boolean('is_default')->default(false); $table->timestamps(); }); diff --git a/resources/views/components/images/navbar.blade.php b/resources/views/components/images/navbar.blade.php new file mode 100644 index 0000000000..c13b94c95f --- /dev/null +++ b/resources/views/components/images/navbar.blade.php @@ -0,0 +1,14 @@ +
+

Images

+
Images and container management.
+ +
diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index f635a67879..b80e44ad55 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -145,6 +145,16 @@ class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item' Sources +
  • + + + + Images + +
  • is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}" href="{{ route('storage.index') }}"> - + diff --git a/resources/views/livewire/images/images/index.blade.php b/resources/views/livewire/images/images/index.blade.php new file mode 100644 index 0000000000..c97d65a2af --- /dev/null +++ b/resources/views/livewire/images/images/index.blade.php @@ -0,0 +1,16 @@ +
    + +
    +

    Images

    + + + + +
    + {{-- Images Tab Content --}} +
    +
    + Image management coming soon... +
    +
    +
    diff --git a/resources/views/livewire/images/registry/create.blade.php b/resources/views/livewire/images/registry/create.blade.php new file mode 100644 index 0000000000..e25d913430 --- /dev/null +++ b/resources/views/livewire/images/registry/create.blade.php @@ -0,0 +1,23 @@ +
    + + + + @foreach ($this->registryTypes as $key => $value) + + @endforeach + + + @if ($type === 'custom') + + @endif + + + + + +
    + Save Registry +
    + diff --git a/resources/views/livewire/images/registry/index.blade.php b/resources/views/livewire/images/registry/index.blade.php new file mode 100644 index 0000000000..455b5c3b7d --- /dev/null +++ b/resources/views/livewire/images/registry/index.blade.php @@ -0,0 +1,18 @@ +
    + +
    +

    Registries

    + + + +
    +
    Configure registries to pull Docker images from.
    + + @forelse($registries as $registry) + + @empty +
    + No registries configured yet. Add one to get started. +
    + @endforelse +
    diff --git a/resources/views/livewire/images/registry/show.blade.php b/resources/views/livewire/images/registry/show.blade.php new file mode 100644 index 0000000000..6f68d4b7d0 --- /dev/null +++ b/resources/views/livewire/images/registry/show.blade.php @@ -0,0 +1,32 @@ +
    +
    +
    +
    + Update + +
    +
    + +
    + + + + @foreach ($this->registryTypes as $key => $value) + + @endforeach + + + @if ($type === 'custom') + + @endif + + + + +
    +
    +
    diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 145c5575a5..dc1aa64a3b 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -144,7 +144,9 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @if ($application->docker_use_custom_registry)
    - + + {{-- --}} @foreach ($registries as $registry) @endforeach diff --git a/resources/views/livewire/project/new/docker-image.blade.php b/resources/views/livewire/project/new/docker-image.blade.php index 7af0ffcfdd..e9cbf6dfe5 100644 --- a/resources/views/livewire/project/new/docker-image.blade.php +++ b/resources/views/livewire/project/new/docker-image.blade.php @@ -10,23 +10,17 @@
    + helper="Select a registry to pull the image from." label="Use Private Registry" />
    @if ($useCustomRegistry) -

    Registry Authentication

    -
    - - - - - +
    + + + @foreach ($registries as $registry) + + @endforeach +
    @endif diff --git a/routes/web.php b/routes/web.php index afe3920524..b860c011a5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -33,6 +33,8 @@ use App\Livewire\Project\Shared\Logs; use App\Livewire\Project\Shared\ScheduledTask\Show as ScheduledTaskShow; use App\Livewire\Project\Show as ProjectShow; +use App\Livewire\Images\Registry\Index as RegistryIndex; +use App\Livewire\Images\Images\Index as ImagesIndex; use App\Livewire\Security\ApiTokens; use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex; use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow; @@ -228,6 +230,8 @@ Route::get('/security/private-key/{private_key_uuid}', SecurityPrivateKeyShow::class)->name('security.private-key.show'); Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens'); + Route::get('/images/images', ImagesIndex::class)->name('images.images.index'); + Route::get('/images/registries', RegistryIndex::class)->name('images.registries.index'); }); Route::middleware(['auth'])->group(function () { @@ -306,13 +310,12 @@ fclose($stream); }, 200, [ 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="'.basename($filename).'"', + 'Content-Disposition' => 'attachment; filename="' . basename($filename) . '"', ]); } catch (\Throwable $e) { return response()->json(['message' => $e->getMessage()], 500); } })->name('download.backup'); - }); Route::any('/{any}', function () { From 1fd873758886d1a97dde5e06c803aa3cb80c3967 Mon Sep 17 00:00:00 2001 From: David Buday Date: Mon, 11 Nov 2024 15:38:23 +0100 Subject: [PATCH 11/39] fix: navbar link color --- resources/views/components/images/navbar.blade.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/views/components/images/navbar.blade.php b/resources/views/components/images/navbar.blade.php index c13b94c95f..e366de27b3 100644 --- a/resources/views/components/images/navbar.blade.php +++ b/resources/views/components/images/navbar.blade.php @@ -3,10 +3,12 @@
    Images and container management.
    @@ -97,7 +96,7 @@ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking- + class="" isError> Delete
    @@ -126,12 +125,22 @@ class="bg-red-600 hover:bg-red-700">

    Image Details

    - +
    + + Prune Unused + + Delete Image + + +
    + +
    -
    +

    ID:

    {{ $imageDetails[0]['Id'] ?? 'N/A' }}

    @@ -144,10 +153,10 @@ class="text-gray-500 hover:text-gray-700">×

    Size:

    {{ $imageDetails[0]['FormattedSize'] ?? 'N/A' }}

    -
    + {{--

    Container Count:

    {{ $imageDetails[0]['ContainerCount'] ?? 'N/A' }}

    -
    +
    --}}
    @if (isset($imageDetails[0]['Config'])) @@ -199,17 +208,7 @@ class="text-gray-500 hover:text-gray-700">×
    @endif -
    - - Prune Unused - - - Delete Image - -
    +
    From 5c2cf9f50979a931893f640234e48159afa29900 Mon Sep 17 00:00:00 2001 From: David Buday Date: Mon, 23 Dec 2024 15:55:56 +0100 Subject: [PATCH 36/39] chore: list and details progress --- app/Livewire/Images/Images/Index.php | 84 ++++-- .../livewire/images/images/index.blade.php | 269 ++++++++++++------ 2 files changed, 248 insertions(+), 105 deletions(-) diff --git a/app/Livewire/Images/Images/Index.php b/app/Livewire/Images/Images/Index.php index 79fbc44582..154f6afc66 100644 --- a/app/Livewire/Images/Images/Index.php +++ b/app/Livewire/Images/Images/Index.php @@ -22,6 +22,9 @@ class Index extends Component public string $searchQuery = ''; public bool $showOnlyDangling = false; public bool $selectAll = false; + public bool $showDeleteConfirmation = false; + public array $imagesToDelete = []; + public string $confirmationText = ''; public function mount() { @@ -50,7 +53,13 @@ public function loadServerImages() return; } - $this->serverImages = collect(ListServerDockerImages::run($server)); + $images = collect(ListServerDockerImages::run($server)); + + // Format sizes for the list + $this->serverImages = $images->map(function ($image) { + $image['FormattedSize'] = $this->formatBytes($image['Size'] ?? 0); + return $image; + }); } catch (\Exception $e) { $this->addError('images', "Error loading docker images: " . $e->getMessage()); } finally { @@ -65,24 +74,49 @@ public function getImageDetails($imageId) if (!$server) { return; } - $this->imageDetails = GetServerDockerImageDetails::run($server, $imageId); + $details = GetServerDockerImageDetails::run($server, $imageId); + + // Add formatted size (total size) + if (isset($details['Size'])) { + $details['FormattedSize'] = $this->formatBytes($details['Size']); + } else { + $details['FormattedSize'] = 'N/A'; + } - // Add formatted size - if (isset($this->imageDetails[0]['Size'])) { - $size = $this->imageDetails[0]['Size']; - $this->imageDetails[0]['FormattedSize'] = $this->formatBytes($size); + // Add formatted virtual size + if (isset($details['VirtualSize'])) { + $details['FormattedVirtualSize'] = $this->formatBytes($details['VirtualSize']); } // Add formatted creation date - if (isset($this->imageDetails[0]['Created'])) { - $this->imageDetails[0]['FormattedCreated'] = \Carbon\Carbon::parse($this->imageDetails[0]['Created'])->diffForHumans(); + if (isset($details['Created'])) { + $details['FormattedCreated'] = \Carbon\Carbon::parse($details['Created'])->diffForHumans(); + } else { + $details['FormattedCreated'] = 'N/A'; } + + $this->imageDetails = $details; } catch (\Exception $e) { $this->addError('details', "Error loading image details: " . $e->getMessage()); } } - public function deleteImage($imageId) + public function confirmDelete($imageIds = null) + { + if ($imageIds) { + $this->imagesToDelete = is_array($imageIds) ? $imageIds : [$imageIds]; + } else { + $this->imagesToDelete = $this->selectedImages; + } + + if (empty($this->imagesToDelete)) { + return; + } + + $this->showDeleteConfirmation = true; + } + + public function deleteImages() { try { $server = $this->servers->firstWhere('uuid', $this->selected_uuid); @@ -90,11 +124,18 @@ public function deleteImage($imageId) return; } - instant_remote_process(["docker rmi -f {$imageId}"], $server); + DeleteServerDockerImages::run($server, $this->imagesToDelete); + + // Reset states + $this->showDeleteConfirmation = false; + $this->imagesToDelete = []; + $this->selectedImages = []; $this->imageDetails = null; + $this->confirmationText = ''; + $this->loadServerImages(); } catch (\Exception $e) { - $this->addError('delete', "Error deleting image: " . $e->getMessage()); + $this->addError('delete', "Error deleting images: " . $e->getMessage()); } } @@ -118,14 +159,13 @@ public function getFilteredImagesProperty() return $this->serverImages ->when($this->searchQuery, function ($collection) { return $collection->filter(function ($image) { - return str_contains(strtolower($image['Repository'] ?? ''), strtolower($this->searchQuery)) || - str_contains(strtolower($image['Tag'] ?? ''), strtolower($this->searchQuery)) || - str_contains(strtolower($image['ID'] ?? ''), strtolower($this->searchQuery)); + return str_contains(strtolower($image['RepoTags'][0] ?? ''), strtolower($this->searchQuery)) || + str_contains(strtolower($image['Id'] ?? ''), strtolower($this->searchQuery)); }); }) ->when($this->showOnlyDangling, function ($collection) { return $collection->filter(function ($image) { - return ($image['Repository'] ?? '') === '' || ($image['Tag'] ?? '') === ''; + return $image['Dangling'] ?? false; }); }) ->values(); @@ -134,12 +174,24 @@ public function getFilteredImagesProperty() public function updatedSelectAll($value) { if ($value) { - $this->selectedImages = $this->filteredImages->pluck('ID')->toArray(); + $this->selectedImages = $this->filteredImages->pluck('Id')->toArray(); } else { $this->selectedImages = []; } } + public function updatedSearchQuery() + { + $this->selectAll = false; + $this->selectedImages = []; + } + + public function updatedShowOnlyDangling() + { + $this->selectAll = false; + $this->selectedImages = []; + } + protected function formatBytes($bytes, $precision = 2) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; diff --git a/resources/views/livewire/images/images/index.blade.php b/resources/views/livewire/images/images/index.blade.php index b16c7db079..220a1f9574 100644 --- a/resources/views/livewire/images/images/index.blade.php +++ b/resources/views/livewire/images/images/index.blade.php @@ -25,10 +25,15 @@ wire:confirm="Are you sure you want to prune unused images?"> Prune Unused - - Delete Selected ({{ count($selectedImages) }}) - +
  • @endif @@ -39,10 +44,10 @@ - --}} @endif @@ -60,9 +65,9 @@ - - Repository + Repository --}} Tag @@ -72,6 +77,9 @@ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking- Size + + Status Actions @@ -82,23 +90,65 @@ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking- + value="{{ $image['Id'] }}"> - {{ $image['Repository'] }} - {{ $image['Tag'] }} + + @if (is_array($image['RepoTags'])) + @foreach ($image['RepoTags'] as $tag) + + {{ $tag }} + + @endforeach + @else + + {{ $image['RepoTags'] }} + + @endif + + {{-- {{ $image['Tag'] }} --}} - {{ substr($image['ID'], 7, 12) }} - {{ $image['Size'] }} + {{ $image['Id'] }} + + {{ $image['FormattedSize'] }} + +
    + + $image['Status'] === 'in use', + 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300' => + $image['Status'] === 'unused', + ])> + {{ $image['Status'] }} + + @if ($image['Dangling']) + + Dangling + + @endif +
    +
    - + Details - - Delete - +
    @@ -120,95 +170,136 @@ class="" isError> + {{-- Image Details Modal --}} @if ($imageDetails)
    -
    +

    Image Details

    -
    - - Prune Unused - - Delete Image - - +
    + +
    - -
    -
    -
    -

    ID:

    -

    {{ $imageDetails[0]['Id'] ?? 'N/A' }}

    -
    -
    -

    Created:

    -

    {{ $imageDetails[0]['FormattedCreated'] ?? 'N/A' }}

    -
    -
    -

    Size:

    -

    {{ $imageDetails[0]['FormattedSize'] ?? 'N/A' }}

    -
    - {{--
    -

    Container Count:

    -

    {{ $imageDetails[0]['ContainerCount'] ?? 'N/A' }}

    -
    --}} -
    - - @if (isset($imageDetails[0]['Config'])) -
    -

    Configuration

    -
    - @if (isset($imageDetails[0]['Config']['Env']) && !empty($imageDetails[0]['Config']['Env'])) -
    -
    Environment Variables
    -
    - @foreach ($imageDetails[0]['Config']['Env'] as $env) -
    {{ $env }}
    - @endforeach -
    +
    +
    + {{-- Basic Information --}} +
    +

    Basic Information

    +
    +
    +
    ID
    +
    {{ $imageDetails['Id'] }}
    - @endif + {{-- ... rest of basic information ... --}} +
    +
    - @if (isset($imageDetails[0]['Config']['ExposedPorts']) && !empty($imageDetails[0]['Config']['ExposedPorts'])) -
    -
    Exposed Ports
    -
    - @foreach (array_keys($imageDetails[0]['Config']['ExposedPorts']) as $port) -
    {{ $port }}
    - @endforeach + {{-- Tags and Digests --}} +
    +

    Tags and Digests

    +
    +
    +
    Repository Tags
    +
    + @if (is_array($imageDetails['RepoTags'] ?? null)) + @foreach ($imageDetails['RepoTags'] as $tag) + + {{ $tag }} + + @endforeach + @elseif($imageDetails['RepoTags'] ?? null) + + {{ $imageDetails['RepoTags'] }} + + @else + No tags + @endif
    - @endif - - @if (isset($imageDetails[0]['Config']['Volumes']) && !empty($imageDetails[0]['Config']['Volumes'])) -
    -
    Volumes
    -
    - @foreach (array_keys($imageDetails[0]['Config']['Volumes']) as $volume) -
    {{ $volume }}
    +
    +
    Repository Digests +
    +
    + @foreach ($imageDetails['RepoDigests'] ?? [] as $digest) +
    {{ $digest }}
    @endforeach
    - @endif - - @if (isset($imageDetails[0]['Config']['Cmd']) && !empty($imageDetails[0]['Config']['Cmd'])) -
    -
    Command
    -
    - {{ implode(' ', $imageDetails[0]['Config']['Cmd']) }} -
    -
    - @endif +
    - @endif +
    + {{-- Configuration --}} + @if (isset($imageDetails['Config'])) +
    +

    Configuration

    +
    + {{-- Exposed Ports --}} + @if (isset($imageDetails['Config']['ExposedPorts'])) +
    +
    Exposed Ports +
    +
    + @foreach (array_keys($imageDetails['Config']['ExposedPorts']) as $port) + + {{ $port }} + + @endforeach +
    +
    + @endif + {{-- Command --}} + @if (isset($imageDetails['Config']['Cmd'])) +
    +
    Command
    +
    + {{ implode(' ', $imageDetails['Config']['Cmd']) }} +
    +
    + @endif + + {{-- Labels --}} + @if (isset($imageDetails['Config']['Labels'])) +
    +
    Labels
    +
    + @foreach ($imageDetails['Config']['Labels'] as $key => $value) +
    + {{ $key }}: + {{ $value }} +
    + @endforeach +
    +
    + @endif +
    +
    + @endif +
    +
    From 3aca8068af3b5fd411db00dcb1ecd0aa55dc488f Mon Sep 17 00:00:00 2001 From: David Buday Date: Thu, 2 Jan 2025 12:42:45 +0100 Subject: [PATCH 37/39] chore: progress --- app/Jobs/ApplicationDeploymentJob.php | 9 ++-- app/Livewire/Images/Images/Index.php | 29 +++++++++-- .../project/application/general.blade.php | 48 +++++++++---------- 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 56f2bf390d..386011abfa 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -335,6 +335,11 @@ public function handle(): void private function decide_what_to_do() { + // login if use custom registry + if ($this->application->docker_use_custom_registry) { + $this->handleRegistryAuth(); + } + if ($this->restart_only) { $this->just_restart(); @@ -408,10 +413,6 @@ private function deploy_dockerimage_buildpack() } $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); - // login if use custom registry - if ($this->application->docker_use_custom_registry) { - $this->handleRegistryAuth(); - } $this->generate_image_names(); $this->prepare_builder_image(); diff --git a/app/Livewire/Images/Images/Index.php b/app/Livewire/Images/Images/Index.php index 154f6afc66..5306e339ed 100644 --- a/app/Livewire/Images/Images/Index.php +++ b/app/Livewire/Images/Images/Index.php @@ -3,6 +3,7 @@ namespace App\Livewire\Images\Images; use App\Actions\Docker\DeleteAllDanglingServerDockerImages; +use App\Actions\Docker\DeleteServerDockerImages; use App\Actions\Docker\GetServerDockerImageDetails; use App\Actions\Docker\ListServerDockerImages; use App\Models\Server; @@ -101,10 +102,10 @@ public function getImageDetails($imageId) } } - public function confirmDelete($imageIds = null) + public function confirmDelete($imageId = null) { - if ($imageIds) { - $this->imagesToDelete = is_array($imageIds) ? $imageIds : [$imageIds]; + if ($imageId) { + $this->imagesToDelete = [$imageId]; } else { $this->imagesToDelete = $this->selectedImages; } @@ -124,6 +125,16 @@ public function deleteImages() return; } + if (empty($this->imagesToDelete)) { + $this->addError('delete', 'No images selected for deletion'); + return; + } + + if ($this->confirmationText !== 'delete') { + $this->addError('confirmation', 'Please type "delete" to confirm'); + return; + } + DeleteServerDockerImages::run($server, $this->imagesToDelete); // Reset states @@ -132,8 +143,10 @@ public function deleteImages() $this->selectedImages = []; $this->imageDetails = null; $this->confirmationText = ''; + $this->selectAll = false; $this->loadServerImages(); + $this->dispatch('success', 'Images deleted successfully.'); } catch (\Exception $e) { $this->addError('delete', "Error deleting images: " . $e->getMessage()); } @@ -159,8 +172,14 @@ public function getFilteredImagesProperty() return $this->serverImages ->when($this->searchQuery, function ($collection) { return $collection->filter(function ($image) { - return str_contains(strtolower($image['RepoTags'][0] ?? ''), strtolower($this->searchQuery)) || - str_contains(strtolower($image['Id'] ?? ''), strtolower($this->searchQuery)); + // Check if RepoTags is an array and has elements + $tags = is_array($image['RepoTags']) ? $image['RepoTags'] : [$image['RepoTags']]; + $tags = array_filter($tags); // Remove empty values + + // Search in all tags and ID + return collect($tags)->some(function ($tag) { + return str_contains(strtolower($tag), strtolower($this->searchQuery)); + }) || str_contains(strtolower($image['Id'] ?? ''), strtolower($this->searchQuery)); }); }) ->when($this->showOnlyDangling, function ($collection) { diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 7181adf67a..95d530dfb8 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -143,33 +143,33 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @endif
    - @if ($application->build_pack === 'dockerimage') -
    -
    - -
    + {{-- @if ($application->build_pack === 'dockerimage') --}} +
    +
    + +
    - @if ($application->docker_use_custom_registry) -
    - - @foreach ($registries as $registry) - - @endforeach - -
    + @if ($application->docker_use_custom_registry) +
    + + @foreach ($registries as $registry) + + @endforeach + +
    - @endif -
    - @endif + @endif +
    + {{-- @endif --}}
    @endif
    From 2d43f1e28553ebcacd04558305e0b1c530020ae8 Mon Sep 17 00:00:00 2001 From: David Buday Date: Thu, 2 Jan 2025 16:12:46 +0100 Subject: [PATCH 38/39] chore: progress --- app/Livewire/Images/Images/Index.php | 24 ++++----- .../livewire/images/images/index.blade.php | 54 +++++++++---------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/app/Livewire/Images/Images/Index.php b/app/Livewire/Images/Images/Index.php index 5306e339ed..80b2e8e58c 100644 --- a/app/Livewire/Images/Images/Index.php +++ b/app/Livewire/Images/Images/Index.php @@ -104,6 +104,15 @@ public function getImageDetails($imageId) public function confirmDelete($imageId = null) { + dd($imageId); + + + $this->showDeleteConfirmation = true; + } + + public function deleteImages($imageId = null) + { + if ($imageId) { $this->imagesToDelete = [$imageId]; } else { @@ -111,14 +120,9 @@ public function confirmDelete($imageId = null) } if (empty($this->imagesToDelete)) { + dd('empty'); return; } - - $this->showDeleteConfirmation = true; - } - - public function deleteImages() - { try { $server = $this->servers->firstWhere('uuid', $this->selected_uuid); if (!$server) { @@ -130,11 +134,6 @@ public function deleteImages() return; } - if ($this->confirmationText !== 'delete') { - $this->addError('confirmation', 'Please type "delete" to confirm'); - return; - } - DeleteServerDockerImages::run($server, $this->imagesToDelete); // Reset states @@ -148,6 +147,7 @@ public function deleteImages() $this->loadServerImages(); $this->dispatch('success', 'Images deleted successfully.'); } catch (\Exception $e) { + dd($e); $this->addError('delete', "Error deleting images: " . $e->getMessage()); } } @@ -227,7 +227,7 @@ protected function formatBytes($bytes, $precision = 2) public function render() { return view('livewire.images.images.index', [ - 'filteredImages' => $this->getFilteredImagesProperty() + 'filteredImages' => $this->getFilteredImagesProperty(), ]); } } diff --git a/resources/views/livewire/images/images/index.blade.php b/resources/views/livewire/images/images/index.blade.php index 220a1f9574..d5c40b58ac 100644 --- a/resources/views/livewire/images/images/index.blade.php +++ b/resources/views/livewire/images/images/index.blade.php @@ -21,19 +21,19 @@ @if ($selected_uuid !== 'default')
    - Prune Unused - - + --}} + @if (!empty($selectedImages)) + + @endif
    @endif
    @@ -136,19 +136,14 @@ class="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800 dark:bg-yell Details - + step2ButtonText="Permanently Delete" />
    @@ -179,15 +174,13 @@ class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 sm:p-6 max-w-4xl w-full max-h

    Image Details

    + step3ButtonText="Permanently Delete" />
    From d67d9fc058758ec0e904c1926fb6b7595873d979 Mon Sep 17 00:00:00 2001 From: David Buday Date: Thu, 16 Jan 2025 20:03:06 +0100 Subject: [PATCH 39/39] chore: updating tags, progress on deletion and list, todo (application used by) --- .../Docker/DeleteServerDockerImages.php | 1 + app/Actions/Docker/ListServerDockerImages.php | 22 +- .../Docker/UpdateServerDockerImageTag.php | 8 +- app/Livewire/Images/Images/Index.php | 233 +++++++++++++----- .../livewire/images/images/index.blade.php | 171 +++++++++---- 5 files changed, 307 insertions(+), 128 deletions(-) diff --git a/app/Actions/Docker/DeleteServerDockerImages.php b/app/Actions/Docker/DeleteServerDockerImages.php index e41ca3377d..e3e256bdf9 100644 --- a/app/Actions/Docker/DeleteServerDockerImages.php +++ b/app/Actions/Docker/DeleteServerDockerImages.php @@ -10,6 +10,7 @@ class DeleteServerDockerImages public static function run($server, $ids) { + $idsForCommand = implode(' ', $ids); return instant_remote_process(["docker rmi $idsForCommand -f"], $server); } diff --git a/app/Actions/Docker/ListServerDockerImages.php b/app/Actions/Docker/ListServerDockerImages.php index d2d0be6a70..899f58884c 100644 --- a/app/Actions/Docker/ListServerDockerImages.php +++ b/app/Actions/Docker/ListServerDockerImages.php @@ -2,6 +2,7 @@ namespace App\Actions\Docker; +use App\Models\Application; use Illuminate\Database\Eloquent\Collection; use Lorisleiva\Actions\Concerns\AsAction; @@ -23,35 +24,36 @@ public static function run($server) $command = "curl --unix-socket /var/run/docker.sock http://localhost/images/json"; $imagesJson = json_decode(instant_remote_process([$command], $server), true); - $images = []; foreach ($imagesJson as $image) { $isRunning = key_exists($image['Id'], $runningImages); - if ($image['RepoTags'] == []){ + if ($image['RepoTags'] == []) { $imageCopy = $image; - $imageCopy["Status"] = 'unused'; $imageCopy["Dangling"] = true; - array_push($images, $imageCopy); - - }else{ - + } else { foreach ($image['RepoTags'] as $tag) { $imageCopy = $image; - $imageCopy["RepoTags"] = $tag; - if ($isRunning){ + if ($isRunning) { $imageCopy["Status"] = 'in use'; - }else{ + } else { $imageCopy["Status"] = 'unused'; } $imageCopy["Dangling"] = false; + // Add applications using this image + [$name, $tag] = array_pad(explode(':', $tag, 2), 2, 'latest'); + $imageCopy['IsUsedBy'] = Application::query() + ->where('docker_registry_image_name', $name) + ->where('docker_registry_image_tag', $tag) + ->get(); + array_push($images, $imageCopy); } } diff --git a/app/Actions/Docker/UpdateServerDockerImageTag.php b/app/Actions/Docker/UpdateServerDockerImageTag.php index 43ef62e3f1..b6b5b57cb6 100644 --- a/app/Actions/Docker/UpdateServerDockerImageTag.php +++ b/app/Actions/Docker/UpdateServerDockerImageTag.php @@ -8,10 +8,12 @@ class UpdateServerDockerImageTag { use AsAction; - public static function run($server, $imageId, $tagName) + public static function run($server, $imageId, $imageRepo, $tagName) { - $imageDetails = GetServerDockerImageDetails::run($server, $imageId); - return instant_remote_process(["docker tag {$imageId} {$imageDetails['Repository']}:{$tagName}"], $server); + //$imageDetails = GetServerDockerImageDetails::run($server, $imageId); + //$imageRepo = explode(':', $imageDetails['RepoTags'][0])[0]; + // dd($imageId, $imageRepo, $tagName); + return instant_remote_process(["docker tag {$imageId} {$imageRepo}:{$tagName}"], $server); } } diff --git a/app/Livewire/Images/Images/Index.php b/app/Livewire/Images/Images/Index.php index 80b2e8e58c..c40dded3ff 100644 --- a/app/Livewire/Images/Images/Index.php +++ b/app/Livewire/Images/Images/Index.php @@ -5,6 +5,7 @@ use App\Actions\Docker\DeleteAllDanglingServerDockerImages; use App\Actions\Docker\DeleteServerDockerImages; use App\Actions\Docker\GetServerDockerImageDetails; +use App\Actions\Docker\UpdateServerDockerImageTag; use App\Actions\Docker\ListServerDockerImages; use App\Models\Server; use Illuminate\Database\Eloquent\Collection; @@ -19,6 +20,7 @@ class Index extends Component public Collection $servers; public bool $isLoadingImages = false; public array $selectedImages = []; + public ?array $imageDetails = null; public string $searchQuery = ''; public bool $showOnlyDangling = false; @@ -27,28 +29,70 @@ class Index extends Component public array $imagesToDelete = []; public string $confirmationText = ''; + public $editingImageId = null; + public $newTag = ''; + public $newRepo = ''; public function mount() { $this->servers = Server::isReachable()->get(); $this->serverImages = collect([]); } + /** + * Whenever user picks a new server, load images & reset selection + */ public function updatedSelectedUuid() { $this->loadServerImages(); + $this->resetSelection(); + } + + /** + * "Select all" checkbox toggled + */ + public function updatedSelectAll($value) + { + // If selectAll is true, grab all filtered images' IDs + // If false, clear out selectedImages + $this->selectedImages = $value + ? $this->filteredImages->pluck('Id')->toArray() + : []; + } + + /** + * Clears out selections (helper function) + */ + protected function resetSelection() + { $this->selectedImages = []; + $this->selectAll = false; + } + /** + * Whenever we search or change "dangling only", reset selection + */ + public function updatedSearchQuery() + { + $this->resetSelection(); } + public function updatedShowOnlyDangling() + { + $this->resetSelection(); + } + + /** + * Load images for the chosen server + */ public function loadServerImages() { $this->isLoadingImages = true; $this->imageDetails = null; - try { - if ($this->selected_uuid === 'default') { - return; - } + if ($this->selected_uuid === 'default') { + return; + } + try { $server = $this->servers->firstWhere('uuid', $this->selected_uuid); if (!$server) { return; @@ -62,12 +106,15 @@ public function loadServerImages() return $image; }); } catch (\Exception $e) { - $this->addError('images', "Error loading docker images: " . $e->getMessage()); + $this->dispatch('error', "Error loading docker images: " . $e->getMessage()); } finally { $this->isLoadingImages = false; } } + /** + * Fetch details for a specific image + */ public function getImageDetails($imageId) { try { @@ -75,21 +122,18 @@ public function getImageDetails($imageId) if (!$server) { return; } + $details = GetServerDockerImageDetails::run($server, $imageId); - // Add formatted size (total size) - if (isset($details['Size'])) { - $details['FormattedSize'] = $this->formatBytes($details['Size']); - } else { - $details['FormattedSize'] = 'N/A'; - } + // Add some nicely formatted fields + $details['FormattedSize'] = isset($details['Size']) + ? $this->formatBytes($details['Size']) + : 'N/A'; - // Add formatted virtual size if (isset($details['VirtualSize'])) { $details['FormattedVirtualSize'] = $this->formatBytes($details['VirtualSize']); } - // Add formatted creation date if (isset($details['Created'])) { $details['FormattedCreated'] = \Carbon\Carbon::parse($details['Created'])->diffForHumans(); } else { @@ -98,60 +142,82 @@ public function getImageDetails($imageId) $this->imageDetails = $details; } catch (\Exception $e) { - $this->addError('details', "Error loading image details: " . $e->getMessage()); + $this->dispatch('error', "Error loading image details: " . $e->getMessage()); } } public function confirmDelete($imageId = null) { - dd($imageId); - - + // You can open a modal here or similar + // dd($imageId); $this->showDeleteConfirmation = true; } public function deleteImages($imageId = null) { - - if ($imageId) { - $this->imagesToDelete = [$imageId]; - } else { - $this->imagesToDelete = $this->selectedImages; - } + // If a single ID was passed, we delete just that + // Otherwise, we delete the entire selection + $this->imagesToDelete = $imageId + ? [$imageId] + : $this->selectedImages; if (empty($this->imagesToDelete)) { - dd('empty'); + $this->dispatch('error', 'No images selected for deletion'); return; } + try { $server = $this->servers->firstWhere('uuid', $this->selected_uuid); if (!$server) { return; } - if (empty($this->imagesToDelete)) { - $this->addError('delete', 'No images selected for deletion'); - return; - } - DeleteServerDockerImages::run($server, $this->imagesToDelete); // Reset states $this->showDeleteConfirmation = false; $this->imagesToDelete = []; - $this->selectedImages = []; + $this->resetSelection(); $this->imageDetails = null; $this->confirmationText = ''; - $this->selectAll = false; + // Reload images $this->loadServerImages(); + $this->dispatch('success', 'Images deleted successfully.'); } catch (\Exception $e) { - dd($e); - $this->addError('delete', "Error deleting images: " . $e->getMessage()); + $this->dispatch('error', "Error deleting images: " . $e->getMessage()); } } + /** + * Delete all dangling images + */ + public function deleteUnusedImages() + { + try { + $server = $this->servers->firstWhere('uuid', $this->selected_uuid); + if (!$server) { + return; + } + + $unusedIds = $this->filteredImages + ->filter(fn($image) => ($image['Status'] ?? '') === 'unused') + ->pluck('Id') + ->toArray(); + + DeleteServerDockerImages::run($server, $unusedIds); + + $this->loadServerImages(); + $this->dispatch('success', 'Unused images deleted successfully.'); + } catch (\Exception $e) { + $this->dispatch('error', "Error deleting unused images: " . $e->getMessage()); + } + } + + /** + * Prune (delete) *dangling* images + */ public function pruneUnused() { try { @@ -163,23 +229,32 @@ public function pruneUnused() DeleteAllDanglingServerDockerImages::run($server); $this->loadServerImages(); } catch (\Exception $e) { - $this->addError('prune', "Error pruning images: " . $e->getMessage()); + $this->dispatch('error', "Error pruning images: " . $e->getMessage()); } } + /** + * Dynamically filter images based on search/dangling + */ public function getFilteredImagesProperty() { return $this->serverImages ->when($this->searchQuery, function ($collection) { return $collection->filter(function ($image) { - // Check if RepoTags is an array and has elements - $tags = is_array($image['RepoTags']) ? $image['RepoTags'] : [$image['RepoTags']]; - $tags = array_filter($tags); // Remove empty values + $tags = is_array($image['RepoTags']) + ? $image['RepoTags'] + : [$image['RepoTags']]; - // Search in all tags and ID - return collect($tags)->some(function ($tag) { + $tags = array_filter($tags); // remove null or empty + + // search by any tag or the ID + $matchTag = collect($tags)->some(function ($tag) { return str_contains(strtolower($tag), strtolower($this->searchQuery)); - }) || str_contains(strtolower($image['Id'] ?? ''), strtolower($this->searchQuery)); + }); + + $matchId = str_contains(strtolower($image['Id'] ?? ''), strtolower($this->searchQuery)); + + return $matchTag || $matchId; }); }) ->when($this->showOnlyDangling, function ($collection) { @@ -190,36 +265,28 @@ public function getFilteredImagesProperty() ->values(); } - public function updatedSelectAll($value) - { - if ($value) { - $this->selectedImages = $this->filteredImages->pluck('Id')->toArray(); - } else { - $this->selectedImages = []; - } - } - - public function updatedSearchQuery() + /** + * Return how many images are "unused" + */ + public function getUnusedImagesCountProperty() { - $this->selectAll = false; - $this->selectedImages = []; - } - - public function updatedShowOnlyDangling() - { - $this->selectAll = false; - $this->selectedImages = []; + return $this->serverImages + ->filter(fn($image) => ($image['Status'] ?? '') === 'unused') + ->count(); } + /** + * Convert bytes to a human-readable format + */ protected function formatBytes($bytes, $precision = 2) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count($units) - 1); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); - $bytes /= pow(1024, $pow); + $bytes /= (1 << (10 * $pow)); // same as pow(1024, $pow) return round($bytes, $precision) . ' ' . $units[$pow]; } @@ -227,7 +294,49 @@ protected function formatBytes($bytes, $precision = 2) public function render() { return view('livewire.images.images.index', [ - 'filteredImages' => $this->getFilteredImagesProperty(), + 'filteredImages' => $this->filteredImages, ]); } + + public function startEditingTag($imageId) + { + $this->editingImageId = $imageId; + $image = $this->serverImages->firstWhere('Id', $imageId); + if ($image && isset($image['RepoTags'])) { + $tag = is_array($image['RepoTags']) ? $image['RepoTags'][0] : $image['RepoTags']; + $this->newTag = explode(':', $tag)[1] ?? ''; + $this->newRepo = explode(':', $tag)[0] ?? ''; + } + } + + public function updateTag() + { + try { + $server = $this->servers->firstWhere('uuid', $this->selected_uuid); + if (!$server) { + return; + } + + UpdateServerDockerImageTag::run($server, $this->editingImageId, $this->newRepo, $this->newTag); + + // Reset states + $this->editingImageId = null; + $this->newTag = ''; + $this->newRepo = ''; + + + // Reload images + $this->loadServerImages(); + $this->dispatch('success', 'Image tag updated successfully.'); + } catch (\Exception $e) { + $this->dispatch('error', "Error updating tag: " . $e->getMessage()); + } + } + + public function cancelEditTag() + { + $this->editingImageId = null; + $this->newTag = ''; + $this->newRepo = ''; + } } diff --git a/resources/views/livewire/images/images/index.blade.php b/resources/views/livewire/images/images/index.blade.php index d5c40b58ac..340b23f642 100644 --- a/resources/views/livewire/images/images/index.blade.php +++ b/resources/views/livewire/images/images/index.blade.php @@ -2,10 +2,11 @@
    +

    Docker Images

    -
    + @foreach ($servers as $server) @@ -21,30 +22,40 @@ @if ($selected_uuid !== 'default')
    - {{-- - Prune Unused - --}} - @if (!empty($selectedImages)) - + ]" confirmationText="delete" + confirmationLabel="Please type 'delete' to confirm" shortConfirmationLabel="Confirmation" + step3ButtonText="Permanently Delete" /> @endif + +
    @endif
    + + @if ($selected_uuid !== 'default')
    - {{--