diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2d354057e..4f12f436c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,5 +4,5 @@ contact_links: url: https://coollabs.io/discord about: Reach out to us on Discord. - name: 🙋♂️ Feature Requests - url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas + url: https://github.com/coollabsio/coolify/discussions/categories/new-features about: All feature requests will be discussed here. diff --git a/app/Console/Commands/CleanupDatabase.php b/app/Console/Commands/CleanupDatabase.php index 1e177ca62..07bc4d338 100644 --- a/app/Console/Commands/CleanupDatabase.php +++ b/app/Console/Commands/CleanupDatabase.php @@ -18,7 +18,11 @@ class CleanupDatabase extends Command } else { echo "Running database cleanup in dry-run mode...\n"; } - $keep_days = 60; + if (isCloud()) { + $keep_days = 60; + } else { + $keep_days = 60; + } echo "Keep days: $keep_days\n"; // Cleanup failed jobs table $failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(1)); diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 54ee8ef11..cd4d724b4 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -739,7 +739,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -835,7 +835,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -927,7 +927,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -947,7 +947,7 @@ class ApplicationsController extends Controller ])); } elseif ($type === 'dockerfile') { if (! $request->has('name')) { - $request->offsetSet('name', 'dockerfile-'.new Cuid2(7)); + $request->offsetSet('name', 'dockerfile-'.new Cuid2); } $validator = customApiValidator($request->all(), [ sharedDataApplications(), @@ -1009,7 +1009,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -1025,7 +1025,7 @@ class ApplicationsController extends Controller ])); } elseif ($type === 'dockerimage') { if (! $request->has('name')) { - $request->offsetSet('name', 'docker-image-'.new Cuid2(7)); + $request->offsetSet('name', 'docker-image-'.new Cuid2); } $validator = customApiValidator($request->all(), [ sharedDataApplications(), @@ -1067,7 +1067,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -1099,7 +1099,7 @@ class ApplicationsController extends Controller ], 422); } if (! $request->has('name')) { - $request->offsetSet('name', 'service'.new Cuid2(7)); + $request->offsetSet('name', 'service'.new Cuid2); } $validator = customApiValidator($request->all(), [ sharedDataApplications(), @@ -1320,7 +1320,7 @@ class ApplicationsController extends Controller #[OA\Patch( summary: 'Update', description: 'Update application by UUID.', - path: '/applications', + path: '/applications/{uuid}', security: [ ['bearerAuth' => []], ], @@ -2322,7 +2322,7 @@ class ApplicationsController extends Controller return response()->json(['message' => 'Application not found.'], 404); } - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -2479,7 +2479,7 @@ class ApplicationsController extends Controller return response()->json(['message' => 'Application not found.'], 404); } - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 2ee56d0cd..437162058 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -84,7 +84,7 @@ class DeployController extends Controller ], tags: ['Deployments'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -290,7 +290,7 @@ class DeployController extends Controller } switch ($resource?->getMorphClass()) { case 'App\Models\Application': - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $resource, deployment_uuid: $deployment_uuid, diff --git a/app/Http/Controllers/Api/OpenApi.php b/app/Http/Controllers/Api/OpenApi.php index 59731ef40..60337a76c 100644 --- a/app/Http/Controllers/Api/OpenApi.php +++ b/app/Http/Controllers/Api/OpenApi.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers\Api; use OpenApi\Attributes as OA; #[OA\Info(title: 'Coolify', version: '0.1')] -#[OA\Server(url: 'https://app.coolify.io/api/v1')] +#[OA\Server(url: 'https://app.coolify.io/api/v1', description: 'Coolify Cloud API. Change the host to your own instance if you are self-hosting.')] #[OA\SecurityScheme( type: 'http', scheme: 'bearer', diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 8316d5e82..6aec31e9b 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -61,7 +61,7 @@ class ProjectController extends Controller ], tags: ['Projects'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -107,7 +107,7 @@ class ProjectController extends Controller ], tags: ['Projects'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')), ], responses: [ diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index 11e8e27ca..67128234e 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -73,7 +73,7 @@ class SecurityController extends Controller ], tags: ['Private Keys'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -318,7 +318,7 @@ class SecurityController extends Controller ], tags: ['Private Keys'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index fa159077d..5d4b56988 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -105,7 +105,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -182,7 +182,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -259,7 +259,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -732,7 +732,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 059438ff4..ef85d59e3 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -103,7 +103,7 @@ class Bitbucket extends Controller if ($x_bitbucket_event === 'repo:push') { if ($application->isDeployable()) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -127,7 +127,7 @@ class Bitbucket extends Controller if ($x_bitbucket_event === 'pullrequest:created') { if ($application->isPRDeployable()) { ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index e6d91efd6..e042b74c9 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -123,7 +123,7 @@ class Gitea extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -162,7 +162,7 @@ class Gitea extends Controller if ($x_gitea_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index ee51b6e0d..5f3ba933b 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -128,7 +128,7 @@ class Github extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -167,7 +167,7 @@ class Github extends Controller if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { @@ -357,7 +357,7 @@ class Github extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -396,7 +396,7 @@ class Github extends Controller if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { ApplicationPreview::create([ diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index f6e6cf7e7..ec7f51a0d 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -137,7 +137,7 @@ class Gitlab extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -177,7 +177,7 @@ class Gitlab extends Controller if ($x_gitlab_event === 'merge_request') { if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 8a79515b5..d7b1a57da 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -307,14 +307,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ] ); - // $this->execute_remote_command( - // [ - // "docker image prune -f >/dev/null 2>&1", - // "hidden" => true, - // "ignore_errors" => true, - // ] - // ); - ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } } @@ -497,13 +489,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $this->write_deployment_configurations(); $server_workdir = $this->application->workdir(); + $this->docker_compose_location = '/docker-compose.yaml'; $command = "{$this->coolify_variables} docker compose"; if ($this->env_filename) { - $command .= " --env-file {$this->workdir}/{$this->env_filename}"; + $command .= " --env-file {$server_workdir}/{$this->env_filename}"; } $command .= " --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; - $this->execute_remote_command( ['command' => $command, 'hidden' => true], ); @@ -636,21 +628,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->server = $this->original_server; } $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); + + $mainDir = $this->configuration_dir; + if ($this->application->settings->is_raw_compose_deployment_enabled) { + $mainDir = $this->application->workdir(); + } if ($this->pull_request_id === 0) { - $composeFileName = "$this->configuration_dir/docker-compose.yaml"; + $composeFileName = "$mainDir/docker-compose.yaml"; } else { - $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yaml"; + $composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml"; $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml"; } $this->execute_remote_command( [ - "mkdir -p $this->configuration_dir", + "mkdir -p $mainDir", ], [ "echo '{$this->docker_compose_base64}' | base64 -d | tee $composeFileName > /dev/null", ], [ - "echo '{$readme}' > $this->configuration_dir/README.md", + "echo '{$readme}' > $mainDir/README.md", ] ); if ($this->use_build_server) { diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 4afe50d53..79b00e9cd 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -90,6 +90,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { try { BackupCreated::dispatch($this->team->id); + // Check if team is exists if (is_null($this->team)) { $this->backup->update(['status' => 'failed']); @@ -476,7 +477,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } else { $network = $this->database->destination->network; } - $commands[] = "docker run --pull=always -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper"; + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; instant_remote_process($commands, $this->server); diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php new file mode 100644 index 000000000..adade3194 --- /dev/null +++ b/app/Jobs/ServerCheckJob.php @@ -0,0 +1,486 @@ +server->uuid))]; + } + + public function uniqueId(): int + { + return $this->server->uuid; + } + + public function handle() + { + + try { + $up = $this->serverStatus(); + if (! $up) { + ray('Server is not reachable.'); + + return 'Server is not reachable.'; + } + if (! $this->server->isFunctional()) { + ray('Server is not ready.'); + + return 'Server is not ready.'; + } + $this->checkSentinel(); + $this->getContainers(); + + if (is_null($this->containers)) { + return 'No containers found.'; + } + $this->checkLogDrainContainer(); + $this->containerStatus(); + + } catch (\Throwable $e) { + ray($e->getMessage()); + + return handleError($e); + } + + } + + private function checkSentinel() + { + if ($this->server->isSentinelEnabled()) { + $sentinelContainerFound = $this->containers->filter(function ($value, $key) { + return data_get($value, 'Name') === '/coolify-sentinel'; + })->first(); + if ($sentinelContainerFound) { + $status = data_get($sentinelContainerFound, 'State.Status'); + if ($status !== 'running') { + PullSentinelImageJob::dispatch($this); + } + } + } + } + + private function serverStatus() + { + $this->removeUnnevessaryCoolifyYaml(); + ['uptime' => $uptime] = $this->server->validateConnection(); + if ($uptime) { + if ($this->server->unreachable_notification_sent === true) { + $this->server->update(['unreachable_notification_sent' => false]); + } + } else { + foreach ($this->applications as $application) { + $application->update(['status' => 'exited']); + } + foreach ($this->databases as $database) { + $database->update(['status' => 'exited']); + } + foreach ($this->services as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + $app->update(['status' => 'exited']); + } + foreach ($dbs as $db) { + $db->update(['status' => 'exited']); + } + } + + return false; + } + + return true; + + } + + private function removeUnnevessaryCoolifyYaml() + { + // This will remote the coolify.yaml file from the server as it is not needed on cloud servers + if (isCloud() && $this->server->id !== 0) { + $file = $this->server->proxyPath().'/dynamic/coolify.yaml'; + + return instant_remote_process([ + "rm -f $file", + ], $this->server, false); + } + } + + private function checkLogDrainContainer() + { + $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { + return data_get($value, 'Name') === '/coolify-log-drain'; + })->first(); + if ($foundLogDrainContainer) { + $status = data_get($foundLogDrainContainer, 'State.Status'); + if ($status !== 'running') { + InstallLogDrain::dispatch($this->server); + } + } else { + InstallLogDrain::dispatch($this->server); + } + } + + private function getContainers() + { + if ($this->server->isSwarm()) { + $this->containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); + $this->containers = format_docker_command_output_to_json($this->containers); + $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false); + if ($containerReplicates) { + $containerReplicates = format_docker_command_output_to_json($containerReplicates); + foreach ($containerReplicates as $containerReplica) { + $name = data_get($containerReplica, 'Name'); + $this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) { + if (data_get($container, 'Spec.Name') === $name) { + $replicas = data_get($containerReplica, 'Replicas'); + $running = str($replicas)->explode('/')[0]; + $total = str($replicas)->explode('/')[1]; + if ($running === $total) { + data_set($container, 'State.Status', 'running'); + data_set($container, 'State.Health.Status', 'healthy'); + } else { + data_set($container, 'State.Status', 'starting'); + data_set($container, 'State.Health.Status', 'unhealthy'); + } + } + + return $container; + }); + } + } + } else { + $this->containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); + $this->containers = format_docker_command_output_to_json($this->containers); + } + + } + + private function containerStatus() + { + + $this->applications = $this->server->applications(); + $this->databases = $this->server->databases(); + $this->services = $this->server->services()->get(); + $this->previews = $this->server->previews(); + + $foundApplications = []; + $foundApplicationPreviews = []; + $foundDatabases = []; + $foundServices = []; + + foreach ($this->containers as $container) { + if ($this->server->isSwarm()) { + $labels = data_get($container, 'Spec.Labels'); + $uuid = data_get($labels, 'coolify.name'); + } else { + $labels = data_get($container, 'Config.Labels'); + } + $containerStatus = data_get($container, 'State.Status'); + $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + $containerStatus = "$containerStatus ($containerHealth)"; + $labels = Arr::undot(format_docker_labels_to_json($labels)); + $applicationId = data_get($labels, 'coolify.applicationId'); + if ($applicationId) { + $pullRequestId = data_get($labels, 'coolify.pullRequestId'); + if ($pullRequestId) { + if (str($applicationId)->contains('-')) { + $applicationId = str($applicationId)->before('-'); + } + $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); + if ($preview) { + $foundApplicationPreviews[] = $preview->id; + $statusFromDb = $preview->status; + if ($statusFromDb !== $containerStatus) { + $preview->update(['status' => $containerStatus]); + } + } else { + //Notify user that this container should not be there. + } + } else { + $application = $this->applications->where('id', $applicationId)->first(); + if ($application) { + $foundApplications[] = $application->id; + $statusFromDb = $application->status; + if ($statusFromDb !== $containerStatus) { + $application->update(['status' => $containerStatus]); + } + } else { + //Notify user that this container should not be there. + } + } + } else { + $uuid = data_get($labels, 'com.docker.compose.service'); + $type = data_get($labels, 'coolify.type'); + + if ($uuid) { + if ($type === 'service') { + $database_id = data_get($labels, 'coolify.service.subId'); + if ($database_id) { + $service_db = ServiceDatabase::where('id', $database_id)->first(); + if ($service_db) { + $uuid = data_get($service_db, 'service.uuid'); + if ($uuid) { + $isPublic = data_get($service_db, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + return data_get($value, 'Name') === "/$uuid-proxy"; + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($service_db); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); + } + } + } + } + } + } else { + $database = $this->databases->where('uuid', $uuid)->first(); + if ($database) { + $isPublic = data_get($database, 'is_public'); + $foundDatabases[] = $database->id; + $statusFromDb = $database->status; + if ($statusFromDb !== $containerStatus) { + $database->update(['status' => $containerStatus]); + } + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + return data_get($value, 'Name') === "/$uuid-proxy"; + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($database); + $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + } + } + } else { + // Notify user that this container should not be there. + } + } + } + if (data_get($container, 'Name') === '/coolify-db') { + $foundDatabases[] = 0; + } + } + $serviceLabelId = data_get($labels, 'coolify.serviceId'); + if ($serviceLabelId) { + $subType = data_get($labels, 'coolify.service.subType'); + $subId = data_get($labels, 'coolify.service.subId'); + $service = $this->services->where('id', $serviceLabelId)->first(); + if (! $service) { + continue; + } + if ($subType === 'application') { + $service = $service->applications()->where('id', $subId)->first(); + } else { + $service = $service->databases()->where('id', $subId)->first(); + } + if ($service) { + $foundServices[] = "$service->id-$service->name"; + $statusFromDb = $service->status; + if ($statusFromDb !== $containerStatus) { + // ray('Updating status: ' . $containerStatus); + $service->update(['status' => $containerStatus]); + } + } + } + } + $exitedServices = collect([]); + foreach ($this->services as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + if (in_array("$app->id-$app->name", $foundServices)) { + continue; + } else { + $exitedServices->push($app); + } + } + foreach ($dbs as $db) { + if (in_array("$db->id-$db->name", $foundServices)) { + continue; + } else { + $exitedServices->push($db); + } + } + } + $exitedServices = $exitedServices->unique('id'); + foreach ($exitedServices as $exitedService) { + if (str($exitedService->status)->startsWith('exited')) { + continue; + } + $name = data_get($exitedService, 'name'); + $fqdn = data_get($exitedService, 'fqdn'); + if ($name) { + if ($fqdn) { + $containerName = "$name, available at $fqdn"; + } else { + $containerName = $name; + } + } else { + if ($fqdn) { + $containerName = $fqdn; + } else { + $containerName = null; + } + } + $projectUuid = data_get($service, 'environment.project.uuid'); + $serviceUuid = data_get($service, 'uuid'); + $environmentName = data_get($service, 'environment.name'); + + if ($projectUuid && $serviceUuid && $environmentName) { + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; + } else { + $url = null; + } + // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + $exitedService->update(['status' => 'exited']); + } + + $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); + foreach ($notRunningApplications as $applicationId) { + $application = $this->applications->where('id', $applicationId)->first(); + if (str($application->status)->startsWith('exited')) { + continue; + } + $application->update(['status' => 'exited']); + + $name = data_get($application, 'name'); + $fqdn = data_get($application, 'fqdn'); + + $containerName = $name ? "$name ($fqdn)" : $fqdn; + + $projectUuid = data_get($application, 'environment.project.uuid'); + $applicationUuid = data_get($application, 'uuid'); + $environment = data_get($application, 'environment.name'); + + if ($projectUuid && $applicationUuid && $environment) { + $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; + } else { + $url = null; + } + + // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + } + $notRunningApplicationPreviews = $this->previews->pluck('id')->diff($foundApplicationPreviews); + foreach ($notRunningApplicationPreviews as $previewId) { + $preview = $this->previews->where('id', $previewId)->first(); + if (str($preview->status)->startsWith('exited')) { + continue; + } + $preview->update(['status' => 'exited']); + + $name = data_get($preview, 'name'); + $fqdn = data_get($preview, 'fqdn'); + + $containerName = $name ? "$name ($fqdn)" : $fqdn; + + $projectUuid = data_get($preview, 'application.environment.project.uuid'); + $environmentName = data_get($preview, 'application.environment.name'); + $applicationUuid = data_get($preview, 'application.uuid'); + + if ($projectUuid && $applicationUuid && $environmentName) { + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; + } else { + $url = null; + } + + // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + } + $notRunningDatabases = $this->databases->pluck('id')->diff($foundDatabases); + foreach ($notRunningDatabases as $database) { + $database = $this->databases->where('id', $database)->first(); + if (str($database->status)->startsWith('exited')) { + continue; + } + $database->update(['status' => 'exited']); + + $name = data_get($database, 'name'); + $fqdn = data_get($database, 'fqdn'); + + $containerName = $name; + + $projectUuid = data_get($database, 'environment.project.uuid'); + $environmentName = data_get($database, 'environment.name'); + $databaseUuid = data_get($database, 'uuid'); + + if ($projectUuid && $databaseUuid && $environmentName) { + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; + } else { + $url = null; + } + // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + } + + // Check if proxy is running + $this->server->proxyType(); + $foundProxyContainer = $this->containers->filter(function ($value, $key) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + } else { + return data_get($value, 'Name') === '/coolify-proxy'; + } + })->first(); + if (! $foundProxyContainer) { + try { + $shouldStart = CheckProxy::run($this->server); + if ($shouldStart) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + ray($e); + } + } else { + $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } +} diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index f822cfa5f..4fc938df8 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -52,7 +52,7 @@ class Docker extends Component if (request()->query('network_name')) { $this->network = request()->query('network_name'); } else { - $this->network = new Cuid2(7); + $this->network = new Cuid2; } if ($this->servers->count() > 0) { $this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab(); diff --git a/app/Livewire/MonacoEditor.php b/app/Livewire/MonacoEditor.php index 156c63d3a..42d276e64 100644 --- a/app/Livewire/MonacoEditor.php +++ b/app/Livewire/MonacoEditor.php @@ -39,7 +39,7 @@ class MonacoEditor extends Component public function render() { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index 3b402b3ec..2bc28026f 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -96,6 +96,20 @@ class Advanced extends Component } else { $this->application->settings->custom_internal_name = null; } + $customInternalName = $this->application->settings->custom_internal_name; + $server = $this->application->destination->server; + $allApplications = $server->applications(); + + $foundSameInternalName = $allApplications->filter(function ($application) { + return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->application->settings->custom_internal_name; + }); + if ($foundSameInternalName->isNotEmpty()) { + $this->dispatch('error', 'This custom container name is already in use by another application on this server.'); + $this->application->settings->custom_internal_name = $customInternalName; + $this->application->settings->refresh(); + + return; + } $this->application->settings->save(); $this->dispatch('success', 'Custom name saved.'); } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 7dfd9bad4..395c45524 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -228,7 +228,7 @@ class General extends Component public function generateDomain(string $serviceName) { - $uuid = new Cuid2(7); + $uuid = new Cuid2; $domain = generateFqdn($this->application->destination->server, $uuid); $this->parsedServiceDomains[$serviceName]['domain'] = $domain; $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index feb54c7f0..b5f01587a 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -102,7 +102,7 @@ class Heading extends Component protected function setDeploymentUuid() { - $this->deploymentUuid = new Cuid2(7); + $this->deploymentUuid = new Cuid2; $this->parameters['deployment_uuid'] = $this->deploymentUuid; } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index df64c3fd3..30bc0a9d1 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -85,7 +85,7 @@ class Previews extends Component $template = $this->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $preview->pull_request_id, $preview_fqdn); @@ -170,7 +170,7 @@ class Previews extends Component protected function setDeploymentUuid() { - $this->deployment_uuid = new Cuid2(7); + $this->deployment_uuid = new Cuid2; $this->parameters['deployment_uuid'] = $this->deployment_uuid; } diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index bf4478e53..b3e838bb3 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -44,7 +44,7 @@ class PreviewsCompose extends Component $template = $this->preview->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); diff --git a/app/Livewire/Project/Application/Rollback.php b/app/Livewire/Project/Application/Rollback.php index ed0ac1cef..1e58a1458 100644 --- a/app/Livewire/Project/Application/Rollback.php +++ b/app/Livewire/Project/Application/Rollback.php @@ -23,7 +23,7 @@ class Rollback extends Component public function rollbackImage($commit) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $this->application, diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 5373f1b3f..4d2bc6589 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -47,7 +47,7 @@ class CloneMe extends Component $this->environment = $this->project->environments->where('name', $this->environment_name)->first(); $this->project_id = $this->project->id; $this->servers = currentTeam()->servers; - $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2(7))->slug(); + $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug(); } public function render() @@ -106,7 +106,7 @@ class CloneMe extends Component $databases = $this->environment->databases(); $services = $this->environment->services; foreach ($applications as $application) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $newApplication = $application->replicate()->fill([ 'uuid' => $uuid, 'fqdn' => generateFqdn($this->server, $uuid), @@ -133,7 +133,7 @@ class CloneMe extends Component } } foreach ($databases as $database) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $newDatabase = $database->replicate()->fill([ 'uuid' => $uuid, 'status' => 'exited', @@ -161,7 +161,7 @@ class CloneMe extends Component } } foreach ($services as $service) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $newService = $service->replicate()->fill([ 'uuid' => $uuid, 'environment_id' => $environment->id, diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index fdad052c7..d3f5b5261 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -48,7 +48,7 @@ class DockerImage extends Component $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); ray($image, $tag); $application = Application::create([ - 'name' => 'docker-image-'.new Cuid2(7), + 'name' => 'docker-image-'.new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index 6f6bc9185..3c7f42329 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -53,7 +53,7 @@ CMD ["nginx", "-g", "daemon off;"] $port = 80; } $application = Application::create([ - 'name' => 'dockerfile-'.new Cuid2(7), + 'name' => 'dockerfile-'.new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index f2fd5ae96..419fef505 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -63,6 +63,8 @@ class Navbar extends Component public function checkDeployments() { try { + // TODO: This is a temporary solution. We need to refactor this. + // We need to delete null bytes somehow. $activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first(); $status = data_get($activity, 'properties.status'); if ($status === 'queued' || $status === 'in_progress') { @@ -70,7 +72,7 @@ class Navbar extends Component } else { $this->isDeploymentProgress = false; } - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->isDeploymentProgress = false; } } diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 30c35410f..5f0178be4 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -22,7 +22,7 @@ class Danger extends Component public function mount() { - $this->modalId = new Cuid2(7); + $this->modalId = new Cuid2; $parameters = get_route_parameters(); $this->projectUuid = data_get($parameters, 'project_uuid'); $this->environmentName = data_get($parameters, 'environment_name'); diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 22ada8ab8..a2c018beb 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -67,7 +67,7 @@ class Destination extends Component return; } - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $server = Server::find($server_id); $destination = StandaloneDocker::find($network_id); queue_application_deployment( diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index b732b6b52..a859c90b0 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -48,14 +48,14 @@ class Add extends Component public function submit() { $this->validate(); - if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) { - $type = str($this->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + // if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) { + // $type = str($this->value)->after('{{')->before('.')->value; + // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - return; - } - } + // return; + // } + // } $this->dispatch('saveKey', [ 'key' => $this->key, 'value' => $this->value, diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index d1edaf4f5..9e6760293 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -39,7 +39,7 @@ class All extends Component if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) { $this->showPreview = true; } - $this->modalId = new Cuid2(7); + $this->modalId = new Cuid2; $this->sortMe(); $this->getDevView(); } @@ -125,14 +125,14 @@ class All extends Component continue; } $found->value = $variable; - if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) { - $type = str($found->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + // if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) { + // $type = str($found->value)->after('{{')->before('.')->value; + // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - return; - } - } + // return; + // } + // } $found->save(); continue; @@ -140,14 +140,14 @@ class All extends Component $environment = new EnvironmentVariable; $environment->key = $key; $environment->value = $variable; - if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) { - $type = str($environment->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + // if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) { + // $type = str($environment->value)->after('{{')->before('.')->value; + // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - return; - } - } + // return; + // } + // } $environment->is_build_time = false; $environment->is_multiline = false; $environment->is_preview = $isPreview ? true : false; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index d78b47363..e63871602 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -58,7 +58,7 @@ class Show extends Component if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') { $this->isSharedVariable = true; } - $this->modalId = new Cuid2(7); + $this->modalId = new Cuid2; $this->parameters = get_route_parameters(); $this->checkEnvs(); } @@ -108,14 +108,14 @@ class Show extends Component } else { $this->validate(); } - if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) { - $type = str($this->env->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + // if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) { + // $type = str($this->env->value)->after('{{')->before('.')->value; + // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - return; - } - } + // return; + // } + // } $this->serialize(); $this->env->save(); $this->dispatch('success', 'Environment variable updated.'); diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 586a125ae..ec09eb80f 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -39,7 +39,7 @@ class ResourceOperations extends Component if (! $new_destination) { return $this->addError('destination_id', 'Destination not found.'); } - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $server = $new_destination->server; if ($this->resource->getMorphClass() === 'App\Models\Application') { $new_resource = $this->resource->replicate()->fill([ @@ -87,7 +87,7 @@ class ResourceOperations extends Component $this->resource->getMorphClass() === 'App\Models\StandaloneDragonfly' || $this->resource->getMorphClass() === 'App\Models\StandaloneClickhouse' ) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, @@ -121,7 +121,7 @@ class ResourceOperations extends Component return redirect()->to($route); } elseif ($this->resource->type() === 'service') { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index dbd420d94..8be4ff643 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -47,7 +47,7 @@ class Show extends Component $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); } - $this->modalId = new Cuid2(7); + $this->modalId = new Cuid2; $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); } diff --git a/app/Models/Application.php b/app/Models/Application.php index 7b39292e0..8ee78bb73 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -232,12 +232,24 @@ class Application extends BaseModel public function failedTaskLink($task_uuid) { if (data_get($this, 'environment.project.uuid')) { - return route('project.application.scheduled-tasks', [ + $route = route('project.application.scheduled-tasks', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), 'application_uuid' => data_get($this, 'uuid'), 'task_uuid' => $task_uuid, ]); + $settings = InstanceSettings::get(); + if (data_get($settings, 'fqdn')) { + $url = Url::fromString($route); + $url = $url->withPort(null); + $fqdn = data_get($settings, 'fqdn'); + $fqdn = str_replace(['http://', 'https://'], '', $fqdn); + $url = $url->withHost($fqdn); + + return $url->__toString(); + } + + return $route; } return null; @@ -275,12 +287,20 @@ class Application extends BaseModel return Attribute::make( get: function () { if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { + if (str($this->git_repository)->contains('bitbucket')) { + return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}"; + } + return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}"; } // Convert the SSH URL to HTTPS URL if (strpos($this->git_repository, 'git@') === 0) { $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + if (str($this->git_repository)->contains('bitbucket')) { + return "https://{$git_repository}/src/{$this->git_branch}"; + } + return "https://{$git_repository}/tree/{$this->git_branch}"; } @@ -431,6 +451,11 @@ class Application extends BaseModel ); } + public function isRunning() + { + return (bool) str($this->status)->startsWith('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -1270,7 +1295,7 @@ class Application extends BaseModel $template = $this->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 3bdd24014..57d20e3aa 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -35,6 +35,11 @@ class ApplicationPreview extends BaseModel return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail(); } + public function isRunning() + { + return (bool) str($this->status)->startsWith('running'); + } + public function application() { return $this->belongsTo(Application::class); @@ -49,7 +54,7 @@ class ApplicationPreview extends BaseModel $template = $this->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 7e028a6b5..17201ea6e 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -14,7 +14,7 @@ abstract class BaseModel extends Model static::creating(function (Model $model) { // Generate a UUID if one isn't set if (! $model->uuid) { - $model->uuid = (string) new Cuid2(7); + $model->uuid = (string) new Cuid2; } }); } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 28cf0ef93..5e1d8ae13 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -5,7 +5,6 @@ namespace App\Models; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; use OpenApi\Attributes as OA; use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; @@ -200,28 +199,33 @@ class EnvironmentVariable extends Model return null; } $environment_variable = trim($environment_variable); - $type = str($environment_variable)->after('{{')->before('.')->value; - if (str($environment_variable)->startsWith('{{'.$type) && str($environment_variable)->endsWith('}}')) { - $variable = Str::after($environment_variable, "{$type}."); - $variable = Str::before($variable, '}}'); - $variable = str($variable)->trim()->value; + $sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/'); + if ($sharedEnvsFound->isEmpty()) { + return $environment_variable; + } + foreach ($sharedEnvsFound as $sharedEnv) { + $type = str($sharedEnv)->match('/(.*?)\./'); if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - return $variable; + continue; } - if ($type === 'environment') { + $variable = str($sharedEnv)->match('/\.(.*)/'); + if ($type->value() === 'environment') { $id = $resource->environment->id; - } elseif ($type === 'project') { + } elseif ($type->value() === 'project') { $id = $resource->environment->project->id; - } else { + } elseif ($type->value() === 'team') { $id = $resource->team()->id; } + if (is_null($id)) { + continue; + } $environment_variable_found = SharedEnvironmentVariable::where('type', $type)->where('key', $variable)->where('team_id', $resource->team()->id)->where("{$type}_id", $id)->first(); if ($environment_variable_found) { - return $environment_variable_found; + $environment_variable = str($environment_variable)->replace("{{{$sharedEnv}}}", $environment_variable_found->value); } } - return $environment_variable; + return str($environment_variable)->value(); } private function get_environment_variables(?string $environment_variable = null): ?string diff --git a/app/Models/Service.php b/app/Models/Service.php index 8336b90c8..1aa88c8ec 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; use OpenApi\Attributes as OA; +use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; #[OA\Schema( @@ -76,6 +77,11 @@ class Service extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status())->contains('running'); + } + public function isExited() { return (bool) str($this->status())->contains('exited'); @@ -575,6 +581,30 @@ class Service extends BaseModel $fields->put('Vaultwarden', $data); break; + case str($image)->contains('gitlab/gitlab'): + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GITLAB')->first(); + $data = collect([]); + if ($password) { + $data = $data->merge([ + 'Root Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $data = $data->merge([ + 'Root User' => [ + 'key' => 'N/A', + 'value' => 'root', + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + + $fields->put('GitLab', $data->toArray()); + break; } } $databases = $this->databases()->get(); @@ -764,12 +794,24 @@ class Service extends BaseModel public function failedTaskLink($task_uuid) { if (data_get($this, 'environment.project.uuid')) { - return route('project.service.scheduled-tasks', [ + $route = route('project.service.scheduled-tasks', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), 'service_uuid' => data_get($this, 'uuid'), 'task_uuid' => $task_uuid, ]); + $settings = InstanceSettings::get(); + if (data_get($settings, 'fqdn')) { + $url = Url::fromString($route); + $url = $url->withPort(null); + $fqdn = data_get($settings, 'fqdn'); + $fqdn = str_replace(['http://', 'https://'], '', $fqdn); + $url = $url->withHost($fqdn); + + return $url->__toString(); + } + + return $route; } return null; diff --git a/app/Models/User.php b/app/Models/User.php index 3625b9930..ecc4ef6b6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -180,6 +180,10 @@ class User extends Authenticatable implements SendsEmail { $found_root_team = auth()->user()->teams->filter(function ($team) { if ($team->id == 0) { + if (! auth()->user()->isAdmin()) { + return false; + } + return true; } diff --git a/app/View/Components/Forms/Datalist.php b/app/View/Components/Forms/Datalist.php index df0c1cb11..25643753d 100644 --- a/app/View/Components/Forms/Datalist.php +++ b/app/View/Components/Forms/Datalist.php @@ -30,7 +30,7 @@ class Datalist extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 35448d5e5..6c9378cac 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -27,7 +27,7 @@ class Input extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php index 21c147c2b..dd5ba66b7 100644 --- a/app/View/Components/Forms/Select.php +++ b/app/View/Components/Forms/Select.php @@ -30,7 +30,7 @@ class Select extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 7d1860500..3f887877c 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -41,7 +41,7 @@ class Textarea extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 5bfdcce78..b8dcc1f3c 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -14,7 +14,7 @@ use Visus\Cuid2\Cuid2; function generate_database_name(string $type): string { - $cuid = new Cuid2(7); + $cuid = new Cuid2; return $type.'-database-'.$cuid; } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 0aa5c6b74..f32100f88 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -338,7 +338,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ foreach ($domains as $loop => $domain) { try { if ($generate_unique_uuid) { - $uuid = new Cuid2(7); + $uuid = new Cuid2; } $url = Url::fromString($domain); diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 23c9a2333..dccfaeb38 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -36,16 +36,23 @@ function collectDockerNetworksByServer(Server $server) } // Service networks foreach ($server->services()->get() as $service) { - $networks->push($service->networks()); + if ($service->isRunning()) { + $networks->push($service->networks()); + } } // Docker compose based apps $docker_compose_apps = $server->dockerComposeBasedApplications(); foreach ($docker_compose_apps as $app) { - $networks->push($app->uuid); + if ($app->isRunning()) { + $networks->push($app->uuid); + } } // Docker compose based preview deployments $docker_compose_previews = $server->dockerComposeBasedPreviewDeployments(); foreach ($docker_compose_previews as $preview) { + if (! $preview->isRunning()) { + continue; + } $pullRequestId = $preview->pull_request_id; $applicationId = $preview->application_id; $application = Application::find($applicationId); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 56487c8a6..b93d15b9a 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -200,7 +200,7 @@ function generate_random_name(?string $cuid = null): string ] ); if (is_null($cuid)) { - $cuid = new Cuid2(7); + $cuid = new Cuid2; } return Str::kebab("{$generator->getName()}-$cuid"); @@ -236,7 +236,7 @@ function formatPrivateKey(string $privateKey) function generate_application_name(string $git_repository, string $git_branch, ?string $cuid = null): string { if (is_null($cuid)) { - $cuid = new Cuid2(7); + $cuid = new Cuid2; } return Str::kebab("$git_repository:$git_branch-$cuid"); @@ -2022,7 +2022,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $template = $resource->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); diff --git a/config/sentry.php b/config/sentry.php index b2f6ded80..a27a18d30 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.319', + 'release' => '4.0.0-beta.320', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 8b5a5afc6..05acb11ca 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ 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.' - docker_compose_location: - type: string - description: 'The Docker Compose location.' - docker_compose_raw: - type: string - description: 'The Docker Compose raw content.' - 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.' - docker_compose_domains: - type: array - description: 'The Docker Compose domains.' - watch_paths: - type: string - description: 'The watch paths.' - type: object - responses: - '200': - description: 'Application updated.' - content: - application/json: - schema: - properties: - uuid: { type: string } - type: object - '401': - $ref: '#/components/responses/401' - '400': - $ref: '#/components/responses/400' - '404': - $ref: '#/components/responses/404' - security: - - - bearerAuth: [] /applications/public: post: tags: @@ -1369,6 +1151,225 @@ paths: security: - bearerAuth: [] + patch: + tags: + - Applications + summary: Update + description: 'Update application by UUID.' + operationId: 62a3b1775e8cba5d39a236ebb69830b7 + requestBody: + description: 'Application updated.' + required: true + content: + application/json: + schema: + properties: + project_uuid: + type: string + description: 'The project UUID.' + server_uuid: + type: string + description: 'The server UUID.' + environment_name: + type: string + description: 'The environment name.' + github_app_uuid: + type: string + description: 'The Github App UUID.' + git_repository: + type: string + description: 'The git repository URL.' + git_branch: + type: string + description: 'The git branch.' + ports_exposes: + type: string + description: 'The ports to expose.' + destination_uuid: + type: string + description: 'The destination UUID.' + 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.' + 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.' + install_command: + type: string + description: 'The install command.' + build_command: + type: string + description: 'The build command.' + start_command: + type: string + description: 'The start command.' + ports_mappings: + type: string + description: 'The ports mappings.' + base_directory: + type: string + description: 'The base directory for all commands.' + publish_directory: + type: string + description: 'The publish directory.' + health_check_enabled: + type: boolean + description: 'Health check enabled.' + health_check_path: + type: string + description: 'Health check path.' + health_check_port: + type: string + nullable: true + description: 'Health check port.' + health_check_host: + type: string + nullable: true + description: 'Health check host.' + health_check_method: + type: string + description: 'Health check method.' + health_check_return_code: + type: integer + description: 'Health check return code.' + health_check_scheme: + type: string + description: 'Health check scheme.' + health_check_response_text: + type: string + nullable: true + description: 'Health check response text.' + health_check_interval: + type: integer + description: 'Health check interval in seconds.' + health_check_timeout: + type: integer + description: 'Health check timeout in seconds.' + health_check_retries: + type: integer + description: 'Health check retries count.' + health_check_start_period: + type: integer + description: 'Health check start period in seconds.' + limits_memory: + type: string + description: 'Memory limit.' + limits_memory_swap: + type: string + description: 'Memory swap limit.' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness.' + limits_memory_reservation: + type: string + description: 'Memory reservation.' + limits_cpus: + type: string + description: 'CPU limit.' + limits_cpuset: + type: string + nullable: true + description: 'CPU set.' + limits_cpu_shares: + type: integer + description: 'CPU shares.' + custom_labels: + type: string + description: 'Custom labels.' + custom_docker_run_options: + type: string + description: 'Custom docker run options.' + post_deployment_command: + type: string + description: 'Post deployment command.' + post_deployment_command_container: + type: string + description: 'Post deployment command container.' + pre_deployment_command: + type: string + description: 'Pre deployment command.' + pre_deployment_command_container: + type: string + description: 'Pre deployment command container.' + manual_webhook_secret_github: + type: string + description: 'Manual webhook secret for Github.' + manual_webhook_secret_gitlab: + type: string + description: 'Manual webhook secret for Gitlab.' + manual_webhook_secret_bitbucket: + type: string + description: 'Manual webhook secret for Bitbucket.' + 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.' + dockerfile: + type: string + description: 'The Dockerfile content.' + docker_compose_location: + type: string + description: 'The Docker Compose location.' + docker_compose_raw: + type: string + description: 'The Docker Compose raw content.' + 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.' + docker_compose_domains: + type: array + description: 'The Docker Compose domains.' + watch_paths: + type: string + description: 'The watch paths.' + type: object + responses: + '200': + description: 'Application updated.' + content: + application/json: + schema: + properties: + uuid: { type: string } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] '/applications/{uuid}/envs': get: tags: @@ -2833,7 +2834,7 @@ paths: description: 'Deployment Uuid' required: true schema: - type: integer + type: string responses: '200': description: 'Get deployment by UUID.' @@ -3062,7 +3063,7 @@ paths: description: 'Project UUID' required: true schema: - type: integer + type: string responses: '200': description: 'Project details' @@ -3166,7 +3167,7 @@ paths: description: 'Project UUID' required: true schema: - type: integer + type: string - name: environment_name in: path @@ -3325,7 +3326,7 @@ paths: description: 'Private Key Uuid' required: true schema: - type: integer + type: string responses: '200': description: 'Get all private keys.' @@ -3357,7 +3358,7 @@ paths: description: 'Private Key Uuid' required: true schema: - type: integer + type: string responses: '200': description: 'Private Key deleted.' @@ -3477,7 +3478,7 @@ paths: description: "Server's Uuid" required: true schema: - type: integer + type: string responses: '200': description: 'Get server by UUID' @@ -3597,7 +3598,7 @@ paths: description: "Server's Uuid" required: true schema: - type: integer + type: string responses: '200': description: 'Get resources by server' @@ -3629,7 +3630,7 @@ paths: description: "Server's Uuid" required: true schema: - type: integer + type: string responses: '200': description: 'Get domains by server' @@ -3661,7 +3662,7 @@ paths: description: 'Server UUID' required: true schema: - type: integer + type: string responses: '201': description: 'Server validation started.' diff --git a/public/svgs/gitlab.svg b/public/svgs/gitlab.svg new file mode 100644 index 000000000..1c7cb0719 --- /dev/null +++ b/public/svgs/gitlab.svg @@ -0,0 +1 @@ + diff --git a/resources/views/livewire/project/service/edit-domain.blade.php b/resources/views/livewire/project/service/edit-domain.blade.php index 214b729fb..5528834eb 100644 --- a/resources/views/livewire/project/service/edit-domain.blade.php +++ b/resources/views/livewire/project/service/edit-domain.blade.php @@ -1,5 +1,6 @@