Add autogenerate_domain API parameter for applications
Allows API consumers to control domain auto-generation behavior. When autogenerate_domain is true (default) and no custom domains are provided, the system auto-generates a domain using the server's wildcard domain or sslip.io fallback.
- Add autogenerate_domain parameter to all 5 application creation endpoints
- Add validation and allowlist rules
- Implement domain auto-generation logic across all application types
- Add comprehensive unit tests for the feature
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8ba7533bdd
commit
eb743cf690
3 changed files with 165 additions and 1 deletions
|
|
@ -192,6 +192,7 @@ public function applications(Request $request)
|
||||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
|
'autogenerate_domain' => ['type' => 'boolean', 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -342,6 +343,7 @@ public function create_public_application(Request $request)
|
||||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
|
'autogenerate_domain' => ['type' => 'boolean', 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -492,6 +494,7 @@ public function create_private_gh_app_application(Request $request)
|
||||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
|
'autogenerate_domain' => ['type' => 'boolean', 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -626,6 +629,7 @@ public function create_private_deploy_key_application(Request $request)
|
||||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
|
'autogenerate_domain' => ['type' => 'boolean', 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -757,6 +761,7 @@ public function create_dockerfile_application(Request $request)
|
||||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
|
'autogenerate_domain' => ['type' => 'boolean', 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -927,7 +932,7 @@ private function create_application(Request $request, $type)
|
||||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override'];
|
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain'];
|
||||||
|
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
|
|
@ -940,6 +945,7 @@ private function create_application(Request $request, $type)
|
||||||
'is_http_basic_auth_enabled' => 'boolean',
|
'is_http_basic_auth_enabled' => 'boolean',
|
||||||
'http_basic_auth_username' => 'string|nullable',
|
'http_basic_auth_username' => 'string|nullable',
|
||||||
'http_basic_auth_password' => 'string|nullable',
|
'http_basic_auth_password' => 'string|nullable',
|
||||||
|
'autogenerate_domain' => 'boolean',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||||
|
|
@ -964,6 +970,7 @@ private function create_application(Request $request, $type)
|
||||||
}
|
}
|
||||||
$serverUuid = $request->server_uuid;
|
$serverUuid = $request->server_uuid;
|
||||||
$fqdn = $request->domains;
|
$fqdn = $request->domains;
|
||||||
|
$autogenerateDomain = $request->boolean('autogenerate_domain', true);
|
||||||
$instantDeploy = $request->instant_deploy;
|
$instantDeploy = $request->instant_deploy;
|
||||||
$githubAppUuid = $request->github_app_uuid;
|
$githubAppUuid = $request->github_app_uuid;
|
||||||
$useBuildServer = $request->use_build_server;
|
$useBuildServer = $request->use_build_server;
|
||||||
|
|
@ -1087,6 +1094,11 @@ private function create_application(Request $request, $type)
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
}
|
}
|
||||||
$application->refresh();
|
$application->refresh();
|
||||||
|
// Auto-generate domain if requested and no custom domain provided
|
||||||
|
if ($autogenerateDomain && blank($fqdn)) {
|
||||||
|
$application->fqdn = generateUrl(server: $server, random: $application->uuid);
|
||||||
|
$application->save();
|
||||||
|
}
|
||||||
if ($application->settings->is_container_label_readonly_enabled) {
|
if ($application->settings->is_container_label_readonly_enabled) {
|
||||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||||
$application->save();
|
$application->save();
|
||||||
|
|
@ -1238,6 +1250,11 @@ private function create_application(Request $request, $type)
|
||||||
|
|
||||||
$application->save();
|
$application->save();
|
||||||
$application->refresh();
|
$application->refresh();
|
||||||
|
// Auto-generate domain if requested and no custom domain provided
|
||||||
|
if ($autogenerateDomain && blank($fqdn)) {
|
||||||
|
$application->fqdn = generateUrl(server: $server, random: $application->uuid);
|
||||||
|
$application->save();
|
||||||
|
}
|
||||||
if (isset($useBuildServer)) {
|
if (isset($useBuildServer)) {
|
||||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -1367,6 +1384,11 @@ private function create_application(Request $request, $type)
|
||||||
$application->environment_id = $environment->id;
|
$application->environment_id = $environment->id;
|
||||||
$application->save();
|
$application->save();
|
||||||
$application->refresh();
|
$application->refresh();
|
||||||
|
// Auto-generate domain if requested and no custom domain provided
|
||||||
|
if ($autogenerateDomain && blank($fqdn)) {
|
||||||
|
$application->fqdn = generateUrl(server: $server, random: $application->uuid);
|
||||||
|
$application->save();
|
||||||
|
}
|
||||||
if (isset($useBuildServer)) {
|
if (isset($useBuildServer)) {
|
||||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -1461,6 +1483,11 @@ private function create_application(Request $request, $type)
|
||||||
$application->git_branch = 'main';
|
$application->git_branch = 'main';
|
||||||
$application->save();
|
$application->save();
|
||||||
$application->refresh();
|
$application->refresh();
|
||||||
|
// Auto-generate domain if requested and no custom domain provided
|
||||||
|
if ($autogenerateDomain && blank($fqdn)) {
|
||||||
|
$application->fqdn = generateUrl(server: $server, random: $application->uuid);
|
||||||
|
$application->save();
|
||||||
|
}
|
||||||
if (isset($useBuildServer)) {
|
if (isset($useBuildServer)) {
|
||||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -1554,6 +1581,11 @@ private function create_application(Request $request, $type)
|
||||||
$application->git_branch = 'main';
|
$application->git_branch = 'main';
|
||||||
$application->save();
|
$application->save();
|
||||||
$application->refresh();
|
$application->refresh();
|
||||||
|
// Auto-generate domain if requested and no custom domain provided
|
||||||
|
if ($autogenerateDomain && blank($fqdn)) {
|
||||||
|
$application->fqdn = generateUrl(server: $server, random: $application->uuid);
|
||||||
|
$application->save();
|
||||||
|
}
|
||||||
if (isset($useBuildServer)) {
|
if (isset($useBuildServer)) {
|
||||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
|
||||||
|
|
@ -178,4 +178,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||||
$request->offsetUnset('use_build_server');
|
$request->offsetUnset('use_build_server');
|
||||||
$request->offsetUnset('is_static');
|
$request->offsetUnset('is_static');
|
||||||
$request->offsetUnset('force_domain_override');
|
$request->offsetUnset('force_domain_override');
|
||||||
|
$request->offsetUnset('autogenerate_domain');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
131
tests/Unit/Api/ApplicationAutogenerateDomainTest.php
Normal file
131
tests/Unit/Api/ApplicationAutogenerateDomainTest.php
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\ServerSetting;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// Mock Log to prevent actual logging
|
||||||
|
Illuminate\Support\Facades\Log::shouldReceive('error')->andReturn(null);
|
||||||
|
Illuminate\Support\Facades\Log::shouldReceive('info')->andReturn(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generateUrl produces correct URL with wildcard domain', function () {
|
||||||
|
$serverSettings = Mockery::mock(ServerSetting::class);
|
||||||
|
$serverSettings->wildcard_domain = 'http://example.com';
|
||||||
|
|
||||||
|
$server = Mockery::mock(Server::class)->makePartial();
|
||||||
|
$server->shouldReceive('getAttribute')
|
||||||
|
->with('settings')
|
||||||
|
->andReturn($serverSettings);
|
||||||
|
|
||||||
|
// Mock data_get to return the wildcard domain
|
||||||
|
$wildcard = data_get($server, 'settings.wildcard_domain');
|
||||||
|
|
||||||
|
expect($wildcard)->toBe('http://example.com');
|
||||||
|
|
||||||
|
// Test the URL generation logic manually (simulating generateUrl behavior)
|
||||||
|
$random = 'abc123-def456';
|
||||||
|
$url = Spatie\Url\Url::fromString($wildcard);
|
||||||
|
$host = $url->getHost();
|
||||||
|
$scheme = $url->getScheme();
|
||||||
|
|
||||||
|
$generatedUrl = "$scheme://{$random}.$host";
|
||||||
|
|
||||||
|
expect($generatedUrl)->toBe('http://abc123-def456.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generateUrl falls back to sslip when no wildcard domain', function () {
|
||||||
|
// Test the sslip fallback logic for IPv4
|
||||||
|
$ip = '192.168.1.100';
|
||||||
|
$fallbackDomain = "http://{$ip}.sslip.io";
|
||||||
|
|
||||||
|
$random = 'test-uuid';
|
||||||
|
$url = Spatie\Url\Url::fromString($fallbackDomain);
|
||||||
|
$host = $url->getHost();
|
||||||
|
$scheme = $url->getScheme();
|
||||||
|
|
||||||
|
$generatedUrl = "$scheme://{$random}.$host";
|
||||||
|
|
||||||
|
expect($generatedUrl)->toBe('http://test-uuid.192.168.1.100.sslip.io');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('autogenerate_domain defaults to true', function () {
|
||||||
|
// Create a mock request
|
||||||
|
$request = new Illuminate\Http\Request;
|
||||||
|
|
||||||
|
// When autogenerate_domain is not set, boolean() should return the default (true)
|
||||||
|
$autogenerateDomain = $request->boolean('autogenerate_domain', true);
|
||||||
|
|
||||||
|
expect($autogenerateDomain)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('autogenerate_domain can be set to false', function () {
|
||||||
|
// Create a request with autogenerate_domain set to false
|
||||||
|
$request = new Illuminate\Http\Request(['autogenerate_domain' => false]);
|
||||||
|
|
||||||
|
$autogenerateDomain = $request->boolean('autogenerate_domain', true);
|
||||||
|
|
||||||
|
expect($autogenerateDomain)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('autogenerate_domain can be set to true explicitly', function () {
|
||||||
|
// Create a request with autogenerate_domain set to true
|
||||||
|
$request = new Illuminate\Http\Request(['autogenerate_domain' => true]);
|
||||||
|
|
||||||
|
$autogenerateDomain = $request->boolean('autogenerate_domain', true);
|
||||||
|
|
||||||
|
expect($autogenerateDomain)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('domain is not auto-generated when domains field is provided', function () {
|
||||||
|
// Test the logic: if domains is set, autogenerate should be skipped
|
||||||
|
$fqdn = 'https://myapp.example.com';
|
||||||
|
$autogenerateDomain = true;
|
||||||
|
|
||||||
|
// The condition: $autogenerateDomain && blank($fqdn)
|
||||||
|
$shouldAutogenerate = $autogenerateDomain && blank($fqdn);
|
||||||
|
|
||||||
|
expect($shouldAutogenerate)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('domain is auto-generated when domains field is empty and autogenerate is true', function () {
|
||||||
|
// Test the logic: if domains is empty and autogenerate is true, should generate
|
||||||
|
$fqdn = null;
|
||||||
|
$autogenerateDomain = true;
|
||||||
|
|
||||||
|
// The condition: $autogenerateDomain && blank($fqdn)
|
||||||
|
$shouldAutogenerate = $autogenerateDomain && blank($fqdn);
|
||||||
|
|
||||||
|
expect($shouldAutogenerate)->toBeTrue();
|
||||||
|
|
||||||
|
// Also test with empty string
|
||||||
|
$fqdn = '';
|
||||||
|
$shouldAutogenerate = $autogenerateDomain && blank($fqdn);
|
||||||
|
|
||||||
|
expect($shouldAutogenerate)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('domain is not auto-generated when autogenerate is false', function () {
|
||||||
|
// Test the logic: if autogenerate is false, should not generate even if domains is empty
|
||||||
|
$fqdn = null;
|
||||||
|
$autogenerateDomain = false;
|
||||||
|
|
||||||
|
// The condition: $autogenerateDomain && blank($fqdn)
|
||||||
|
$shouldAutogenerate = $autogenerateDomain && blank($fqdn);
|
||||||
|
|
||||||
|
expect($shouldAutogenerate)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removeUnnecessaryFieldsFromRequest removes autogenerate_domain', function () {
|
||||||
|
$request = new Illuminate\Http\Request([
|
||||||
|
'autogenerate_domain' => true,
|
||||||
|
'name' => 'test-app',
|
||||||
|
'project_uuid' => 'abc123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Simulate removeUnnecessaryFieldsFromRequest
|
||||||
|
$request->offsetUnset('autogenerate_domain');
|
||||||
|
|
||||||
|
expect($request->has('autogenerate_domain'))->toBeFalse();
|
||||||
|
expect($request->has('name'))->toBeTrue();
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue