diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 8513dc1180..ec81538a0e 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -161,6 +161,11 @@ class ApplicationsController extends Controller $settings['custom_container_name_prefix'] = str($settings['custom_container_name_prefix'])->slug()->value() ?: null; } + // Container names use custom_internal_name only with consistent container naming, so a name sent alone turns it on. + if (filled($settings['custom_internal_name'] ?? null) && ! array_key_exists('is_consistent_container_name_enabled', $settings)) { + $settings['is_consistent_container_name_enabled'] = true; + } + return $settings; } @@ -184,16 +189,11 @@ class ApplicationsController extends Controller } /** - * Container names honor custom_internal_name only with consistent container naming, so reject a name that would be ignored. + * Container names honor custom_internal_name only with consistent container naming, so reject a name sent while the request turns it off. */ - private function customInternalNameValidationResponse(array $settings, ?Application $application = null): ?JsonResponse + private function customInternalNameValidationResponse(array $settings): ?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) { + if (! filled($settings['custom_internal_name'] ?? null) || ($settings['is_consistent_container_name_enabled'] ?? true)) { return null; } @@ -439,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. Requires is_consistent_container_name_enabled to be true.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'], '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.'], @@ -634,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. Requires is_consistent_container_name_enabled to be true.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'], '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.'], @@ -829,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. Requires is_consistent_container_name_enabled to be true.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'], '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.'], @@ -995,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. Requires is_consistent_container_name_enabled to be true.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'], '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.'], @@ -1157,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. Requires is_consistent_container_name_enabled to be true.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'], '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.'], @@ -3151,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. Requires is_consistent_container_name_enabled to be true.'], + 'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'], '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.'], @@ -3335,7 +3335,7 @@ class ApplicationsController extends Controller if ($prefixValidation = $this->containerNamePrefixValidationResponse($applicationSettings, $application->destination->server, $application)) { return $prefixValidation; } - if ($internalNameValidation = $this->customInternalNameValidationResponse($applicationSettings, $application)) { + if ($internalNameValidation = $this->customInternalNameValidationResponse($applicationSettings)) { return $internalNameValidation; } $requestedBuildPack = $request->input('build_pack', $application->build_pack); diff --git a/openapi.json b/openapi.json index f3a237b856..9c89dcf413 100644 --- a/openapi.json +++ b/openapi.json @@ -950,7 +950,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." + "description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422." }, "custom_container_name_prefix": { "type": "string", @@ -1541,7 +1541,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." + "description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422." }, "custom_container_name_prefix": { "type": "string", @@ -2132,7 +2132,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." + "description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422." }, "custom_container_name_prefix": { "type": "string", @@ -2618,7 +2618,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." + "description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422." }, "custom_container_name_prefix": { "type": "string", @@ -3084,7 +3084,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." + "description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422." }, "custom_container_name_prefix": { "type": "string", @@ -3795,7 +3795,7 @@ "custom_internal_name": { "type": "string", "nullable": true, - "description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true." + "description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422." }, "custom_container_name_prefix": { "type": "string", diff --git a/openapi.yaml b/openapi.yaml index 405551392b..802aee71a7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -623,7 +623,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' + description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.' custom_container_name_prefix: type: string nullable: true @@ -1009,7 +1009,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' + description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.' custom_container_name_prefix: type: string nullable: true @@ -1395,7 +1395,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' + description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.' custom_container_name_prefix: type: string nullable: true @@ -1722,7 +1722,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' + description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.' custom_container_name_prefix: type: string nullable: true @@ -2035,7 +2035,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' + description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.' custom_container_name_prefix: type: string nullable: true @@ -2502,7 +2502,7 @@ paths: custom_internal_name: type: string nullable: true - description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.' + description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.' custom_container_name_prefix: type: string nullable: true diff --git a/tests/Feature/Api/ApplicationCustomInternalNameConsistentNamingApiTest.php b/tests/Feature/Api/ApplicationCustomInternalNameConsistentNamingApiTest.php index f745425781..e50274f3d4 100644 --- a/tests/Feature/Api/ApplicationCustomInternalNameConsistentNamingApiTest.php +++ b/tests/Feature/Api/ApplicationCustomInternalNameConsistentNamingApiTest.php @@ -51,13 +51,28 @@ function customInternalNameCreatePayload(array $overrides): array 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 () { +test('updating only custom_internal_name turns consistent container naming on', function () { $this->withToken($this->bearerToken) ->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_internal_name' => 'my-app']) + ->assertOk(); + + $settings = $this->application->fresh()->settings; + expect($settings->custom_internal_name)->toBe('my-app') + ->and($settings->is_consistent_container_name_enabled)->toBeTrue(); +}); + +test('updating custom_internal_name with consistent container naming explicitly off is rejected', function () { + $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); - expect($this->application->fresh()->settings->custom_internal_name)->toBeNull(); + $settings = $this->application->fresh()->settings; + expect($settings->custom_internal_name)->toBeNull() + ->and($settings->is_consistent_container_name_enabled)->toBeFalse(); }); test('updating custom_internal_name while turning consistent container naming off is rejected', function () { @@ -99,13 +114,30 @@ test('clearing custom_internal_name does not need consistent container naming', $this->withToken($this->bearerToken) ->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_internal_name' => $value]) ->assertOk(); + + expect($this->application->fresh()->settings->is_consistent_container_name_enabled)->toBeFalse(); })->with(['null' => [null], 'empty string' => ['']]); -test('creating an application with custom_internal_name requires consistent container naming', function () { +test('creating an application with only custom_internal_name turns consistent container naming on', function () { + Queue::fake(); + + $response = $this->withToken($this->bearerToken) + ->postJson('/api/v1/applications/public', customInternalNameCreatePayload(['custom_internal_name' => 'my-app'])) + ->assertCreated(); + + $settings = Application::query()->where('uuid', $response->json('uuid'))->sole()->settings; + expect($settings->custom_internal_name)->toBe('my-app') + ->and($settings->is_consistent_container_name_enabled)->toBeTrue(); +}); + +test('creating an application with custom_internal_name and consistent container naming explicitly off is rejected', function () { Queue::fake(); $this->withToken($this->bearerToken) - ->postJson('/api/v1/applications/public', customInternalNameCreatePayload(['custom_internal_name' => 'my-app'])) + ->postJson('/api/v1/applications/public', customInternalNameCreatePayload([ + 'custom_internal_name' => 'my-app', + 'is_consistent_container_name_enabled' => false, + ])) ->assertUnprocessable() ->assertJsonPath('errors.custom_internal_name.0', CUSTOM_INTERNAL_NAME_NEEDS_CONSISTENT_NAMING);