From 75b61a6b009839443047b10daf14f190dd24aed4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:21:30 +0200 Subject: [PATCH] feat(domains): add force_domain_override option and enhance domain conflict detection responses --- .../Api/ApplicationsController.php | 375 +++++++---- bootstrap/helpers/api.php | 1 + bootstrap/helpers/domains.php | 98 +++ bootstrap/helpers/shared.php | 73 --- openapi.json | 600 +++++++++++++++++- openapi.yaml | 217 ++++++- 6 files changed, 1142 insertions(+), 222 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index c07ac354d..16413d2ad 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -190,6 +190,7 @@ class ApplicationsController extends Controller 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username 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.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -217,6 +218,35 @@ class ApplicationsController extends Controller response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_public_application(Request $request) @@ -310,6 +340,7 @@ class ApplicationsController extends Controller 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username 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.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -337,6 +368,35 @@ class ApplicationsController extends Controller response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_private_gh_app_application(Request $request) @@ -430,6 +490,7 @@ class ApplicationsController extends Controller 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username 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.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -457,6 +518,35 @@ class ApplicationsController extends Controller response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_private_deploy_key_application(Request $request) @@ -534,6 +624,7 @@ class ApplicationsController extends Controller 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username 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.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -561,6 +652,35 @@ class ApplicationsController extends Controller response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_dockerfile_application(Request $request) @@ -635,6 +755,7 @@ class ApplicationsController extends Controller 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username 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.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -662,6 +783,35 @@ class ApplicationsController extends Controller response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_dockerimage_application(Request $request) @@ -699,6 +849,7 @@ class ApplicationsController extends Controller 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], '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.'], ], ) ), @@ -726,6 +877,35 @@ class ApplicationsController extends Controller response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_dockercompose_application(Request $request) @@ -746,7 +926,7 @@ class ApplicationsController extends Controller if ($return instanceof \Illuminate\Http\JsonResponse) { 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']; + $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']; $validator = customApiValidator($request->all(), [ 'name' => 'string|max:255', @@ -1380,7 +1560,7 @@ class ApplicationsController extends Controller 'domains' => data_get($application, 'domains'), ]))->setStatusCode(201); } elseif ($type === 'dockercompose') { - $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw']; + $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw', 'force_domain_override']; $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { @@ -1810,6 +1990,7 @@ class ApplicationsController extends Controller 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], '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.'], ], ) ), @@ -1843,6 +2024,35 @@ class ApplicationsController extends Controller response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function update_by_uuid(Request $request) @@ -1866,7 +2076,7 @@ class ApplicationsController extends Controller $this->authorize('update', $application); $server = $application->destination->server; - $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', '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', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network']; + $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', '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', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override']; $validationRules = [ 'name' => 'string|max:255', @@ -1982,14 +2192,23 @@ class ApplicationsController extends Controller 'errors' => $errors, ], 422); } - if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + // Check for domain conflicts + $result = checkIfDomainIsAlreadyUsedViaAPI($fqdn, $teamId, $uuid); + if (isset($result['error'])) { return response()->json([ 'message' => 'Validation failed.', - 'errors' => [ - 'domains' => 'One of the domain is already used.', - ], + 'errors' => ['domains' => $result['error']], ], 422); } + + // If there are conflicts and force is not enabled, return warning + if ($result['hasConflicts'] && ! $request->boolean('force_domain_override')) { + return response()->json([ + 'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.', + 'conflicts' => $result['conflicts'], + 'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.', + ], 409); + } } $dockerComposeDomainsJson = collect(); @@ -3102,131 +3321,6 @@ class ApplicationsController extends Controller ); } - // #[OA\Post( - // summary: 'Execute Command', - // description: "Execute a command on the application's current container.", - // path: '/applications/{uuid}/execute', - // operationId: 'execute-command-application', - // security: [ - // ['bearerAuth' => []], - // ], - // tags: ['Applications'], - // parameters: [ - // new OA\Parameter( - // name: 'uuid', - // in: 'path', - // description: 'UUID of the application.', - // required: true, - // schema: new OA\Schema( - // type: 'string', - // format: 'uuid', - // ) - // ), - // ], - // requestBody: new OA\RequestBody( - // required: true, - // description: 'Command to execute.', - // content: new OA\MediaType( - // mediaType: 'application/json', - // schema: new OA\Schema( - // type: 'object', - // properties: [ - // 'command' => ['type' => 'string', 'description' => 'Command to execute.'], - // ], - // ), - // ), - // ), - // responses: [ - // new OA\Response( - // response: 200, - // description: "Execute a command on the application's current container.", - // content: [ - // new OA\MediaType( - // mediaType: 'application/json', - // schema: new OA\Schema( - // type: 'object', - // properties: [ - // 'message' => ['type' => 'string', 'example' => 'Command executed.'], - // 'response' => ['type' => 'string'], - // ] - // ) - // ), - // ] - // ), - // new OA\Response( - // response: 401, - // ref: '#/components/responses/401', - // ), - // new OA\Response( - // response: 400, - // ref: '#/components/responses/400', - // ), - // new OA\Response( - // response: 404, - // ref: '#/components/responses/404', - // ), - // ] - // )] - // public function execute_command_by_uuid(Request $request) - // { - // // TODO: Need to review this from security perspective, to not allow arbitrary command execution - // $allowedFields = ['command']; - // $teamId = getTeamIdFromToken(); - // if (is_null($teamId)) { - // return invalidTokenResponse(); - // } - // $uuid = $request->route('uuid'); - // if (! $uuid) { - // return response()->json(['message' => 'UUID is required.'], 400); - // } - // $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - // if (! $application) { - // return response()->json(['message' => 'Application not found.'], 404); - // } - // $return = validateIncomingRequest($request); - // if ($return instanceof \Illuminate\Http\JsonResponse) { - // return $return; - // } - // $validator = customApiValidator($request->all(), [ - // 'command' => 'string|required', - // ]); - - // $extraFields = array_diff(array_keys($request->all()), $allowedFields); - // if ($validator->fails() || ! empty($extraFields)) { - // $errors = $validator->errors(); - // if (! empty($extraFields)) { - // foreach ($extraFields as $field) { - // $errors->add($field, 'This field is not allowed.'); - // } - // } - - // return response()->json([ - // 'message' => 'Validation failed.', - // 'errors' => $errors, - // ], 422); - // } - - // $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail(); - // $status = getContainerStatus($application->destination->server, $container['Names']); - - // if ($status !== 'running') { - // return response()->json([ - // 'message' => 'Application is not running.', - // ], 400); - // } - - // $commands = collect([ - // executeInDocker($container['Names'], $request->command), - // ]); - - // $res = instant_remote_process(command: $commands, server: $application->destination->server); - - // return response()->json([ - // 'message' => 'Command executed.', - // 'response' => $res, - // ]); - // } - private function validateDataApplications(Request $request, Server $server) { $teamId = getTeamIdFromToken(); @@ -3286,14 +3380,23 @@ class ApplicationsController extends Controller 'errors' => $errors, ], 422); } - if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + // Check for domain conflicts + $result = checkIfDomainIsAlreadyUsedViaAPI($fqdn, $teamId, $uuid); + if (isset($result['error'])) { return response()->json([ 'message' => 'Validation failed.', - 'errors' => [ - 'domains' => 'One of the domain is already used.', - ], + 'errors' => ['domains' => $result['error']], ], 422); } + + // If there are conflicts and force is not enabled, return warning + if ($result['hasConflicts'] && ! $request->boolean('force_domain_override')) { + return response()->json([ + 'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.', + 'conflicts' => $result['conflicts'], + 'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.', + ], 409); + } } } } diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 307c7ed1b..5d0f9a2a7 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -176,4 +176,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('private_key_uuid'); $request->offsetUnset('use_build_server'); $request->offsetUnset('is_static'); + $request->offsetUnset('force_domain_override'); } diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php index b562873b5..5b665890c 100644 --- a/bootstrap/helpers/domains.php +++ b/bootstrap/helpers/domains.php @@ -2,6 +2,7 @@ use App\Models\Application; use App\Models\ServiceApplication; +use Illuminate\Support\Collection; function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { @@ -137,3 +138,100 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null, 'hasConflicts' => count($conflicts) > 0, ]; } + +function checkIfDomainIsAlreadyUsedViaAPI(Collection|array $domains, ?string $teamId = null, ?string $uuid = null) +{ + $conflicts = []; + + if (is_null($teamId)) { + return ['error' => 'Team ID is required.']; + } + if (is_array($domains)) { + $domains = collect($domains); + } + + $domains = $domains->map(function ($domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + + return str($domain); + }); + + // Check applications within the same team + $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid', 'name', 'id']); + $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->with('service:id,name')->get(['fqdn', 'uuid', 'id', 'service_id']); + + if ($uuid) { + $applications = $applications->filter(fn ($app) => $app->uuid !== $uuid); + $serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid); + } + + foreach ($applications as $app) { + if (is_null($app->fqdn)) { + continue; + } + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->name, + 'resource_uuid' => $app->uuid, + 'resource_type' => 'application', + 'message' => "Domain $naked_domain is already in use by application '{$app->name}'", + ]; + } + } + } + + foreach ($serviceApplications as $app) { + if (str($app->fqdn)->isEmpty()) { + continue; + } + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->service->name ?? 'Unknown Service', + 'resource_uuid' => $app->uuid, + 'resource_type' => 'service', + 'message' => "Domain $naked_domain is already in use by service '{$app->service->name}'", + ]; + } + } + } + + // Check instance-level domain + $settings = instanceSettings(); + if (data_get($settings, 'fqdn')) { + $domain = data_get($settings, 'fqdn'); + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => 'Coolify Instance', + 'resource_uuid' => null, + 'resource_type' => 'instance', + 'message' => "Domain $naked_domain is already in use by this Coolify instance", + ]; + } + } + + return [ + 'conflicts' => $conflicts, + 'hasConflicts' => count($conflicts) > 0, + ]; +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 4743a811b..e01f4d58b 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1084,79 +1084,6 @@ function check_ip_against_allowlist($ip, $allowlist) return false; } -function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = null, ?string $uuid = null) -{ - if (is_null($teamId)) { - return response()->json(['error' => 'Team ID is required.'], 400); - } - if (is_array($domains)) { - $domains = collect($domains); - } - - $domains = $domains->map(function ($domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - - return str($domain); - }); - $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); - $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); - if ($uuid) { - $applications = $applications->filter(fn ($app) => $app->uuid !== $uuid); - $serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid); - } - $domainFound = false; - foreach ($applications as $app) { - if (is_null($app->fqdn)) { - continue; - } - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); - foreach ($list_of_domains as $domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - $domainFound = true; - break; - } - } - } - if ($domainFound) { - return true; - } - foreach ($serviceApplications as $app) { - if (str($app->fqdn)->isEmpty()) { - continue; - } - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); - foreach ($list_of_domains as $domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - $domainFound = true; - break; - } - } - } - if ($domainFound) { - return true; - } - $settings = instanceSettings(); - if (data_get($settings, 'fqdn')) { - $domain = data_get($settings, 'fqdn'); - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - return true; - } - } -} function parseCommandsByLineForSudo(Collection $commands, Server $server): array { diff --git a/openapi.json b/openapi.json index 791828aed..ad20633c4 100644 --- a/openapi.json +++ b/openapi.json @@ -357,6 +357,10 @@ "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." } }, "type": "object" @@ -385,6 +389,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -709,6 +767,10 @@ "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." } }, "type": "object" @@ -737,6 +799,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -1061,6 +1177,10 @@ "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." } }, "type": "object" @@ -1089,6 +1209,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -1342,6 +1516,10 @@ "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." } }, "type": "object" @@ -1370,6 +1548,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -1606,6 +1838,10 @@ "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." } }, "type": "object" @@ -1634,6 +1870,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -1709,6 +1999,10 @@ "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." } }, "type": "object" @@ -1737,6 +2031,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -2175,6 +2523,10 @@ "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." } }, "type": "object" @@ -2206,6 +2558,60 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -5196,6 +5602,190 @@ ] } }, + "\/projects\/{uuid}\/environments": { + "get": { + "tags": [ + "Projects" + ], + "summary": "List Environments", + "description": "List all environments in a project.", + "operationId": "get-environments", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Project UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of environments", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/Environment" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "description": "Project not found." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Projects" + ], + "summary": "Create Environment", + "description": "Create environment in project.", + "operationId": "create-environment", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Project UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Environment created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "The name of the environment." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Environment created.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string", + "example": "env123", + "description": "The UUID of the environment." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "description": "Project not found." + }, + "409": { + "description": "Environment with this name already exists." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/projects\/{uuid}\/environments\/{environment_name_or_uuid}": { + "delete": { + "tags": [ + "Projects" + ], + "summary": "Delete Environment", + "description": "Delete environment by name or UUID. Environment must be empty.", + "operationId": "delete-environment", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Project UUID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "environment_name_or_uuid", + "in": "path", + "description": "Environment name or UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Environment deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Environment deleted." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "description": "Environment has resources, so it cannot be deleted." + }, + "404": { + "description": "Project or environment not found." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/resources": { "get": { "tags": [ @@ -6412,13 +7002,6 @@ "content": { "application\/json": { "schema": { - "required": [ - "server_uuid", - "project_uuid", - "environment_name", - "environment_uuid", - "docker_compose_raw" - ], "properties": { "name": { "type": "string", @@ -8026,6 +8609,9 @@ "is_swarm_worker": { "type": "boolean" }, + "is_terminal_enabled": { + "type": "boolean" + }, "is_usable": { "type": "boolean" }, diff --git a/openapi.yaml b/openapi.yaml index 3f2fa1c59..ddd814e32 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -262,6 +262,9 @@ paths: 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.' type: object responses: '201': @@ -276,6 +279,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -515,6 +528,9 @@ paths: 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.' type: object responses: '201': @@ -529,6 +545,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -768,6 +794,9 @@ paths: 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.' type: object responses: '201': @@ -782,6 +811,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -968,6 +1007,9 @@ paths: 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.' type: object responses: '201': @@ -982,6 +1024,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -1159,6 +1211,9 @@ paths: 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.' type: object responses: '201': @@ -1173,6 +1228,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -1230,6 +1295,9 @@ paths: 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.' type: object responses: '201': @@ -1244,6 +1312,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -1560,6 +1638,9 @@ paths: 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.' type: object responses: '200': @@ -1576,6 +1657,16 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -3570,6 +3661,124 @@ paths: security: - bearerAuth: [] + '/projects/{uuid}/environments': + get: + tags: + - Projects + summary: 'List Environments' + description: 'List all environments in a project.' + operationId: get-environments + parameters: + - + name: uuid + in: path + description: 'Project UUID' + required: true + schema: + type: string + responses: + '200': + description: 'List of environments' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Environment' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + description: 'Project not found.' + security: + - + bearerAuth: [] + post: + tags: + - Projects + summary: 'Create Environment' + description: 'Create environment in project.' + operationId: create-environment + parameters: + - + name: uuid + in: path + description: 'Project UUID' + required: true + schema: + type: string + requestBody: + description: 'Environment created.' + required: true + content: + application/json: + schema: + properties: + name: + type: string + description: 'The name of the environment.' + type: object + responses: + '201': + description: 'Environment created.' + content: + application/json: + schema: + properties: + uuid: { type: string, example: env123, description: 'The UUID of the environment.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + description: 'Project not found.' + '409': + description: 'Environment with this name already exists.' + security: + - + bearerAuth: [] + '/projects/{uuid}/environments/{environment_name_or_uuid}': + delete: + tags: + - Projects + summary: 'Delete Environment' + description: 'Delete environment by name or UUID. Environment must be empty.' + operationId: delete-environment + parameters: + - + name: uuid + in: path + description: 'Project UUID' + required: true + schema: + type: string + - + name: environment_name_or_uuid + in: path + description: 'Environment name or UUID' + required: true + schema: + type: string + responses: + '200': + description: 'Environment deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + description: 'Environment has resources, so it cannot be deleted.' + '404': + description: 'Project or environment not found.' + security: + - + bearerAuth: [] /resources: get: tags: @@ -4289,12 +4498,6 @@ paths: content: application/json: schema: - required: - - server_uuid - - project_uuid - - environment_name - - environment_uuid - - docker_compose_raw properties: name: type: string @@ -5377,6 +5580,8 @@ components: type: boolean is_swarm_worker: type: boolean + is_terminal_enabled: + type: boolean is_usable: type: boolean logdrain_axiom_api_key: