From 50268139be512ba3ae38cc4addccd692134df596 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:23:55 +0200 Subject: [PATCH] fix(api): reject custom_internal_name without consistent container naming Container names use custom_internal_name only when consistent container naming is on. The API accepted the name and then ignored it. - Return 422 with a message that says to set is_consistent_container_name_enabled to true - Accept the name when the request or the application turns the setting on - Document the requirement in the OpenAPI spec --- .../Api/ApplicationsController.php | 38 +++++- openapi.json | 12 +- openapi.yaml | 12 +- ...tomInternalNameConsistentNamingApiTest.php | 126 ++++++++++++++++++ .../Api/ApplicationSettingsApiTest.php | 1 + 5 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 tests/Feature/Api/ApplicationCustomInternalNameConsistentNamingApiTest.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index a611a60f4f..0886ca812f 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -183,6 +183,26 @@ class ApplicationsController extends Controller ], 422); } + /** + * Container names honor custom_internal_name only with consistent container naming, so reject a name that would be ignored. + */ + private function customInternalNameValidationResponse(array $settings, ?Application $application = null): ?JsonResponse + { + if (! filled($settings['custom_internal_name'] ?? null)) { + return null; + } + $consistentNaming = $settings['is_consistent_container_name_enabled'] + ?? (bool) $application?->settings?->is_consistent_container_name_enabled; + if ($consistentNaming) { + return null; + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['custom_internal_name' => ['Set is_consistent_container_name_enabled to true to use custom_internal_name. Coolify ignores the custom internal name while consistent container naming is turned off.']], + ], 422); + } + private function applyApplicationSettings(Application $application, array $settings): void { if ($settings === []) { @@ -419,7 +439,7 @@ class ApplicationsController extends Controller 'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'], 'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'], 'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'], - 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'], 'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'], 'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'], 'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'], @@ -614,7 +634,7 @@ class ApplicationsController extends Controller 'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'], 'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'], 'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'], - 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'], 'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'], 'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'], 'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'], @@ -809,7 +829,7 @@ class ApplicationsController extends Controller 'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'], 'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'], 'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'], - 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'], 'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'], 'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'], 'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'], @@ -975,7 +995,7 @@ class ApplicationsController extends Controller 'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'], 'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'], 'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'], - 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'], 'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'], 'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'], 'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'], @@ -1137,7 +1157,7 @@ class ApplicationsController extends Controller 'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'], 'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'], 'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'], - 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'], 'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'], 'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'], 'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'], @@ -1391,6 +1411,9 @@ class ApplicationsController extends Controller if ($prefixValidation = $this->containerNamePrefixValidationResponse($applicationSettings, $destination->server)) { return $prefixValidation; } + if ($internalNameValidation = $this->customInternalNameValidationResponse($applicationSettings)) { + return $internalNameValidation; + } if ($type === 'public') { $validationRules = [ 'git_repository' => ['string', 'required', new ValidGitRepositoryUrl], @@ -3128,7 +3151,7 @@ class ApplicationsController extends Controller 'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'], 'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'], 'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'], - 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'], 'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'], 'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'], 'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'], @@ -3312,6 +3335,9 @@ class ApplicationsController extends Controller if ($prefixValidation = $this->containerNamePrefixValidationResponse($applicationSettings, $application->destination->server, $application)) { return $prefixValidation; } + if ($internalNameValidation = $this->customInternalNameValidationResponse($applicationSettings, $application)) { + return $internalNameValidation; + } $requestedBuildPack = $request->input('build_pack', $application->build_pack); if (($applicationSettings['is_raw_compose_deployment_enabled'] ?? false) && $requestedBuildPack !== 'dockercompose') { return response()->json([ diff --git a/openapi.json b/openapi.json index bfc925779c..1f0537a281 100644 --- a/openapi.json +++ b/openapi.json @@ -949,7 +949,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name." + "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." }, "custom_container_name_prefix": { "type": "string", @@ -1540,7 +1540,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name." + "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." }, "custom_container_name_prefix": { "type": "string", @@ -2131,7 +2131,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name." + "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." }, "custom_container_name_prefix": { "type": "string", @@ -2617,7 +2617,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name." + "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." }, "custom_container_name_prefix": { "type": "string", @@ -3083,7 +3083,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name." + "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." }, "custom_container_name_prefix": { "type": "string", @@ -3794,7 +3794,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name." + "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." }, "custom_container_name_prefix": { "type": "string", diff --git a/openapi.yaml b/openapi.yaml index 28ed4f49ed..69f2a33bc3 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -622,7 +622,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name.' + description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' custom_container_name_prefix: type: string nullable: true @@ -1008,7 +1008,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name.' + description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' custom_container_name_prefix: type: string nullable: true @@ -1394,7 +1394,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name.' + description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' custom_container_name_prefix: type: string nullable: true @@ -1721,7 +1721,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name.' + description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' custom_container_name_prefix: type: string nullable: true @@ -2034,7 +2034,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name.' + description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' custom_container_name_prefix: type: string nullable: true @@ -2501,7 +2501,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name.' + description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' custom_container_name_prefix: type: string nullable: true diff --git a/tests/Feature/Api/ApplicationCustomInternalNameConsistentNamingApiTest.php b/tests/Feature/Api/ApplicationCustomInternalNameConsistentNamingApiTest.php new file mode 100644 index 0000000000..f745425781 --- /dev/null +++ b/tests/Feature/Api/ApplicationCustomInternalNameConsistentNamingApiTest.php @@ -0,0 +1,126 @@ + 'array']); + InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0])); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + session(['currentTeam' => $this->team]); + + $this->bearerToken = $this->user->createToken('custom-internal-name-test', ['*'])->plainTextToken; + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = StandaloneDocker::where('server_id', $this->server->id)->first(); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); + $this->application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); +}); + +function customInternalNameCreatePayload(array $overrides): array +{ + return array_merge([ + 'project_uuid' => test()->project->uuid, + 'environment_uuid' => test()->environment->uuid, + 'server_uuid' => test()->server->uuid, + 'git_repository' => 'https://gitlab.com/coolify/custom-internal-name', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'autogenerate_domain' => false, + ], $overrides); +} + +const CUSTOM_INTERNAL_NAME_NEEDS_CONSISTENT_NAMING = 'Set is_consistent_container_name_enabled to true to use custom_internal_name. Coolify ignores the custom internal name while consistent container naming is turned off.'; + +test('updating only custom_internal_name is rejected while consistent container naming is off', function () { + $this->withToken($this->bearerToken) + ->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_internal_name' => 'my-app']) + ->assertUnprocessable() + ->assertJsonPath('errors.custom_internal_name.0', CUSTOM_INTERNAL_NAME_NEEDS_CONSISTENT_NAMING); + + expect($this->application->fresh()->settings->custom_internal_name)->toBeNull(); +}); + +test('updating custom_internal_name while turning consistent container naming off is rejected', function () { + $this->application->settings->update(['is_consistent_container_name_enabled' => true]); + + $this->withToken($this->bearerToken) + ->patchJson("/api/v1/applications/{$this->application->uuid}", [ + 'custom_internal_name' => 'my-app', + 'is_consistent_container_name_enabled' => false, + ]) + ->assertUnprocessable() + ->assertJsonPath('errors.custom_internal_name.0', CUSTOM_INTERNAL_NAME_NEEDS_CONSISTENT_NAMING); +}); + +test('updating custom_internal_name together with consistent container naming succeeds', function () { + $this->withToken($this->bearerToken) + ->patchJson("/api/v1/applications/{$this->application->uuid}", [ + 'custom_internal_name' => 'my-app', + 'is_consistent_container_name_enabled' => true, + ]) + ->assertOk(); + + $settings = $this->application->fresh()->settings; + expect($settings->custom_internal_name)->toBe('my-app') + ->and($settings->is_consistent_container_name_enabled)->toBeTrue(); +}); + +test('updating only custom_internal_name succeeds when consistent container naming is already on', function () { + $this->application->settings->update(['is_consistent_container_name_enabled' => true]); + + $this->withToken($this->bearerToken) + ->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_internal_name' => 'my-app']) + ->assertOk(); + + expect($this->application->fresh()->settings->custom_internal_name)->toBe('my-app'); +}); + +test('clearing custom_internal_name does not need consistent container naming', function (?string $value) { + $this->withToken($this->bearerToken) + ->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_internal_name' => $value]) + ->assertOk(); +})->with(['null' => [null], 'empty string' => ['']]); + +test('creating an application with custom_internal_name requires consistent container naming', function () { + Queue::fake(); + + $this->withToken($this->bearerToken) + ->postJson('/api/v1/applications/public', customInternalNameCreatePayload(['custom_internal_name' => 'my-app'])) + ->assertUnprocessable() + ->assertJsonPath('errors.custom_internal_name.0', CUSTOM_INTERNAL_NAME_NEEDS_CONSISTENT_NAMING); + + expect(Application::query()->count())->toBe(1); +}); + +test('creating an application with custom_internal_name and consistent container naming succeeds', function () { + Queue::fake(); + + $response = $this->withToken($this->bearerToken) + ->postJson('/api/v1/applications/public', customInternalNameCreatePayload([ + 'custom_internal_name' => 'my-app', + 'is_consistent_container_name_enabled' => true, + ])) + ->assertCreated(); + + expect(Application::query()->where('uuid', $response->json('uuid'))->sole()->settings->custom_internal_name)->toBe('my-app'); +}); diff --git a/tests/Feature/Api/ApplicationSettingsApiTest.php b/tests/Feature/Api/ApplicationSettingsApiTest.php index 8074be8bbe..7742d7ddbd 100644 --- a/tests/Feature/Api/ApplicationSettingsApiTest.php +++ b/tests/Feature/Api/ApplicationSettingsApiTest.php @@ -351,6 +351,7 @@ test('PATCH /api/v1/applications/{uuid} accepts Docker-compatible custom interna $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken)) ->patchJson("/api/v1/applications/{$this->application->uuid}", [ 'custom_internal_name' => $name, + 'is_consistent_container_name_enabled' => true, ]) ->assertOk();