From 7efece06c435959f59ffd775b0a975e9ae0d5105 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:18:34 +0200 Subject: [PATCH] feat(ui): add container name prefix to application container settings --- app/Livewire/Project/Application/Advanced.php | 28 ++++++++++++ .../project/application/advanced.blade.php | 19 ++++++-- .../ApplicationConfigAuthorizationTest.php | 12 +++++ .../AdvancedContainerNamingTest.php | 44 +++++++++++++++++++ 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index a9e1c0be28..b3564df0bb 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Application; use App\Models\Application; +use App\Models\ApplicationSetting; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; @@ -69,6 +70,9 @@ class Advanced extends Component #[Validate(['string', 'nullable'])] public ?string $customInternalName = null; + #[Validate(['string', 'nullable', 'max:47'])] + public ?string $customContainerNamePrefix = null; + #[Validate(['boolean'])] public bool $isGzipEnabled = true; @@ -111,6 +115,7 @@ class Advanced extends Component $this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled; $this->application->settings->is_consistent_container_name_enabled = $this->isConsistentContainerNameEnabled; $this->application->settings->custom_internal_name = $this->customInternalName; + $this->application->settings->custom_container_name_prefix = $this->customContainerNamePrefix; $this->application->settings->is_gzip_enabled = $this->isGzipEnabled; $this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled; $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled; @@ -137,6 +142,7 @@ class Advanced extends Component $this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled; $this->isConsistentContainerNameEnabled = $this->application->settings->is_consistent_container_name_enabled; $this->customInternalName = $this->application->settings->custom_internal_name; + $this->customContainerNamePrefix = $this->application->settings->custom_container_name_prefix; $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled; $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network; $this->disableBuildCache = $this->application->settings->disable_build_cache; @@ -258,6 +264,28 @@ class Advanced extends Component } } + public function saveCustomNamePrefix() + { + try { + $this->authorize('update', $this->application); + + $this->customContainerNamePrefix = str($this->customContainerNamePrefix)->slug()->value() ?: null; + + if ($this->customContainerNamePrefix && ApplicationSetting::isContainerNamePrefixInUse($this->customContainerNamePrefix, $this->application->destination->server, $this->application->id)) { + $this->customContainerNamePrefix = $this->application->settings->custom_container_name_prefix; + $this->dispatch('error', 'This container name prefix is already in use by another application on this Coolify instance.'); + + return; + } + + $this->syncData(true); + $this->dispatch('success', 'Container name prefix saved.'); + $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function saveStopGracePeriod() { try { diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index c6113808c5..3a5549e0ea 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -45,10 +45,21 @@ ['value' => true, 'label' => 'Consistent name (no rolling updates)'], ]" :disabled="! $canUpdate" /> @if ($isConsistentContainerNameEnabled === true) - +
+ + + + @else +
+ + + @endif diff --git a/tests/Feature/Authorization/ApplicationConfigAuthorizationTest.php b/tests/Feature/Authorization/ApplicationConfigAuthorizationTest.php index eb331a775f..93c9658a7b 100644 --- a/tests/Feature/Authorization/ApplicationConfigAuthorizationTest.php +++ b/tests/Feature/Authorization/ApplicationConfigAuthorizationTest.php @@ -247,6 +247,18 @@ test('member cannot submit application advanced settings', function () { ->assertDispatched('error'); }); +test('member cannot save the application container name prefix', function () { + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + Livewire::test(ApplicationAdvanced::class, ['application' => $this->application]) + ->set('customContainerNamePrefix', 'member-prefix') + ->call('saveCustomNamePrefix') + ->assertDispatched('error'); + + expect($this->application->settings->fresh()->custom_container_name_prefix)->toBeNull(); +}); + test('the private application advanced syncData helper is not remotely callable', function () { $this->actingAs($this->member); session(['currentTeam' => $this->team]); diff --git a/tests/Feature/Livewire/Project/Application/AdvancedContainerNamingTest.php b/tests/Feature/Livewire/Project/Application/AdvancedContainerNamingTest.php index 079bd6daa4..ac381762cb 100644 --- a/tests/Feature/Livewire/Project/Application/AdvancedContainerNamingTest.php +++ b/tests/Feature/Livewire/Project/Application/AdvancedContainerNamingTest.php @@ -104,3 +104,47 @@ it('only shows the custom container name for consistent naming', function () { ->set('isConsistentContainerNameEnabled', true) ->assertSee('Custom container name'); }); + +it('saves a slugged container name prefix in generated naming mode', function () { + $otherTeamApplication = createApplicationForContainerNamingTest(); + $otherTeamApplication->settings->update(['custom_container_name_prefix' => 'my-api']); + + $application = createApplicationForContainerNamingTest(); + $application->settings->update(['custom_internal_name' => 'legacy-name']); + $application = $application->fresh(['environment.project', 'settings', 'destination']); + + Livewire::test(Advanced::class, ['application' => $application]) + ->assertSee('Container name prefix') + ->set('customContainerNamePrefix', 'My API') + ->call('saveCustomNamePrefix') + ->assertDispatched('success') + ->assertSet('customContainerNamePrefix', 'my-api'); + + $settings = $application->settings()->first(); + expect($settings->custom_container_name_prefix)->toBe('my-api') + ->and($settings->custom_internal_name)->toBe('legacy-name'); +}); + +it('rejects a container name prefix already used on the server', function () { + $application = createApplicationForContainerNamingTest(); + $sibling = fn () => Application::factory()->create([ + 'environment_id' => $application->environment_id, + 'destination_id' => $application->destination_id, + 'destination_type' => $application->destination_type, + ]); + $prefixedApplication = $sibling(); + $prefixedApplication->settings->update(['custom_container_name_prefix' => 'shared-prefix']); + $sibling()->settings->update(['custom_internal_name' => 'api']); + + $application = $application->fresh(['environment.project', 'settings', 'destination']); + $component = Livewire::test(Advanced::class, ['application' => $application]); + + foreach (['shared-prefix', 'api', $prefixedApplication->uuid] as $takenPrefix) { + $component->set('customContainerNamePrefix', $takenPrefix) + ->call('saveCustomNamePrefix') + ->assertDispatched('error') + ->assertSet('customContainerNamePrefix', null); + } + + expect($application->settings()->first()->custom_container_name_prefix)->toBeNull(); +});