mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-25 17:01:49 -04:00
feat(api): allow setting the container name prefix
This commit is contained in:
@@ -10,6 +10,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\LocalFileVolume;
|
||||
@@ -61,6 +62,7 @@ class ApplicationsController extends Controller
|
||||
'gpu_options',
|
||||
'is_consistent_container_name_enabled',
|
||||
'custom_internal_name',
|
||||
'custom_container_name_prefix',
|
||||
];
|
||||
|
||||
private const BOOLEAN_APPLICATION_SETTING_FIELDS = [
|
||||
@@ -153,9 +155,26 @@ class ApplicationsController extends Controller
|
||||
: $request->input($field);
|
||||
}
|
||||
|
||||
if (array_key_exists('custom_container_name_prefix', $settings)) {
|
||||
$settings['custom_container_name_prefix'] = str($settings['custom_container_name_prefix'])->slug()->value() ?: null;
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
private function containerNamePrefixValidationResponse(array $settings, Server $server, ?Application $application = null): ?JsonResponse
|
||||
{
|
||||
$prefix = $settings['custom_container_name_prefix'] ?? null;
|
||||
if (! filled($prefix) || ! ApplicationSetting::isContainerNamePrefixInUse($prefix, $server, $application?->id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['custom_container_name_prefix' => ['This container name prefix is already in use by another application.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
private function applyApplicationSettings(Application $application, array $settings): void
|
||||
{
|
||||
if ($settings === []) {
|
||||
@@ -393,6 +412,7 @@ class ApplicationsController extends Controller
|
||||
'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_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.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
@@ -587,6 +607,7 @@ class ApplicationsController extends Controller
|
||||
'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_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.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
@@ -781,6 +802,7 @@ class ApplicationsController extends Controller
|
||||
'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_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.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
@@ -946,6 +968,7 @@ class ApplicationsController extends Controller
|
||||
'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_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.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
@@ -1107,6 +1130,7 @@ class ApplicationsController extends Controller
|
||||
'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_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.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
@@ -1335,6 +1359,9 @@ class ApplicationsController extends Controller
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
if ($prefixValidation = $this->containerNamePrefixValidationResponse($applicationSettings, $destination->server)) {
|
||||
return $prefixValidation;
|
||||
}
|
||||
if ($type === 'public') {
|
||||
$validationRules = [
|
||||
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
|
||||
@@ -2967,6 +2994,7 @@ class ApplicationsController extends Controller
|
||||
'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_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.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
@@ -3140,6 +3168,9 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
|
||||
$applicationSettings = $this->applicationSettingsFromRequest($request);
|
||||
if ($prefixValidation = $this->containerNamePrefixValidationResponse($applicationSettings, $application->destination->server, $application)) {
|
||||
return $prefixValidation;
|
||||
}
|
||||
$requestedBuildPack = $request->input('build_pack', $application->build_pack);
|
||||
if (($applicationSettings['is_raw_compose_deployment_enabled'] ?? false) && $requestedBuildPack !== 'dockercompose') {
|
||||
return response()->json([
|
||||
|
||||
@@ -141,6 +141,7 @@ function sharedDataApplications()
|
||||
'gpu_options' => 'string|nullable',
|
||||
'is_consistent_container_name_enabled' => 'boolean',
|
||||
'custom_internal_name' => 'string|nullable',
|
||||
'custom_container_name_prefix' => 'string|nullable|max:47',
|
||||
'preview_url_template' => 'string',
|
||||
'max_restart_count' => 'integer|min:0',
|
||||
'stop_grace_period' => 'nullable|integer|min:'.MIN_STOP_GRACE_PERIOD_SECONDS.'|max:'.MAX_STOP_GRACE_PERIOD_SECONDS,
|
||||
@@ -408,6 +409,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||
$request->offsetUnset('gpu_options');
|
||||
$request->offsetUnset('is_consistent_container_name_enabled');
|
||||
$request->offsetUnset('custom_internal_name');
|
||||
$request->offsetUnset('custom_container_name_prefix');
|
||||
$request->offsetUnset('docker_compose_raw');
|
||||
$request->offsetUnset('tags');
|
||||
}
|
||||
|
||||
@@ -467,3 +467,32 @@ test('rejects swarm fields on application update', function (string $field, mixe
|
||||
'swarm_placement_constraints' => ['swarm_placement_constraints', 'node.role==worker'],
|
||||
'is_swarm_only_worker_nodes' => ['is_swarm_only_worker_nodes', true],
|
||||
]);
|
||||
|
||||
test('PATCH /api/v1/applications/{uuid} saves a slugged container name prefix', function () {
|
||||
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_container_name_prefix' => 'My API'])
|
||||
->assertOk();
|
||||
|
||||
expect($this->application->fresh()->settings->custom_container_name_prefix)->toBe('my-api');
|
||||
|
||||
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}")
|
||||
->assertOk()
|
||||
->assertJsonPath('settings.custom_container_name_prefix', 'my-api');
|
||||
});
|
||||
|
||||
test('PATCH /api/v1/applications/{uuid} rejects a container name prefix that is in use', function () {
|
||||
$otherApplication = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
$otherApplication->settings->update(['custom_container_name_prefix' => 'shared-prefix']);
|
||||
|
||||
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_container_name_prefix' => 'shared-prefix'])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('custom_container_name_prefix');
|
||||
|
||||
expect($this->application->fresh()->settings->custom_container_name_prefix)->toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user