diff --git a/app/Http/Controllers/Api/Applications.php b/app/Http/Controllers/Api/Applications.php index 0e651f476..c73b894ba 100644 --- a/app/Http/Controllers/Api/Applications.php +++ b/app/Http/Controllers/Api/Applications.php @@ -37,7 +37,7 @@ public function create_application(Request $request) { ray()->clearAll(); - $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', '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']; + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', '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']; $teamId = get_team_id_from_token(); if (is_null($teamId)) { return invalid_token(); @@ -94,9 +94,6 @@ public function create_application(Request $request) if (! $environment) { return response()->json(['error' => 'Environment not found.'], 404); } - if (! $request->has('name')) { - $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); - } $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); if (! $server) { return response()->json(['error' => 'Server not found.'], 404); @@ -110,6 +107,9 @@ public function create_application(Request $request) } $destination = $destinations->first(); if ($type === 'public') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } $validator = customApiValidator($request->all(), [ sharedDataApplications(), 'git_repository' => 'string|required', @@ -151,6 +151,9 @@ public function create_application(Request $request) return response()->json(serialize_api_response($application)); } elseif ($type === 'private-gh-app') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } $validator = customApiValidator($request->all(), [ sharedDataApplications(), 'git_repository' => 'string|required', @@ -204,6 +207,9 @@ public function create_application(Request $request) return response()->json(serialize_api_response($application)); } elseif ($type === 'private-deploy-key') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } $validator = customApiValidator($request->all(), [ sharedDataApplications(), 'git_repository' => 'string|required', @@ -249,6 +255,75 @@ public function create_application(Request $request) ); } + return response()->json(serialize_api_response($application)); + } elseif ($type === 'dockerfile') { + if (! $request->has('name')) { + $request->offsetSet('name', 'dockerfile-'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'dockerfile' => 'string|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->dockerfile)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + if (mb_detect_encoding($dockerFile, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + $this->removeUnnecessaryFieldsFromRequest($request); + + $port = get_port_from_dockerfile($request->dockerfile); + if (! $port) { + $port = 80; + } + + $application = new Application(); + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->ports_exposes = $port; + $application->build_pack = 'dockerfile'; + $application->dockerfile = $dockerFile; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + return response()->json(serialize_api_response($application)); } @@ -826,6 +901,8 @@ private function removeUnnecessaryFieldsFromRequest(Request $request) private function validateDataApplications(Request $request, Server $server) { + $teamId = get_team_id_from_token(); + // Validate ports_mappings if ($request->has('ports_mappings')) { $ports = []; @@ -881,6 +958,14 @@ private function validateDataApplications(Request $request, Server $server) 'errors' => $errors, ], 422); } + if (checkIfDomainIsAlreadyUsed($fqdn, $teamId)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'One of the domain is already used.', + ], + ], 422); + } } } } diff --git a/app/Models/Server.php b/app/Models/Server.php index cd6cc9890..ea487fee7 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -502,7 +502,7 @@ public function checkSentinel() $sentinel_found = json_decode($sentinel_found, true); $status = data_get($sentinel_found, '0.State.Status', 'exited'); if ($status !== 'running') { - ray('Sentinel is not running, starting it...'); + // ray('Sentinel is not running, starting it...'); PullSentinelImageJob::dispatch($this); } else { // ray('Sentinel is running'); diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 98c1cf4e7..6690f254e 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -27,6 +27,11 @@ public function restart() instant_remote_process(["docker restart {$container_id}"], $this->service->server); } + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index a4676cfd4..1d70b674c 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -56,6 +56,8 @@ use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; +use function PHPUnit\Framework\isEmpty; + function base_configuration_dir(): string { return '/data/coolify'; @@ -2129,6 +2131,75 @@ function ip_match($ip, $cidrs, &$match = null) return false; } +function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = 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'); + $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get('fqdn'); + $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 (isEmpty($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; + } + $settings = InstanceSettings::get(); + 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 check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { if ($resource) {