diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 60fd45ef4..701c92d4d 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -153,11 +153,14 @@ public function applications(Request $request) 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], 'name' => ['type' => 'string', 'description' => 'The application name.'], 'description' => ['type' => 'string', 'description' => 'The application description.'], - 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'], 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'], + 'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'], + 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], @@ -198,6 +201,7 @@ public function applications(Request $request) // 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], @@ -315,11 +319,14 @@ public function create_public_application(Request $request) 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], 'name' => ['type' => 'string', 'description' => 'The application name.'], 'description' => ['type' => 'string', 'description' => 'The application description.'], - 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'], 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'], + 'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'], + 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], @@ -359,6 +366,7 @@ public function create_public_application(Request $request) 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], @@ -476,11 +484,14 @@ public function create_private_gh_app_application(Request $request) 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], 'name' => ['type' => 'string', 'description' => 'The application name.'], 'description' => ['type' => 'string', 'description' => 'The application description.'], - 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'], 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'], + 'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'], + 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], @@ -520,6 +531,7 @@ public function create_private_gh_app_application(Request $request) 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], @@ -635,7 +647,7 @@ public function create_private_deploy_key_application(Request $request) 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], 'name' => ['type' => 'string', 'description' => 'The application name.'], 'description' => ['type' => 'string', 'description' => 'The application description.'], - 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'], 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], @@ -671,6 +683,7 @@ public function create_private_deploy_key_application(Request $request) 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], @@ -771,7 +784,7 @@ public function create_dockerfile_application(Request $request) 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], 'name' => ['type' => 'string', 'description' => 'The application name.'], 'description' => ['type' => 'string', 'description' => 'The application description.'], - 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'], 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], @@ -804,6 +817,7 @@ public function create_dockerfile_application(Request $request) 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], @@ -987,7 +1001,7 @@ private function create_application(Request $request, $type) 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', 'custom_network_aliases', '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', 'is_container_label_escape_enabled']; + $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', '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', 'custom_network_aliases', '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', 'dockerfile_location', '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', 'is_container_label_escape_enabled']; $validator = customApiValidator($request->all(), [ 'name' => 'string|max:255', @@ -1030,6 +1044,9 @@ private function create_application(Request $request, $type) $githubAppUuid = $request->github_app_uuid; $useBuildServer = $request->use_build_server; $isStatic = $request->is_static; + $isSpa = $request->is_spa; + $isAutoDeployEnabled = $request->is_auto_deploy_enabled; + $isForceHttpsEnabled = $request->is_force_https_enabled; $connectToDockerNetwork = $request->connect_to_docker_network; $customNginxConfiguration = $request->custom_nginx_configuration; $isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true); @@ -1130,39 +1147,63 @@ private function create_application(Request $request, $type) $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { $dockerComposeDomains = collect($request->docker_compose_domains); - $domainErrors = []; - foreach ($dockerComposeDomains as $index => $item) { + // Collect all URLs from all docker_compose_domains items + $urls = $dockerComposeDomains->flatMap(function ($item) { $domainValue = data_get($item, 'domain'); - if (filled($domainValue)) { - $urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim(); - str($urls)->explode(',')->each(function ($url) use (&$domainErrors) { - $url = trim($url); - if (empty($url)) { - return; - } - if (! filter_var($url, FILTER_VALIDATE_URL)) { - $domainErrors[] = "Invalid URL: {$url}"; - - return; - } - $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; - if (! in_array(strtolower($scheme), ['http', 'https'])) { - $domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; - } - }); + if (blank($domainValue)) { + return []; } + + return str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter(); + }); + + $errors = []; + $urls = $urls->map(function ($url) use (&$errors) { + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $errors[] = "Invalid URL: {$url}"; + + return $url; + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + + return $url; + }); + + $duplicates = $urls->duplicates()->unique()->values(); + if ($duplicates->isNotEmpty() && ! $request->boolean('force_domain_override')) { + $errors[] = 'The current request contains conflicting URLs: '.implode(', ', $duplicates->toArray()).' Use force_domain_override=true to proceed.'; } - if (! empty($domainErrors)) { + if (count($errors) > 0) { return response()->json([ 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_domains' => $domainErrors, - ], + 'errors' => ['docker_compose_domains' => $errors], ], 422); } + // Check for domain conflicts + if ($urls->isNotEmpty()) { + $result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId); + if (isset($result['error'])) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['docker_compose_domains' => $result['error']], + ], 422); + } + + 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); + } + } + $dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) { $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); }); @@ -1187,6 +1228,18 @@ private function create_application(Request $request, $type) $application->settings->is_static = $isStatic; $application->settings->save(); } + if (isset($isSpa)) { + $application->settings->is_spa = $isSpa; + $application->settings->save(); + } + if (isset($isAutoDeployEnabled)) { + $application->settings->is_auto_deploy_enabled = $isAutoDeployEnabled; + $application->settings->save(); + } + if (isset($isForceHttpsEnabled)) { + $application->settings->is_force_https_enabled = $isForceHttpsEnabled; + $application->settings->save(); + } if (isset($connectToDockerNetwork)) { $application->settings->connect_to_docker_network = $connectToDockerNetwork; $application->settings->save(); @@ -1319,39 +1372,63 @@ private function create_application(Request $request, $type) $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { $dockerComposeDomains = collect($request->docker_compose_domains); - $domainErrors = []; - foreach ($dockerComposeDomains as $index => $item) { + // Collect all URLs from all docker_compose_domains items + $urls = $dockerComposeDomains->flatMap(function ($item) { $domainValue = data_get($item, 'domain'); - if (filled($domainValue)) { - $urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim(); - str($urls)->explode(',')->each(function ($url) use (&$domainErrors) { - $url = trim($url); - if (empty($url)) { - return; - } - if (! filter_var($url, FILTER_VALIDATE_URL)) { - $domainErrors[] = "Invalid URL: {$url}"; - - return; - } - $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; - if (! in_array(strtolower($scheme), ['http', 'https'])) { - $domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; - } - }); + if (blank($domainValue)) { + return []; } + + return str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter(); + }); + + $errors = []; + $urls = $urls->map(function ($url) use (&$errors) { + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $errors[] = "Invalid URL: {$url}"; + + return $url; + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + + return $url; + }); + + $duplicates = $urls->duplicates()->unique()->values(); + if ($duplicates->isNotEmpty() && ! $request->boolean('force_domain_override')) { + $errors[] = 'The current request contains conflicting URLs: '.implode(', ', $duplicates->toArray()).' Use force_domain_override=true to proceed. '; } - if (! empty($domainErrors)) { + if (count($errors) > 0) { return response()->json([ 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_domains' => $domainErrors, - ], + 'errors' => ['docker_compose_domains' => $errors], ], 422); } + // Check for domain conflicts + if ($urls->isNotEmpty()) { + $result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId); + if (isset($result['error'])) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['docker_compose_domains' => $result['error']], + ], 422); + } + + 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); + } + } + $dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) { $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); }); @@ -1376,6 +1453,26 @@ private function create_application(Request $request, $type) $application->fqdn = generateUrl(server: $server, random: $application->uuid); $application->save(); } + if (isset($isStatic)) { + $application->settings->is_static = $isStatic; + $application->settings->save(); + } + if (isset($isSpa)) { + $application->settings->is_spa = $isSpa; + $application->settings->save(); + } + if (isset($isAutoDeployEnabled)) { + $application->settings->is_auto_deploy_enabled = $isAutoDeployEnabled; + $application->settings->save(); + } + if (isset($isForceHttpsEnabled)) { + $application->settings->is_force_https_enabled = $isForceHttpsEnabled; + $application->settings->save(); + } + if (isset($connectToDockerNetwork)) { + $application->settings->connect_to_docker_network = $connectToDockerNetwork; + $application->settings->save(); + } if (isset($useBuildServer)) { $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); @@ -1476,39 +1573,63 @@ private function create_application(Request $request, $type) $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { $dockerComposeDomains = collect($request->docker_compose_domains); - $domainErrors = []; - foreach ($dockerComposeDomains as $index => $item) { + // Collect all URLs from all docker_compose_domains items + $urls = $dockerComposeDomains->flatMap(function ($item) { $domainValue = data_get($item, 'domain'); - if (filled($domainValue)) { - $urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim(); - str($urls)->explode(',')->each(function ($url) use (&$domainErrors) { - $url = trim($url); - if (empty($url)) { - return; - } - if (! filter_var($url, FILTER_VALIDATE_URL)) { - $domainErrors[] = "Invalid URL: {$url}"; - - return; - } - $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; - if (! in_array(strtolower($scheme), ['http', 'https'])) { - $domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; - } - }); + if (blank($domainValue)) { + return []; } + + return str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter(); + }); + + $errors = []; + $urls = $urls->map(function ($url) use (&$errors) { + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $errors[] = "Invalid URL: {$url}"; + + return $url; + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + + return $url; + }); + + $duplicates = $urls->duplicates()->unique()->values(); + if ($duplicates->isNotEmpty() && ! $request->boolean('force_domain_override')) { + $errors[] = 'The current request contains conflicting URLs: '.implode(', ', $duplicates->toArray()).' Use force_domain_override=true to proceed.'; } - if (! empty($domainErrors)) { + if (count($errors) > 0) { return response()->json([ 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_domains' => $domainErrors, - ], + 'errors' => ['docker_compose_domains' => $errors], ], 422); } + // Check for domain conflicts + if ($urls->isNotEmpty()) { + $result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId); + if (isset($result['error'])) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['docker_compose_domains' => $result['error']], + ], 422); + } + + 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); + } + } + $dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) { $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); }); @@ -1529,6 +1650,26 @@ private function create_application(Request $request, $type) $application->fqdn = generateUrl(server: $server, random: $application->uuid); $application->save(); } + if (isset($isStatic)) { + $application->settings->is_static = $isStatic; + $application->settings->save(); + } + if (isset($isSpa)) { + $application->settings->is_spa = $isSpa; + $application->settings->save(); + } + if (isset($isAutoDeployEnabled)) { + $application->settings->is_auto_deploy_enabled = $isAutoDeployEnabled; + $application->settings->save(); + } + if (isset($isForceHttpsEnabled)) { + $application->settings->is_force_https_enabled = $isForceHttpsEnabled; + $application->settings->save(); + } + if (isset($connectToDockerNetwork)) { + $application->settings->connect_to_docker_network = $connectToDockerNetwork; + $application->settings->save(); + } if (isset($useBuildServer)) { $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); @@ -1632,6 +1773,14 @@ private function create_application(Request $request, $type) $application->fqdn = generateUrl(server: $server, random: $application->uuid); $application->save(); } + if (isset($isForceHttpsEnabled)) { + $application->settings->is_force_https_enabled = $isForceHttpsEnabled; + $application->settings->save(); + } + if (isset($connectToDockerNetwork)) { + $application->settings->connect_to_docker_network = $connectToDockerNetwork; + $application->settings->save(); + } if (isset($useBuildServer)) { $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); @@ -1734,6 +1883,14 @@ private function create_application(Request $request, $type) $application->fqdn = generateUrl(server: $server, random: $application->uuid); $application->save(); } + if (isset($isForceHttpsEnabled)) { + $application->settings->is_force_https_enabled = $isForceHttpsEnabled; + $application->settings->save(); + } + if (isset($connectToDockerNetwork)) { + $application->settings->connect_to_docker_network = $connectToDockerNetwork; + $application->settings->save(); + } if (isset($useBuildServer)) { $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); @@ -2150,11 +2307,14 @@ public function delete_by_uuid(Request $request) 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], 'name' => ['type' => 'string', 'description' => 'The application name.'], 'description' => ['type' => 'string', 'description' => 'The application description.'], - 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'], 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'], + 'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'], + 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], 'start_command' => ['type' => 'string', 'description' => 'The start command.'], @@ -2193,6 +2353,7 @@ public function delete_by_uuid(Request $request) 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], @@ -2297,7 +2458,7 @@ public function update_by_uuid(Request $request) $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','custom_network_aliases', '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_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', 'is_container_label_escape_enabled']; + $allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', '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', 'custom_network_aliases', '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', 'dockerfile_location', 'docker_compose_location', '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', 'is_container_label_escape_enabled']; $validationRules = [ 'name' => 'string|max:255', @@ -2411,18 +2572,30 @@ public function update_by_uuid(Request $request) $requestHasDomains = $request->has('domains'); if ($requestHasDomains && $server->isProxyShouldRun()) { $uuid = $request->uuid; - $fqdn = $request->domains; - $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); - $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $urls = $request->domains; + $urls = str($urls)->replaceStart(',', '')->replaceEnd(',', '')->trim(); $errors = []; - $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { - $domain = trim($domain); - if (filter_var($domain, FILTER_VALIDATE_URL) === false || ! preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) { - $errors[] = 'Invalid domain: '.$domain; + $urls = str($urls)->trim()->explode(',')->map(function ($url) use (&$errors) { + $url = trim($url); + + // If "domains" is empty clear all URLs from the fqdn column + if (blank($url)) { + return null; } - return $domain; + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $errors[] = 'Invalid URL: '.$url; + + return $url; + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + + return str($url)->lower(); }); + if (count($errors) > 0) { return response()->json([ 'message' => 'Validation failed.', @@ -2430,7 +2603,7 @@ public function update_by_uuid(Request $request) ], 422); } // Check for domain conflicts - $result = checkIfDomainIsAlreadyUsedViaAPI($fqdn, $teamId, $uuid); + $result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $uuid); if (isset($result['error'])) { return response()->json([ 'message' => 'Validation failed.', @@ -2460,39 +2633,63 @@ public function update_by_uuid(Request $request) } $dockerComposeDomains = collect($request->docker_compose_domains); - $domainErrors = []; - foreach ($dockerComposeDomains as $item) { + // Collect all URLs from all docker_compose_domains items + $urls = $dockerComposeDomains->flatMap(function ($item) { $domainValue = data_get($item, 'domain'); - if (filled($domainValue)) { - $urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim(); - str($urls)->explode(',')->each(function ($url) use (&$domainErrors) { - $url = trim($url); - if (empty($url)) { - return; - } - if (! filter_var($url, FILTER_VALIDATE_URL)) { - $domainErrors[] = "Invalid URL: {$url}"; - - return; - } - $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; - if (! in_array(strtolower($scheme), ['http', 'https'])) { - $domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; - } - }); + if (blank($domainValue)) { + return []; } + + return str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter(); + }); + + $errors = []; + $urls = $urls->map(function ($url) use (&$errors) { + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $errors[] = "Invalid URL: {$url}"; + + return $url; + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + + return $url; + }); + + $duplicates = $urls->duplicates()->unique()->values(); + if ($duplicates->isNotEmpty() && ! $request->boolean('force_domain_override')) { + $errors[] = 'The current request contains conflicting URLs: '.implode(', ', $duplicates->toArray()).' Use force_domain_override=true to proceed.'; } - if (! empty($domainErrors)) { + if (count($errors) > 0) { return response()->json([ 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_domains' => $domainErrors, - ], + 'errors' => ['docker_compose_domains' => $errors], ], 422); } + // Check for domain conflicts + if ($urls->isNotEmpty()) { + $result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $request->uuid); + if (isset($result['error'])) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['docker_compose_domains' => $result['error']], + ], 422); + } + + 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); + } + } + $yaml = Yaml::parse($application->docker_compose_raw); $services = data_get($yaml, 'services', []); $dockerComposeDomains->each(function ($domain) use ($services, $dockerComposeDomainsJson) { @@ -2505,6 +2702,9 @@ public function update_by_uuid(Request $request) } $instantDeploy = $request->instant_deploy; $isStatic = $request->is_static; + $isSpa = $request->is_spa; + $isAutoDeployEnabled = $request->is_auto_deploy_enabled; + $isForceHttpsEnabled = $request->is_force_https_enabled; $connectToDockerNetwork = $request->connect_to_docker_network; $useBuildServer = $request->use_build_server; $isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled'); @@ -2519,6 +2719,21 @@ public function update_by_uuid(Request $request) $application->settings->save(); } + if (isset($isSpa)) { + $application->settings->is_spa = $isSpa; + $application->settings->save(); + } + + if (isset($isAutoDeployEnabled)) { + $application->settings->is_auto_deploy_enabled = $isAutoDeployEnabled; + $application->settings->save(); + } + + if (isset($isForceHttpsEnabled)) { + $application->settings->is_force_https_enabled = $isForceHttpsEnabled; + $application->settings->save(); + } + if (isset($connectToDockerNetwork)) { $application->settings->connect_to_docker_network = $connectToDockerNetwork; $application->settings->save(); @@ -3626,17 +3841,29 @@ private function validateDataApplications(Request $request, Server $server) } if ($request->has('domains') && $server->isProxyShouldRun()) { $uuid = $request->uuid; - $fqdn = $request->domains; - $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); - $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $urls = $request->domains; + $urls = str($urls)->replaceEnd(',', '')->trim(); + $urls = str($urls)->replaceStart(',', '')->trim(); $errors = []; - $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { - $domain = trim($domain); - if (filter_var($domain, FILTER_VALIDATE_URL) === false) { - $errors[] = 'Invalid domain: '.$domain; + $urls = str($urls)->trim()->explode(',')->map(function ($url) use (&$errors) { + $url = trim($url); + + // If "domains" is empty clear all URLs from the fqdn column + if (blank($url)) { + return null; } - return str($domain)->lower(); + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $errors[] = 'Invalid URL: '.$url; + + return str($url)->lower(); + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + + return str($url)->lower(); }); if (count($errors) > 0) { return response()->json([ @@ -3645,7 +3872,7 @@ private function validateDataApplications(Request $request, Server $server) ], 422); } // Check for domain conflicts - $result = checkIfDomainIsAlreadyUsedViaAPI($fqdn, $teamId, $uuid); + $result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $uuid); if (isset($result['error'])) { return response()->json([ 'message' => 'Validation failed.', diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 55cff42d0..d5c2c996b 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -86,8 +86,11 @@ function sharedDataApplications() 'git_branch' => 'string', 'build_pack' => Rule::enum(BuildPackTypes::class), 'is_static' => 'boolean', + 'is_spa' => 'boolean', + 'is_auto_deploy_enabled' => 'boolean', + 'is_force_https_enabled' => 'boolean', 'static_image' => Rule::enum(StaticImageTypes::class), - 'domains' => 'string', + 'domains' => 'string|nullable', 'redirect' => Rule::enum(RedirectTypes::class), 'git_commit_sha' => 'string', 'docker_registry_image_name' => 'string|nullable', @@ -129,6 +132,7 @@ function sharedDataApplications() 'manual_webhook_secret_gitlab' => 'string|nullable', 'manual_webhook_secret_bitbucket' => 'string|nullable', 'manual_webhook_secret_gitea' => 'string|nullable', + 'dockerfile_location' => 'string|nullable', 'docker_compose_location' => 'string', 'docker_compose' => 'string|nullable', 'docker_compose_domains' => 'array|nullable', @@ -177,6 +181,10 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('private_key_uuid'); $request->offsetUnset('use_build_server'); $request->offsetUnset('is_static'); + $request->offsetUnset('is_spa'); + $request->offsetUnset('is_auto_deploy_enabled'); + $request->offsetUnset('is_force_https_enabled'); + $request->offsetUnset('connect_to_docker_network'); $request->offsetUnset('force_domain_override'); $request->offsetUnset('autogenerate_domain'); $request->offsetUnset('is_container_label_escape_enabled'); diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php index 5b665890c..ff77a78e2 100644 --- a/bootstrap/helpers/domains.php +++ b/bootstrap/helpers/domains.php @@ -158,8 +158,7 @@ function checkIfDomainIsAlreadyUsedViaAPI(Collection|array $domains, ?string $te return str($domain); }); - // Check applications within the same team - $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid', 'name', 'id']); + $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid', 'name', 'id', 'docker_compose_domains', 'build_pack']); $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->with('service:id,name')->get(['fqdn', 'uuid', 'id', 'service_id']); if ($uuid) { @@ -168,23 +167,51 @@ function checkIfDomainIsAlreadyUsedViaAPI(Collection|array $domains, ?string $te } 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('/'); + if (! is_null($app->fqdn)) { + $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}'", + ]; + } } - $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}'", - ]; + } + + if ($app->build_pack === 'dockercompose' && ! empty($app->docker_compose_domains)) { + $dockerComposeDomains = json_decode($app->docker_compose_domains, true); + if (is_array($dockerComposeDomains)) { + foreach ($dockerComposeDomains as $serviceName => $domainConfig) { + $domainValue = data_get($domainConfig, 'domain'); + if (empty($domainValue)) { + continue; + } + $list_of_domains = collect(explode(',', $domainValue))->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', + 'service_name' => $serviceName, + 'message' => "Domain $naked_domain is already in use by application '{$app->name}' (service: {$serviceName})", + ]; + } + } + } } } } diff --git a/openapi.json b/openapi.json index a94ef79b1..7bb1ff8f0 100644 --- a/openapi.json +++ b/openapi.json @@ -135,7 +135,7 @@ }, "domains": { "type": "string", - "description": "The application domains." + "description": "The application URLs in a comma-separated list." }, "git_commit_sha": { "type": "string", @@ -153,6 +153,18 @@ "type": "boolean", "description": "The flag to indicate if the application is static." }, + "is_spa": { + "type": "boolean", + "description": "The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true." + }, + "is_auto_deploy_enabled": { + "type": "boolean", + "description": "The flag to indicate if auto-deploy is enabled on git push. Defaults to true." + }, + "is_force_https_enabled": { + "type": "boolean", + "description": "The flag to indicate if HTTPS is forced. Defaults to true." + }, "static_image": { "type": "string", "enum": [ @@ -322,6 +334,10 @@ "type": "string", "description": "The Dockerfile content." }, + "dockerfile_location": { + "type": "string", + "description": "The Dockerfile location in the repository." + }, "docker_compose_location": { "type": "string", "description": "The Docker Compose location." @@ -564,7 +580,7 @@ }, "domains": { "type": "string", - "description": "The application domains." + "description": "The application URLs in a comma-separated list." }, "git_commit_sha": { "type": "string", @@ -582,6 +598,18 @@ "type": "boolean", "description": "The flag to indicate if the application is static." }, + "is_spa": { + "type": "boolean", + "description": "The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true." + }, + "is_auto_deploy_enabled": { + "type": "boolean", + "description": "The flag to indicate if auto-deploy is enabled on git push. Defaults to true." + }, + "is_force_https_enabled": { + "type": "boolean", + "description": "The flag to indicate if HTTPS is forced. Defaults to true." + }, "static_image": { "type": "string", "enum": [ @@ -751,6 +779,10 @@ "type": "string", "description": "The Dockerfile content." }, + "dockerfile_location": { + "type": "string", + "description": "The Dockerfile location in the repository" + }, "docker_compose_location": { "type": "string", "description": "The Docker Compose location." @@ -993,7 +1025,7 @@ }, "domains": { "type": "string", - "description": "The application domains." + "description": "The application URLs in a comma-separated list." }, "git_commit_sha": { "type": "string", @@ -1011,6 +1043,18 @@ "type": "boolean", "description": "The flag to indicate if the application is static." }, + "is_spa": { + "type": "boolean", + "description": "The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true." + }, + "is_auto_deploy_enabled": { + "type": "boolean", + "description": "The flag to indicate if auto-deploy is enabled on git push. Defaults to true." + }, + "is_force_https_enabled": { + "type": "boolean", + "description": "The flag to indicate if HTTPS is forced. Defaults to true." + }, "static_image": { "type": "string", "enum": [ @@ -1180,6 +1224,10 @@ "type": "string", "description": "The Dockerfile content." }, + "dockerfile_location": { + "type": "string", + "description": "The Dockerfile location in the repository." + }, "docker_compose_location": { "type": "string", "description": "The Docker Compose location." @@ -1410,7 +1458,7 @@ }, "domains": { "type": "string", - "description": "The application domains." + "description": "The application URLs in a comma-separated list." }, "docker_registry_image_name": { "type": "string", @@ -1562,6 +1610,10 @@ "type": "boolean", "description": "The flag to indicate if the application should be deployed instantly." }, + "is_force_https_enabled": { + "type": "boolean", + "description": "The flag to indicate if HTTPS is forced. Defaults to true." + }, "use_build_server": { "type": "boolean", "nullable": true, @@ -1754,7 +1806,7 @@ }, "domains": { "type": "string", - "description": "The application domains." + "description": "The application URLs in a comma-separated list." }, "ports_mappings": { "type": "string", @@ -1894,6 +1946,10 @@ "type": "boolean", "description": "The flag to indicate if the application should be deployed instantly." }, + "is_force_https_enabled": { + "type": "boolean", + "description": "The flag to indicate if HTTPS is forced. Defaults to true." + }, "use_build_server": { "type": "boolean", "nullable": true, @@ -2402,7 +2458,7 @@ }, "domains": { "type": "string", - "description": "The application domains." + "description": "The application URLs in a comma-separated list." }, "git_commit_sha": { "type": "string", @@ -2420,6 +2476,18 @@ "type": "boolean", "description": "The flag to indicate if the application is static." }, + "is_spa": { + "type": "boolean", + "description": "The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true." + }, + "is_auto_deploy_enabled": { + "type": "boolean", + "description": "The flag to indicate if auto-deploy is enabled on git push. Defaults to true." + }, + "is_force_https_enabled": { + "type": "boolean", + "description": "The flag to indicate if HTTPS is forced. Defaults to true." + }, "install_command": { "type": "string", "description": "The install command." @@ -2582,6 +2650,10 @@ "type": "string", "description": "The Dockerfile content." }, + "dockerfile_location": { + "type": "string", + "description": "The Dockerfile location in the repository." + }, "docker_compose_location": { "type": "string", "description": "The Docker Compose location." diff --git a/openapi.yaml b/openapi.yaml index 75ccb69fe..5d7adec32 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -97,7 +97,7 @@ paths: description: 'The application description.' domains: type: string - description: 'The application domains.' + description: 'The application URLs in a comma-separated list.' git_commit_sha: type: string description: 'The git commit SHA.' @@ -110,6 +110,15 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + is_spa: + type: boolean + description: 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.' + is_auto_deploy_enabled: + type: boolean + description: 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.' + is_force_https_enabled: + type: boolean + description: 'The flag to indicate if HTTPS is forced. Defaults to true.' static_image: type: string enum: ['nginx:alpine'] @@ -234,6 +243,9 @@ paths: dockerfile: type: string description: 'The Dockerfile content.' + dockerfile_location: + type: string + description: 'The Dockerfile location in the repository.' docker_compose_location: type: string description: 'The Docker Compose location.' @@ -369,7 +381,7 @@ paths: description: 'The application description.' domains: type: string - description: 'The application domains.' + description: 'The application URLs in a comma-separated list.' git_commit_sha: type: string description: 'The git commit SHA.' @@ -382,6 +394,15 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + is_spa: + type: boolean + description: 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.' + is_auto_deploy_enabled: + type: boolean + description: 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.' + is_force_https_enabled: + type: boolean + description: 'The flag to indicate if HTTPS is forced. Defaults to true.' static_image: type: string enum: ['nginx:alpine'] @@ -506,6 +527,9 @@ paths: dockerfile: type: string description: 'The Dockerfile content.' + dockerfile_location: + type: string + description: 'The Dockerfile location in the repository' docker_compose_location: type: string description: 'The Docker Compose location.' @@ -641,7 +665,7 @@ paths: description: 'The application description.' domains: type: string - description: 'The application domains.' + description: 'The application URLs in a comma-separated list.' git_commit_sha: type: string description: 'The git commit SHA.' @@ -654,6 +678,15 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + is_spa: + type: boolean + description: 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.' + is_auto_deploy_enabled: + type: boolean + description: 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.' + is_force_https_enabled: + type: boolean + description: 'The flag to indicate if HTTPS is forced. Defaults to true.' static_image: type: string enum: ['nginx:alpine'] @@ -778,6 +811,9 @@ paths: dockerfile: type: string description: 'The Dockerfile content.' + dockerfile_location: + type: string + description: 'The Dockerfile location in the repository.' docker_compose_location: type: string description: 'The Docker Compose location.' @@ -903,7 +939,7 @@ paths: description: 'The application description.' domains: type: string - description: 'The application domains.' + description: 'The application URLs in a comma-separated list.' docker_registry_image_name: type: string description: 'The docker registry image name.' @@ -1015,6 +1051,9 @@ paths: instant_deploy: type: boolean description: 'The flag to indicate if the application should be deployed instantly.' + is_force_https_enabled: + type: boolean + description: 'The flag to indicate if HTTPS is forced. Defaults to true.' use_build_server: type: boolean nullable: true @@ -1124,7 +1163,7 @@ paths: description: 'The application description.' domains: type: string - description: 'The application domains.' + description: 'The application URLs in a comma-separated list.' ports_mappings: type: string description: 'The ports mappings.' @@ -1227,6 +1266,9 @@ paths: instant_deploy: type: boolean description: 'The flag to indicate if the application should be deployed instantly.' + is_force_https_enabled: + type: boolean + description: 'The flag to indicate if HTTPS is forced. Defaults to true.' use_build_server: type: boolean nullable: true @@ -1524,7 +1566,7 @@ paths: description: 'The application description.' domains: type: string - description: 'The application domains.' + description: 'The application URLs in a comma-separated list.' git_commit_sha: type: string description: 'The git commit SHA.' @@ -1537,6 +1579,15 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + is_spa: + type: boolean + description: 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.' + is_auto_deploy_enabled: + type: boolean + description: 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.' + is_force_https_enabled: + type: boolean + description: 'The flag to indicate if HTTPS is forced. Defaults to true.' install_command: type: string description: 'The install command.' @@ -1657,6 +1708,9 @@ paths: dockerfile: type: string description: 'The Dockerfile content.' + dockerfile_location: + type: string + description: 'The Dockerfile location in the repository.' docker_compose_location: type: string description: 'The Docker Compose location.'