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
This commit is contained in:
Andras Bacsai committed 2026-10-05 21:23:55 +02:00
1 parent ac5a0c289f
commit 50268139be
5 files changed
+171 -18

No files matched your search

@@ -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([
+6 -6
View File
@@ -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",
+6 -6
View File
@@ -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
@@ -0,0 +1,126 @@
<?php
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
beforeEach(function () {
Server::flushIdentityMap();
config(['app.maintenance.store' => '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');
});
@@ -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();