diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index a42f03eb5..870b5b7e5 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -217,6 +217,10 @@ public function handle(StandaloneMongodb $database) if ($this->database->enable_ssl) { $commandParts = ['mongod']; + if (! empty($this->database->mongo_conf)) { + $commandParts = ['mongod', '--config', '/etc/mongo/mongod.conf']; + } + $sslConfig = match ($this->database->ssl_mode) { 'allow' => [ '--tlsMode=allowTLS', diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index ba4c2311a..754feecb1 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -14,15 +14,26 @@ class CleanupDocker public function handle(Server $server) { $settings = instanceSettings(); + $realtimeImage = config('constants.coolify.realtime_image'); + $realtimeImageVersion = config('constants.coolify.realtime_version'); + $realtimeImageWithVersion = "$realtimeImage:$realtimeImageVersion"; + $realtimeImageWithoutPrefix = 'coollabsio/coolify-realtime'; + $realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion"; + $helperImageVersion = data_get($settings, 'helper_version'); $helperImage = config('constants.coolify.helper_image'); $helperImageWithVersion = "$helperImage:$helperImageVersion"; + $helperImageWithoutPrefix = 'coollabsio/coolify-helper'; + $helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion"; $commands = [ 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"', 'docker image prune -af --filter "label!=coolify.managed=true"', 'docker builder prune -af', "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f", + "docker images --filter before=$realtimeImageWithVersion --filter reference=$realtimeImage | grep $realtimeImage | awk '{print $3}' | xargs -r docker rmi -f", + "docker images --filter before=$helperImageWithoutPrefixVersion --filter reference=$helperImageWithoutPrefix | grep $helperImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f", + "docker images --filter before=$realtimeImageWithoutPrefixVersion --filter reference=$realtimeImageWithoutPrefix | grep $realtimeImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f", ]; if ($server->settings->delete_unused_volumes) { diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/Generate/OpenApi.php similarity index 89% rename from app/Console/Commands/OpenApi.php rename to app/Console/Commands/Generate/OpenApi.php index 3cef85477..2b266c258 100644 --- a/app/Console/Commands/OpenApi.php +++ b/app/Console/Commands/Generate/OpenApi.php @@ -1,6 +1,6 @@ json([ + 'message' => $result['message'], + ], 200); + } } else { if ($application->build_pack === 'dockercompose') { LoadComposeFile::dispatch($application); @@ -1004,12 +1009,17 @@ private function create_application(Request $request, $type) if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, no_questions_asked: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } else { if ($application->build_pack === 'dockercompose') { LoadComposeFile::dispatch($application); @@ -1101,12 +1111,17 @@ private function create_application(Request $request, $type) if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, no_questions_asked: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } else { if ($application->build_pack === 'dockercompose') { LoadComposeFile::dispatch($application); @@ -1190,12 +1205,17 @@ private function create_application(Request $request, $type) if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, no_questions_asked: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } return response()->json(serializeApiResponse([ @@ -1254,12 +1274,17 @@ private function create_application(Request $request, $type) if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, no_questions_asked: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } return response()->json(serializeApiResponse([ @@ -1610,6 +1635,18 @@ public function delete_by_uuid(Request $request) ['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( description: 'Application updated.', required: true, @@ -1884,11 +1921,16 @@ public function update_by_uuid(Request $request) if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } return response()->json([ @@ -2705,13 +2747,21 @@ public function action_deploy(Request $request) $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, force_rebuild: $force, is_api: true, no_questions_asked: $instant_deploy ); + if ($result['status'] === 'skipped') { + return response()->json( + [ + 'message' => $result['message'], + ], + 200 + ); + } return response()->json( [ @@ -2866,12 +2916,17 @@ public function action_restart(Request $request) $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, restart_only: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } return response()->json( [ @@ -3006,73 +3061,73 @@ public function action_restart(Request $request) // ]); // } - private function validateDataApplications(Request $request, Server $server) - { - $teamId = getTeamIdFromToken(); + private function validateDataApplications(Request $request, Server $server) + { + $teamId = getTeamIdFromToken(); - // Validate ports_mappings - if ($request->has('ports_mappings')) { - $ports = []; - foreach (explode(',', $request->ports_mappings) as $portMapping) { - $port = explode(':', $portMapping); - if (in_array($port[0], $ports)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'ports_mappings' => 'The first number before : should be unique between mappings.', - ], - ], 422); - } - $ports[] = $port[0]; - } - } - // Validate custom_labels - if ($request->has('custom_labels')) { - if (! isBase64Encoded($request->custom_labels)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - } - $customLabels = base64_decode($request->custom_labels); - if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - } - } - if ($request->has('domains') && $server->isProxyShouldRun()) { - $uuid = $request->uuid; - $fqdn = $request->domains; - $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); - $fqdn = str($fqdn)->replaceStart(',', '')->trim(); - $errors = []; - $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { - if (filter_var($domain, FILTER_VALIDATE_URL) === false) { - $errors[] = 'Invalid domain: '.$domain; - } + // Validate ports_mappings + if ($request->has('ports_mappings')) { + $ports = []; + foreach (explode(',', $request->ports_mappings) as $portMapping) { + $port = explode(':', $portMapping); + if (in_array($port[0], $ports)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_mappings' => 'The first number before : should be unique between mappings.', + ], + ], 422); + } + $ports[] = $port[0]; + } + } + // Validate custom_labels + if ($request->has('custom_labels')) { + if (! isBase64Encoded($request->custom_labels)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + $customLabels = base64_decode($request->custom_labels); + if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + } + if ($request->has('domains') && $server->isProxyShouldRun()) { + $uuid = $request->uuid; + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $errors = []; + $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + if (filter_var($domain, FILTER_VALIDATE_URL) === false) { + $errors[] = 'Invalid domain: '.$domain; + } - return str($domain)->trim()->lower(); - }); - if (count($errors) > 0) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => $errors, - ], 422); - } - if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'domains' => 'One of the domain is already used.', - ], - ], 422); - } - } - } + return str($domain)->trim()->lower(); + }); + if (count($errors) > 0) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'One of the domain is already used.', + ], + ], 422); + } + } + } } diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 424c2cc76..46606e24a 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -8,6 +8,7 @@ use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\Server; +use App\Models\Service; use App\Models\Tag; use Illuminate\Http\Request; use OpenApi\Attributes as OA; @@ -132,7 +133,7 @@ public function deployment_by_uuid(Request $request) #[OA\Get( summary: 'Deploy', - description: 'Deploy by tag or uuid. `Post` request also accepted.', + description: 'Deploy by tag or uuid. `Post` request also accepted with `uuid` and `tag` json body.', path: '/deploy', operationId: 'deploy-by-tag-or-uuid', security: [ @@ -191,10 +192,10 @@ public function deploy(Request $request) return invalidTokenResponse(); } - $uuids = $request->query->get('uuid'); - $tags = $request->query->get('tag'); - $force = $request->query->get('force') ?? false; - $pr = $request->query->get('pr') ? max((int) $request->query->get('pr'), 0) : 0; + $uuids = $request->input('uuid'); + $tags = $request->input('tag'); + $force = $request->input('force') ?? false; + $pr = $request->input('pr') ? max((int) $request->input('pr'), 0) : 0; if ($uuids && $tags) { return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400); @@ -297,17 +298,21 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; } switch ($resource?->getMorphClass()) { - case \App\Models\Application::class: + case Application::class: $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $resource, deployment_uuid: $deployment_uuid, force_rebuild: $force, pull_request_id: $pr, ); - $message = "Application {$resource->name} deployment queued."; + if ($result['status'] === 'skipped') { + $message = $result['message']; + } else { + $message = "Application {$resource->name} deployment queued."; + } break; - case \App\Models\Service::class: + case Service::class: StartService::run($resource); $message = "Service {$resource->name} started. It could take a while, be patient."; break; @@ -333,6 +338,40 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar ['bearerAuth' => []], ], tags: ['Deployments'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter( + name: 'skip', + in: 'query', + description: 'Number of records to skip.', + required: false, + schema: new OA\Schema( + type: 'integer', + minimum: 0, + default: 0, + ) + ), + new OA\Parameter( + name: 'take', + in: 'query', + description: 'Number of records to take.', + required: false, + schema: new OA\Schema( + type: 'integer', + minimum: 1, + default: 10, + ) + ), + ], responses: [ new OA\Response( response: 200, diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index b94ce9c67..98637c3e8 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -267,6 +267,18 @@ public function create_project(Request $request) ['bearerAuth' => []], ], tags: ['Projects'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the project.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], requestBody: new OA\RequestBody( required: true, description: 'Project updated.', diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 027bd5c1c..c3730d83f 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -527,6 +527,18 @@ public function delete_by_uuid(Request $request) ['bearerAuth' => []], ], tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], requestBody: new OA\RequestBody( description: 'Service updated.', required: true, diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 33d8f8532..490b66e58 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -100,18 +100,26 @@ public function manual(Request $request) if ($x_bitbucket_event === 'repo:push') { if ($application->isDeployable()) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, commit: $commit, force_rebuild: false, is_webhook: true ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, @@ -143,7 +151,7 @@ public function manual(Request $request) ]); } } - queue_application_deployment( + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -152,11 +160,19 @@ public function manual(Request $request) is_webhook: true, git_type: 'bitbucket' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 87fd2255f..3c3d6e0b6 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -116,19 +116,27 @@ public function manual(Request $request) $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, force_rebuild: false, commit: data_get($payload, 'after', 'HEAD'), is_webhook: true, ); - $return_payloads->push([ - 'status' => 'success', - 'message' => 'Deployment queued.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'status' => 'success', + 'message' => 'Deployment queued.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } } else { $paths = str($application->watch_paths)->explode("\n"); $return_payloads->push([ @@ -175,7 +183,7 @@ public function manual(Request $request) ]); } } - queue_application_deployment( + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -184,11 +192,19 @@ public function manual(Request $request) is_webhook: true, git_type: 'gitea' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 882f2be8b..597ec023f 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -122,19 +122,29 @@ public function manual(Request $request) $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, force_rebuild: false, commit: data_get($payload, 'after', 'HEAD'), is_webhook: true, ); - $return_payloads->push([ - 'status' => 'success', - 'message' => 'Deployment queued.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Deployment queued.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + 'deployment_uuid' => $result['deployment_uuid'], + ]); + } } else { $paths = str($application->watch_paths)->explode("\n"); $return_payloads->push([ @@ -181,7 +191,8 @@ public function manual(Request $request) ]); } } - queue_application_deployment( + + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -190,11 +201,19 @@ public function manual(Request $request) is_webhook: true, git_type: 'github' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, @@ -341,7 +360,7 @@ public function normal(Request $request) $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, commit: data_get($payload, 'after', 'HEAD'), @@ -349,10 +368,11 @@ public function normal(Request $request) is_webhook: true, ); $return_payloads->push([ - 'status' => 'success', - 'message' => 'Deployment queued.', + 'status' => $result['status'], + 'message' => $result['message'], 'application_uuid' => $application->uuid, 'application_name' => $application->name, + 'deployment_uuid' => $result['deployment_uuid'], ]); } else { $paths = str($application->watch_paths)->explode("\n"); @@ -389,7 +409,7 @@ public function normal(Request $request) 'pull_request_html_url' => $pull_request_html_url, ]); } - queue_application_deployment( + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -398,11 +418,19 @@ public function normal(Request $request) is_webhook: true, git_type: 'github' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index cf6874b8c..d6d12a05f 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -142,19 +142,28 @@ public function manual(Request $request) $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, commit: data_get($payload, 'after', 'HEAD'), force_rebuild: false, is_webhook: true, ); - $return_payloads->push([ - 'status' => 'success', - 'message' => 'Deployment queued.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'status' => $result['status'], + 'message' => $result['message'], + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } else { + $return_payloads->push([ + 'status' => 'success', + 'message' => 'Deployment queued.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } } else { $paths = str($application->watch_paths)->explode("\n"); $return_payloads->push([ @@ -201,7 +210,7 @@ public function manual(Request $request) ]); } } - queue_application_deployment( + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -210,11 +219,19 @@ public function manual(Request $request) is_webhook: true, git_type: 'gitlab' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview Deployment queued', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview Deployment queued', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 6cf642f27..ab80a19de 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -899,100 +899,12 @@ private function save_environment_variables() $sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id'); } $ports = $this->application->main_port(); - if ($this->pull_request_id !== 0) { - $this->env_filename = ".env-pr-$this->pull_request_id"; - // Add SOURCE_COMMIT if not exists - if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) { - if (! is_null($this->commit)) { - $envs->push("SOURCE_COMMIT={$this->commit}"); - } else { - $envs->push('SOURCE_COMMIT=unknown'); - } - } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) { - $envs->push("COOLIFY_FQDN={$this->preview->fqdn}"); - $envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}"); - } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) { - $url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', ''); - $envs->push("COOLIFY_URL={$url}"); - $envs->push("COOLIFY_DOMAIN_FQDN={$url}"); - } - if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); - } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { - $envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}"); - } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); - } - } - - add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview); - - foreach ($sorted_environment_variables_preview as $env) { - $real_value = $env->real_value; - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - } - $envs->push($env->key.'='.$real_value); - } - // Add PORT if not exists, use the first port as default - if ($this->build_pack !== 'dockercompose') { - if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { - $envs->push("PORT={$ports[0]}"); - } - } - // Add HOST if not exists - if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { - $envs->push('HOST=0.0.0.0'); - } - } else { + $coolify_envs = $this->generate_coolify_env_variables(); + $coolify_envs->each(function ($item, $key) use ($envs) { + $envs->push($key.'='.$item); + }); + if ($this->pull_request_id === 0) { $this->env_filename = '.env'; - // Add SOURCE_COMMIT if not exists - if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) { - if (! is_null($this->commit)) { - $envs->push("SOURCE_COMMIT={$this->commit}"); - } else { - $envs->push('SOURCE_COMMIT=unknown'); - } - } - if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { - if ((int) $this->application->compose_parsing_version >= 3) { - $envs->push("COOLIFY_URL={$this->application->fqdn}"); - } else { - $envs->push("COOLIFY_FQDN={$this->application->fqdn}"); - } - } - if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { - $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); - if ((int) $this->application->compose_parsing_version >= 3) { - $envs->push("COOLIFY_FQDN={$url}"); - } else { - $envs->push("COOLIFY_URL={$url}"); - } - } - if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { - if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); - } - if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { - $envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}"); - } - if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); - } - } - - add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables); foreach ($sorted_environment_variables as $env) { $real_value = $env->real_value; @@ -1017,6 +929,32 @@ private function save_environment_variables() if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { $envs->push('HOST=0.0.0.0'); } + } else { + $this->env_filename = ".env-pr-$this->pull_request_id"; + foreach ($sorted_environment_variables_preview as $env) { + $real_value = $env->real_value; + if ($env->version === '4.0.0-beta.239') { + $real_value = $env->real_value; + } else { + if ($env->is_literal || $env->is_multiline) { + $real_value = '\''.$real_value.'\''; + } else { + $real_value = escapeEnvVariables($env->real_value); + } + } + $envs->push($env->key.'='.$real_value); + } + // Add PORT if not exists, use the first port as default + if ($this->build_pack !== 'dockercompose') { + if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { + $envs->push("PORT={$ports[0]}"); + } + } + // Add HOST if not exists + if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { + $envs->push('HOST=0.0.0.0'); + } + } if ($envs->isEmpty()) { $this->env_filename = null; @@ -1393,6 +1331,9 @@ private function deploy_to_additional_destinations() } foreach ($destination_ids as $destination_id) { $destination = StandaloneDocker::find($destination_id); + if (! $destination) { + continue; + } $server = $destination->server; if ($server->team_id !== $this->mainServer->team_id) { $this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!"); @@ -1625,20 +1566,128 @@ private function generate_nixpacks_env_variables() $this->env_nixpacks_args = $this->env_nixpacks_args->implode(' '); } + private function generate_coolify_env_variables(): Collection + { + $coolify_envs = collect([]); + $local_branch = $this->branch; + if ($this->pull_request_id !== 0) { + // Add SOURCE_COMMIT if not exists + if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) { + if (! is_null($this->commit)) { + $coolify_envs->put('SOURCE_COMMIT', $this->commit); + } else { + $coolify_envs->put('SOURCE_COMMIT', 'unknown'); + } + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) { + $coolify_envs->put('COOLIFY_FQDN', $this->preview->fqdn); + $coolify_envs->put('COOLIFY_DOMAIN_URL', $this->preview->fqdn); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) { + $url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', ''); + $coolify_envs->put('COOLIFY_URL', $url); + $coolify_envs->put('COOLIFY_DOMAIN_FQDN', $url); + } + if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $coolify_envs->put('COOLIFY_BRANCH', $local_branch); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { + $coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name); + } + } + + add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables_preview); + + } else { + // Add SOURCE_COMMIT if not exists + if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) { + if (! is_null($this->commit)) { + $coolify_envs->put('SOURCE_COMMIT', $this->commit); + } else { + $coolify_envs->put('SOURCE_COMMIT', 'unknown'); + } + } + if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { + if ((int) $this->application->compose_parsing_version >= 3) { + $coolify_envs->put('COOLIFY_URL', $this->application->fqdn); + } else { + $coolify_envs->put('COOLIFY_FQDN', $this->application->fqdn); + } + } + if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { + $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); + if ((int) $this->application->compose_parsing_version >= 3) { + $coolify_envs->put('COOLIFY_FQDN', $url); + } else { + $coolify_envs->put('COOLIFY_URL', $url); + } + } + if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { + if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $coolify_envs->put('COOLIFY_BRANCH', $local_branch); + } + if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { + $coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid); + } + if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name); + } + } + + add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables); + + } + + return $coolify_envs; + } + private function generate_env_variables() { $this->env_args = collect([]); $this->env_args->put('SOURCE_COMMIT', $this->commit); + $coolify_envs = $this->generate_coolify_env_variables(); if ($this->pull_request_id === 0) { foreach ($this->application->build_environment_variables as $env) { if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); + if (str($env->real_value)->startsWith('$')) { + $variable_key = str($env->real_value)->after('$'); + if ($variable_key->startsWith('COOLIFY_')) { + $variable = $coolify_envs->get($variable_key->value()); + if (filled($variable)) { + $this->env_args->prepend($variable, $variable_key->value()); + } + } else { + $variable = $this->application->environment_variables()->where('key', $variable_key)->first(); + if ($variable) { + $this->env_args->prepend($variable->real_value, $env->key); + } + } + } } } } else { foreach ($this->application->build_environment_variables_preview as $env) { if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); + if (str($env->real_value)->startsWith('$')) { + $variable_key = str($env->real_value)->after('$'); + if ($variable_key->startsWith('COOLIFY_')) { + $variable = $coolify_envs->get($variable_key->value()); + if (filled($variable)) { + $this->env_args->prepend($variable, $variable_key->value()); + } + } else { + $variable = $this->application->environment_variables_preview()->where('key', $variable_key)->first(); + if ($variable) { + $this->env_args->prepend($variable->real_value, $env->key); + } + } + } } } } @@ -2413,20 +2462,23 @@ private function run_post_deployment_command() private function next(string $status) { queue_next_deployment($this->application); - // If the deployment is cancelled by the user, don't update the status - if ( - $this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && - $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value - ) { - $this->application_deployment_queue->update([ - 'status' => $status, - ]); + + // Never allow changing status from FAILED or CANCELLED_BY_USER to anything else + if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value || + $this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + return; } - if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) { + + $this->application_deployment_queue->update([ + 'status' => $status, + ]); + + if ($status === ApplicationDeploymentStatus::FAILED->value) { $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); return; } + if ($status === ApplicationDeploymentStatus::FINISHED->value) { if (! $this->only_this_server) { $this->deploy_to_additional_destinations(); diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 3ed20f907..c5f518e16 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -269,7 +269,7 @@ public function submitSmtp() } catch (\Throwable $e) { $this->smtpEnabled = false; - return handleError($e); + return handleError($e, $this); } } @@ -337,32 +337,29 @@ function () { public function copyFromInstanceSettings() { $settings = instanceSettings(); + $this->smtpFromAddress = $settings->smtp_from_address; + $this->smtpFromName = $settings->smtp_from_name; if ($settings->smtp_enabled) { $this->smtpEnabled = true; - $this->smtpFromAddress = $settings->smtp_from_address; - $this->smtpFromName = $settings->smtp_from_name; - $this->smtpRecipients = $settings->smtp_recipients; - $this->smtpHost = $settings->smtp_host; - $this->smtpPort = $settings->smtp_port; - $this->smtpEncryption = $settings->smtp_encryption; - $this->smtpUsername = $settings->smtp_username; - $this->smtpPassword = $settings->smtp_password; - $this->smtpTimeout = $settings->smtp_timeout; $this->resendEnabled = false; - $this->saveModel(); - - return; } + + $this->smtpRecipients = $settings->smtp_recipients; + $this->smtpHost = $settings->smtp_host; + $this->smtpPort = $settings->smtp_port; + $this->smtpEncryption = $settings->smtp_encryption; + $this->smtpUsername = $settings->smtp_username; + $this->smtpPassword = $settings->smtp_password; + $this->smtpTimeout = $settings->smtp_timeout; + if ($settings->resend_enabled) { $this->resendEnabled = true; - $this->resendApiKey = $settings->resend_api_key; $this->smtpEnabled = false; - $this->saveModel(); - - return; } - $this->dispatch('error', 'Instance SMTP/Resend settings are not enabled.'); + $this->resendApiKey = $settings->resend_api_key; + $this->saveModel(); + } public function render() diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 0d7d7755f..475d2dfa8 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -84,11 +84,16 @@ public function deploy(bool $force_rebuild = false) return; } $this->setDeploymentUuid(); - queue_application_deployment( + $result = queue_application_deployment( application: $this->application, deployment_uuid: $this->deploymentUuid, force_rebuild: $force_rebuild, ); + if ($result['status'] === 'skipped') { + $this->dispatch('success', 'Deployment skipped', $result['message']); + + return; + } return $this->redirectRoute('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], @@ -126,11 +131,16 @@ public function restart() return; } $this->setDeploymentUuid(); - queue_application_deployment( + $result = queue_application_deployment( application: $this->application, deployment_uuid: $this->deploymentUuid, restart_only: true, ); + if ($result['status'] === 'skipped') { + $this->dispatch('success', 'Deployment skipped', $result['message']); + + return; + } return $this->redirectRoute('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index bdf62706c..88ce65c53 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -159,13 +159,18 @@ public function deploy(int $pull_request_id, ?string $pull_request_html_url = nu 'pull_request_html_url' => $pull_request_html_url, ]); } - queue_application_deployment( + $result = queue_application_deployment( application: $this->application, deployment_uuid: $this->deployment_uuid, force_rebuild: false, pull_request_id: $pull_request_id, git_type: $found->git_type ?? null, ); + if ($result['status'] === 'skipped') { + $this->dispatch('success', 'Deployment skipped', $result['message']); + + return; + } return redirect()->route('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php index ade297d50..013d8b8fe 100644 --- a/app/Livewire/Project/Application/Source.php +++ b/app/Livewire/Project/Application/Source.php @@ -30,11 +30,15 @@ class Source extends Component #[Validate(['nullable', 'string'])] public ?string $gitCommitSha = null; + #[Locked] + public $sources; + public function mount() { try { $this->syncData(); $this->getPrivateKeys(); + $this->getSources(); } catch (\Throwable $e) { handleError($e, $this); } @@ -66,6 +70,14 @@ private function getPrivateKeys() }); } + private function getSources() + { + // filter the current source out + $this->sources = currentTeam()->sources()->whereNotNull('app_id')->reject(function ($source) { + return $source->id === $this->application->source_id; + })->sortBy('name'); + } + public function setPrivateKey(int $privateKeyId) { try { @@ -92,4 +104,19 @@ public function submit() return handleError($e, $this); } } + + public function changeSource($sourceId, $sourceType) + { + try { + $this->application->update([ + 'source_id' => $sourceId, + 'source_type' => $sourceType, + ]); + $this->application->refresh(); + $this->getSources(); + $this->dispatch('success', 'Source updated!'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } } diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 1759fe08a..71a913add 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -79,7 +79,7 @@ public function redeploy(int $network_id, int $server_id) $deployment_uuid = new Cuid2; $server = Server::ownedByCurrentTeam()->findOrFail($server_id); $destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail(); - queue_application_deployment( + $result = queue_application_deployment( deployment_uuid: $deployment_uuid, application: $this->resource, server: $server, @@ -87,6 +87,11 @@ public function redeploy(int $network_id, int $server_id) only_this_server: true, no_questions_asked: true, ); + if ($result['status'] === 'skipped') { + $this->dispatch('success', 'Deployment skipped', $result['message']); + + return; + } return redirect()->route('project.application.deployment.show', [ 'project_uuid' => data_get($this->resource, 'environment.project.uuid'), diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php index 6277a24bd..7db890638 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurations.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php @@ -38,7 +38,8 @@ public function loadDynamicConfigurations() $contents = collect([]); foreach ($files as $file) { $without_extension = str_replace('.', '|', $file); - $contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server); + $content = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server); + $contents[$without_extension] = $content ?? ''; } $this->contents = $contents; $this->dispatch('$refresh'); diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index b2394d7b0..73e8c7398 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -177,7 +177,7 @@ public function submitSmtp() } catch (\Throwable $e) { $this->smtpEnabled = false; - return handleError($e); + return handleError($e, $this); } } @@ -207,7 +207,7 @@ public function submitResend() } catch (\Throwable $e) { $this->resendEnabled = false; - return handleError($e); + return handleError($e, $this); } } diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php index df450cf7e..8a9cc456f 100644 --- a/app/Livewire/Subscription/Index.php +++ b/app/Livewire/Subscription/Index.php @@ -12,19 +12,30 @@ class Index extends Component public bool $alreadySubscribed = false; + public bool $isUnpaid = false; + + public bool $isCancelled = false; + + public bool $isMember = false; + + public bool $loading = true; + public function mount() { if (! isCloud()) { return redirect(RouteServiceProvider::HOME); } if (auth()->user()?->isMember()) { - return redirect()->route('dashboard'); + $this->isMember = true; } if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) { return redirect()->route('subscription.show'); } $this->settings = instanceSettings(); $this->alreadySubscribed = currentTeam()->subscription()->exists(); + if (! $this->alreadySubscribed) { + $this->loading = false; + } } public function stripeCustomerPortal() @@ -37,6 +48,41 @@ public function stripeCustomerPortal() return redirect($session->url); } + public function getStripeStatus() + { + try { + $subscription = currentTeam()->subscription; + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $customer = $stripe->customers->retrieve(currentTeam()->subscription->stripe_customer_id); + if ($customer) { + $subscriptions = $stripe->subscriptions->all(['customer' => $customer->id]); + $currentTeam = currentTeam()->id ?? null; + if (count($subscriptions->data) > 0 && $currentTeam) { + $foundSubscription = collect($subscriptions->data)->firstWhere('metadata.team_id', $currentTeam); + if ($foundSubscription) { + $status = data_get($foundSubscription, 'status'); + $subscription->update([ + 'stripe_subscription_id' => $foundSubscription->id, + ]); + if ($status === 'unpaid') { + $this->isUnpaid = true; + } + } + } + if (count($subscriptions->data) === 0) { + $this->isCancelled = true; + } + } + } catch (\Exception $e) { + // Log the error + logger()->error('Stripe API error: ' . $e->getMessage()); + // Set a flag to show an error message to the user + $this->addError('stripe', 'Could not retrieve subscription information. Please try again later.'); + } finally { + $this->loading = false; + } + } + public function render() { return view('livewire.subscription.index'); diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index fd8f1cba2..2a9bea67a 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; use OpenApi\Attributes as OA; #[OA\Schema( @@ -101,17 +102,23 @@ public function addLogEntry(string $message, string $type = 'stdout', bool $hidd 'hidden' => $hidden, 'batch' => 1, ]; - if ($this->logs) { - $previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR); - $newLogEntry['order'] = count($previousLogs) + 1; - $previousLogs[] = $newLogEntry; - $this->update([ - 'logs' => json_encode($previousLogs, flags: JSON_THROW_ON_ERROR), - ]); - } else { - $this->update([ - 'logs' => json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR), - ]); - } + + // Use a transaction to ensure atomicity + DB::transaction(function () use ($newLogEntry) { + // Reload the model to get the latest logs + $this->refresh(); + + if ($this->logs) { + $previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR); + $newLogEntry['order'] = count($previousLogs) + 1; + $previousLogs[] = $newLogEntry; + $this->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR); + } else { + $this->logs = json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR); + } + + // Save without triggering events to prevent potential race conditions + $this->saveQuietly(); + }); } } diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index 0e702e460..97c32fa31 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -17,6 +17,8 @@ 'name' => ['type' => 'string'], 'description' => ['type' => 'string'], 'private_key' => ['type' => 'string', 'format' => 'private-key'], + 'public_key' => ['type' => 'string'], + 'fingerprint' => ['type' => 'string'], 'is_git_related' => ['type' => 'boolean'], 'team_id' => ['type' => 'integer'], 'created_at' => ['type' => 'string'], diff --git a/app/Models/Server.php b/app/Models/Server.php index 60d0da3ed..41af45f30 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -20,7 +20,6 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Stringable; use OpenApi\Attributes as OA; @@ -1026,22 +1025,11 @@ public function isReachableChanged() $this->refresh(); $unreachableNotificationSent = (bool) $this->unreachable_notification_sent; $isReachable = (bool) $this->settings->is_reachable; - - Log::debug('Server reachability check', [ - 'server_id' => $this->id, - 'is_reachable' => $isReachable, - 'notification_sent' => $unreachableNotificationSent, - 'unreachable_count' => $this->unreachable_count, - ]); - if ($isReachable === true) { $this->unreachable_count = 0; $this->save(); if ($unreachableNotificationSent === true) { - Log::debug('Server is now reachable, sending notification', [ - 'server_id' => $this->id, - ]); $this->sendReachableNotification(); } @@ -1049,17 +1037,10 @@ public function isReachableChanged() } $this->increment('unreachable_count'); - Log::debug('Incremented unreachable count', [ - 'server_id' => $this->id, - 'new_count' => $this->unreachable_count, - ]); if ($this->unreachable_count === 1) { $this->settings->is_reachable = true; $this->settings->save(); - Log::debug('First unreachable attempt, marking as reachable', [ - 'server_id' => $this->id, - ]); return; } @@ -1068,11 +1049,6 @@ public function isReachableChanged() $failedChecks = 0; for ($i = 0; $i < 3; $i++) { $status = $this->serverStatus(); - Log::debug('Additional reachability check', [ - 'server_id' => $this->id, - 'attempt' => $i + 1, - 'status' => $status, - ]); sleep(5); if (! $status) { $failedChecks++; @@ -1080,9 +1056,6 @@ public function isReachableChanged() } if ($failedChecks === 3 && ! $unreachableNotificationSent) { - Log::debug('Server confirmed unreachable after 3 attempts, sending notification', [ - 'server_id' => $this->id, - ]); $this->sendUnreachableNotification(); } } diff --git a/app/Models/Team.php b/app/Models/Team.php index d36f8c1ab..42b88f9e7 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -192,8 +192,6 @@ public function isAnyNotificationEnabled() public function subscriptionEnded() { $this->subscription->update([ - 'stripe_subscription_id' => null, - 'stripe_plan_id' => null, 'stripe_cancel_at_period_end' => false, 'stripe_invoice_paid' => false, 'stripe_trial_already_ended' => false, diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index d5283898e..3f1e8513c 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -24,6 +24,26 @@ function queue_application_deployment(Application $application, string $deployme if ($destination) { $destination_id = $destination->id; } + + // Check if there's already a deployment in progress or queued for this application and commit + $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id) + ->where('commit', $commit) + ->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value]) + ->first(); + + if ($existing_deployment) { + // If force_rebuild is true or rollback is true or no_questions_asked is true, we'll still create a new deployment + if (! $force_rebuild && ! $rollback && ! $no_questions_asked) { + // Return the existing deployment's details + return [ + 'status' => 'skipped', + 'message' => 'Deployment already queued for this commit.', + 'deployment_uuid' => $existing_deployment->deployment_uuid, + 'existing_deployment' => $existing_deployment, + ]; + } + } + $deployment = ApplicationDeploymentQueue::create([ 'application_id' => $application_id, 'application_name' => $application->name, @@ -47,11 +67,17 @@ function queue_application_deployment(Application $application, string $deployme ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, ); - } elseif (next_queuable($server_id, $application_id)) { + } elseif (next_queuable($server_id, $application_id, $commit)) { ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, ); } + + return [ + 'status' => 'queued', + 'message' => 'Deployment queued.', + 'deployment_uuid' => $deployment_uuid, + ]; } function force_start_deployment(ApplicationDeploymentQueue $deployment) { @@ -78,20 +104,35 @@ function queue_next_deployment(Application $application) } } -function next_queuable(string $server_id, string $application_id): bool +function next_queuable(string $server_id, string $application_id, string $commit = 'HEAD'): bool { - $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); - $same_application_deployments = $deployments->where('application_id', $application_id); - $in_progress = $same_application_deployments->filter(function ($value, $key) { - return $value->status === 'in_progress'; - }); - if ($in_progress->count() > 0) { + // Check if there's already a deployment in progress for this application and commit + $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id) + ->where('commit', $commit) + ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value) + ->first(); + + if ($existing_deployment) { return false; } + + // Check if there's any deployment in progress for this application + $in_progress = ApplicationDeploymentQueue::where('application_id', $application_id) + ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value) + ->exists(); + + if ($in_progress) { + return false; + } + + // Check server's concurrent build limit $server = Server::find($server_id); $concurrent_builds = $server->settings->concurrent_builds; + $active_deployments = ApplicationDeploymentQueue::where('server_id', $server_id) + ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value) + ->count(); - if ($deployments->count() > $concurrent_builds) { + if ($active_deployments >= $concurrent_builds) { return false; } diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 81f8ff18a..0de2f2fd9 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -52,6 +52,9 @@ function generateGithubToken(GithubApp $source, string $type) if (! $response->successful()) { $error = data_get($response->json(), 'message', 'no error message found'); + if ($error === 'Not Found') { + $error = 'Repository not found. Is it moved or deleted?'; + } throw new RuntimeException("Failed to get installation token for {$source->name} with error: ".$error); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index b90de4dbc..117e5c9dc 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2987,7 +2987,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $predefinedPort = '8000'; } if ($isDatabase) { - $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); if ($applicationFound) { $savedService = $applicationFound; $savedService = ServiceDatabase::firstOrCreate([ @@ -2999,17 +2999,22 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } else { $savedService = ServiceDatabase::firstOrCreate([ 'name' => $serviceName, - 'image' => $image, 'service_id' => $resource->id, ]); } } else { $savedService = ServiceApplication::firstOrCreate([ 'name' => $serviceName, - 'image' => $image, 'service_id' => $resource->id, ]); } + + // Check if image changed + if ($savedService->image !== $image) { + $savedService->image = $image; + $savedService->save(); + } + $environment = collect(data_get($service, 'environment', [])); $buildArgs = collect(data_get($service, 'build.args', [])); $environment = $environment->merge($buildArgs); diff --git a/config/constants.php b/config/constants.php index 439f32940..519ed55bb 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.407', + 'version' => '4.0.0-beta.408', 'helper_version' => '1.0.8', 'realtime_version' => '1.0.6', 'self_hosted' => env('SELF_HOSTED', true), @@ -10,6 +10,7 @@ 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), 'registry_url' => env('REGISTRY_URL', 'ghcr.io'), 'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'), + 'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'), 'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false), ], diff --git a/database/migrations/2025_04_01_124212_stripe_comment_nullable.php b/database/migrations/2025_04_01_124212_stripe_comment_nullable.php new file mode 100644 index 000000000..7f61c202e --- /dev/null +++ b/database/migrations/2025_04_01_124212_stripe_comment_nullable.php @@ -0,0 +1,28 @@ +longText('stripe_comment')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('subscriptions', function (Blueprint $table) { + $table->longText('stripe_comment')->nullable(false)->change(); + }); + } +}; diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json index 0a9b80cb5..7851d7f4d 100644 --- a/docker/coolify-realtime/package.json +++ b/docker/coolify-realtime/package.json @@ -5,9 +5,9 @@ "@xterm/addon-fit": "0.10.0", "@xterm/xterm": "5.5.0", "cookie": "1.0.2", - "axios": "1.7.9", - "dotenv": "16.4.7", + "axios": "1.8.4", + "dotenv": "16.5.0", "node-pty": "1.0.0", "ws": "8.18.1" } -} +} \ No newline at end of file diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 38bb50f3f..8d74ba107 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -89,9 +89,9 @@ RUN echo "alias ll='ls -al'" >> /etc/profile && \ # Install Cloudflared based on architecture RUN mkdir -p /usr/local/bin && \ if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ - curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" -o /usr/local/bin/cloudflared; \ + curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" -o /usr/local/bin/cloudflared; \ elif [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ - curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64" -o /usr/local/bin/cloudflared; \ + curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64" -o /usr/local/bin/cloudflared; \ fi && \ chmod +x /usr/local/bin/cloudflared diff --git a/hooks/pre-commit b/hooks/pre-commit index 69a5a9d41..029f67917 100644 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -1,7 +1,7 @@ #!/bin/sh # Detect whether /dev/tty is available & functional if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then - exec < /dev/tty + exec Dashboard
Your self-hosted infrastructure.
@if (request()->query->get('success')) -
- - - - Your subscription has been activated! Welcome onboard!
It could take a few seconds before your +
+ Your subscription has been activated! Welcome onboard! It could take a few seconds before your subscription is activated.
Please be patient.
@endif diff --git a/resources/views/livewire/navbar-delete-team.blade.php b/resources/views/livewire/navbar-delete-team.blade.php index 60b25a3d5..d0c47b874 100644 --- a/resources/views/livewire/navbar-delete-team.blade.php +++ b/resources/views/livewire/navbar-delete-team.blade.php @@ -1,13 +1,6 @@ -
- + + shortConfirmationLabel="Team Name" step3ButtonText="Permanently Delete" />
diff --git a/resources/views/livewire/project/application/source.blade.php b/resources/views/livewire/project/application/source.blade.php index b542d5428..29c5c6142 100644 --- a/resources/views/livewire/project/application/source.blade.php +++ b/resources/views/livewire/project/application/source.blade.php @@ -26,6 +26,9 @@
Code source of your application.
+
Currently connected source: {{ $application->source->name }} +
@@ -34,6 +37,39 @@
+ +
+

Change Git Source

+
+ @forelse ($sources as $source) +
+ + +
+
+ {{ $source->name }} + @if ($application->source_id === $source->id) + (current) + @endif +
+
+ {{ $source->organization ?? 'Personal Account' }} +
+
+
+
+
+ @empty +
No sources found
+ @endforelse +
+
+ @if ($privateKeyId)

Deploy Key

Currently attached Private Key: @else diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php index 1b5493c58..a47452437 100644 --- a/resources/views/livewire/source/github/change.blade.php +++ b/resources/views/livewire/source/github/change.blade.php @@ -277,12 +277,15 @@ function createGithubApp(webhook_endpoint, preview_deployment_permissions, admin emails: 'read', administration: 'read' }; + const default_events = ['push']; if (preview_deployment_permissions) { default_permissions.pull_requests = 'write'; + default_events.push('pull_request'); } if (administration) { default_permissions.administration = 'write'; } + const data = { name, url: baseUrl, @@ -297,7 +300,7 @@ function createGithubApp(webhook_endpoint, preview_deployment_permissions, admin setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`, setup_on_update: true, default_permissions, - default_events: ['pull_request', 'push'] + default_events }; const form = document.createElement('form'); form.setAttribute('method', 'post'); diff --git a/resources/views/livewire/subscription/index.blade.php b/resources/views/livewire/subscription/index.blade.php index c29fdae89..974b209c7 100644 --- a/resources/views/livewire/subscription/index.blade.php +++ b/resources/views/livewire/subscription/index.blade.php @@ -3,37 +3,62 @@ Subscribe | Coolify @if (auth()->user()->isAdminFromSession()) -
-
-

Subscriptions

- @if (subscriptionProvider() === 'stripe' && $alreadySubscribed) - Manage My Subscription - @endif + @if (request()->query->get('cancelled')) +
+ + + + Something went wrong with your subscription. Please try again or contact + support.
- @if (request()->query->get('cancelled')) -
- - - - Something went wrong with your subscription. Please try again or contact - support. -
- @endif - - @if (config('subscription.provider') === 'stripe') - - @endif + @endif +
+

Subscriptions

+ @if ($loading) +
+ Loading your subscription status... +
+ @else + @if ($isUnpaid) +
+ Your last payment was failed for Coolify Cloud. +
+
+

Open the following link, navigate to the button and pay your unpaid/past due + subscription. +

+ Billing Portal +
+ @else + @if (config('subscription.provider') === 'stripe') +
$isCancelled, + 'pb-10' => !$isCancelled, + ])> + @if ($isCancelled) +
+ It looks like your previous subscription has been cancelled, because you forgot to + pay + the bills.
Please subscribe again to continue using Coolify.
+
+ @endif +
+ + @endif + @endif + @endif @else

Subscription

-
You are not an admin or have been removed from this team. If this does not make sense, please contact - us.
+
You are not an admin so you cannot manage your Team's subscription. If this does not make sense, please + contact + us. +
@endif
diff --git a/resources/views/livewire/subscription/pricing-plans.blade.php b/resources/views/livewire/subscription/pricing-plans.blade.php index 2cbfbb1ca..19b795102 100644 --- a/resources/views/livewire/subscription/pricing-plans.blade.php +++ b/resources/views/livewire/subscription/pricing-plans.blade.php @@ -1,4 +1,4 @@ -
+
+ class="grid grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-1 lg:divide-x lg:divide-y-0 xl:-mx-4">

Pay-as-you-go

@@ -72,14 +72,16 @@ class="font-normal dark:text-white">billed annually (+VAT)

- - Subscribe - - - Subscribe - +
+ + Subscribe + + + Subscribe + +