diff --git a/.gitpod.yml b/.gitpod.yml index 228f1b94c..6fd6797b5 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -3,7 +3,7 @@ tasks: # Fix because of https://github.com/gitpod-io/gitpod/issues/16614 before: sudo curl -o /usr/local/bin/docker-compose -fsSL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-$(uname -m) init: | - cp .env.example .env && + cp .env.development.example .env && sed -i "s#APP_URL=http://localhost#APP_URL=$(gp url 8000)#g" .env sed -i "s#USERID=#USERID=33333#g" .env sed -i "s#GROUPID=#GROUPID=33333#g" .env @@ -20,7 +20,7 @@ tasks: echo "Waiting for Sail environment to boot up." gp sync-await spin-is-ready ./vendor/bin/spin exec vite npm install - ./vendor/bin/spin exec vite npm run dev + ./vendor/bin/spin exec vite npm run dev -- --host - name: Laravel Queue Worker, listening to code changes command: | diff --git a/README.md b/README.md index 01c1ccef9..97580b875 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,12 @@ # Donations ## Github Sponsors ($40+) BC Direct +SerpAPI typebot +QuantCDN + + + FlintCompany American Cloud CryptoJobsList UXWizz @@ -62,6 +67,7 @@ ## Organizations + ## Individuals diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index d043da410..5f567802f 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -33,7 +33,6 @@ public function handle(StandaloneClickhouse $database) $environment_variables = $this->generate_environment_variables(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index a1e47710c..547884b7a 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -107,7 +107,6 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St COPY nginx.conf /etc/nginx/nginx.conf EOF; $docker_compose = [ - 'version' => '3.8', 'services' => [ $proxyContainerName => [ 'build' => [ diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index bb71d8c48..92daf195d 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -36,7 +36,6 @@ public function handle(StandaloneDragonfly $database) $environment_variables = $this->generate_environment_variables(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 489c74053..8c833efd5 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -37,7 +37,6 @@ public function handle(StandaloneKeydb $database) $this->add_custom_keydb(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -96,7 +95,7 @@ public function handle(StandaloneKeydb $database) if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->keydb_conf)) { + if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', 'source' => $this->configuration_dir . '/keydb.conf', @@ -162,7 +161,7 @@ private function generate_environment_variables() } private function add_custom_keydb() { - if (is_null($this->database->keydb_conf)) { + if (is_null($this->database->keydb_conf) || empty($this->database->keydb_conf)) { return; } $filename = 'keydb.conf'; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index e02b28b2e..c79df0dc5 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -32,7 +32,6 @@ public function handle(StandaloneMariadb $database) $environment_variables = $this->generate_environment_variables(); $this->add_custom_mysql(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -90,7 +89,7 @@ public function handle(StandaloneMariadb $database) if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->mariadb_conf)) { + if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', 'source' => $this->configuration_dir . '/custom-config.cnf', @@ -165,7 +164,7 @@ private function generate_environment_variables() } private function add_custom_mysql() { - if (is_null($this->database->mariadb_conf)) { + if (is_null($this->database->mariadb_conf) || empty($this->database->mariadb_conf)) { return; } $filename = 'custom-config.cnf'; diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 7bb6cbcd0..46b426ad8 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -35,7 +35,6 @@ public function handle(StandaloneMongodb $database) $this->add_custom_mongo_conf(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -51,8 +50,9 @@ public function handle(StandaloneMongodb $database) ], 'healthcheck' => [ 'test' => [ - 'CMD-SHELL', - 'mongosh --eval "printjson(db.runCommand(\"ping\"))"' + "CMD", + "echo", + "ok" ], 'interval' => '5s', 'timeout' => '5s', @@ -97,7 +97,7 @@ public function handle(StandaloneMongodb $database) if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->mongo_conf)) { + if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', 'source' => $this->configuration_dir . '/mongod.conf', @@ -178,7 +178,7 @@ private function generate_environment_variables() } private function add_custom_mongo_conf() { - if (is_null($this->database->mongo_conf)) { + if (is_null($this->database->mongo_conf) || empty($this->database->mongo_conf)) { return; } $filename = 'mongod.conf'; diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index b3f695d72..6fdc8cdad 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -32,7 +32,6 @@ public function handle(StandaloneMysql $database) $environment_variables = $this->generate_environment_variables(); $this->add_custom_mysql(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -90,7 +89,7 @@ public function handle(StandaloneMysql $database) if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->mysql_conf)) { + if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', 'source' => $this->configuration_dir . '/custom-config.cnf', @@ -165,7 +164,7 @@ private function generate_environment_variables() } private function add_custom_mysql() { - if (is_null($this->database->mysql_conf)) { + if (is_null($this->database->mysql_conf) || empty($this->database->mysql_conf)) { return; } $filename = 'custom-config.cnf'; diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index f19a8b036..8db874ea6 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -35,7 +35,6 @@ public function handle(StandalonePostgresql $database) $this->add_custom_conf(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -78,7 +77,6 @@ public function handle(StandalonePostgresql $database) data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { - ray('Log Drain Enabled'); $docker_compose['services'][$container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ @@ -107,7 +105,7 @@ public function handle(StandalonePostgresql $database) ]; } } - if (!is_null($this->database->postgres_conf)) { + if (!is_null($this->database->postgres_conf) && !empty($this->database->postgres_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', 'source' => $this->configuration_dir . '/custom-postgres.conf', @@ -165,8 +163,6 @@ private function generate_local_persistent_volumes_only_volume_names() private function generate_environment_variables() { $environment_variables = collect(); - ray('Generate Environment Variables')->green(); - ray($this->database->runtime_environment_variables)->green(); foreach ($this->database->runtime_environment_variables as $env) { $environment_variables->push("$env->key=$env->real_value"); } @@ -203,11 +199,16 @@ private function generate_init_scripts() } private function add_custom_conf() { - if (is_null($this->database->postgres_conf)) { + if (is_null($this->database->postgres_conf) || empty($this->database->postgres_conf)) { return; } $filename = 'custom-postgres.conf'; $content = $this->database->postgres_conf; + if (!str($content)->contains('listen_addresses')) { + $content .= "\nlisten_addresses = '*'"; + $this->database->postgres_conf = $content; + $this->database->save(); + } $content_base64 = base64_encode($content); $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; } diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 01e9a9bef..5b6ab2999 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -37,7 +37,6 @@ public function handle(StandaloneRedis $database) $this->add_custom_redis(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -100,7 +99,7 @@ public function handle(StandaloneRedis $database) if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->redis_conf)) { + if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', 'source' => $this->configuration_dir . '/redis.conf', @@ -166,7 +165,7 @@ private function generate_environment_variables() } private function add_custom_redis() { - if (is_null($this->database->redis_conf)) { + if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) { return; } $filename = 'redis.conf'; diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php new file mode 100644 index 000000000..f0e1de8f6 --- /dev/null +++ b/app/Actions/Docker/GetContainersStatus.php @@ -0,0 +1,657 @@ +server = $server; + if (!$this->server->isFunctional()) { + return 'Server is not ready.'; + }; + $this->applications = $this->server->applications(); + $skip_these_applications = collect([]); + foreach ($this->applications as $application) { + if ($application->additional_servers->count() > 0) { + $skip_these_applications->push($application); + ComplexStatusCheck::run($application); + $this->applications = $this->applications->filter(function ($value, $key) use ($application) { + return $value->id !== $application->id; + }); + } + } + $this->applications = $this->applications->filter(function ($value, $key) use ($skip_these_applications) { + return !$skip_these_applications->pluck('id')->contains($value->id); + }); + $this->old_way(); + // if ($this->server->isSwarm()) { + // $this->old_way(); + // } else { + // if (!$this->server->is_metrics_enabled) { + // $this->old_way(); + // return; + // } + // $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this->server, false); + // $sentinel_found = json_decode($sentinel_found, true); + // $status = data_get($sentinel_found, '0.State.Status', 'exited'); + // if ($status === 'running') { + // ray('Checking with Sentinel'); + // $this->sentinel(); + // } else { + // ray('Checking the Old way'); + // $this->old_way(); + // } + // } + } + + private function sentinel() + { + try { + $containers = $this->server->getContainers(); + if ($containers->count() === 0) { + return; + } + $databases = $this->server->databases(); + $services = $this->server->services()->get(); + $previews = $this->server->previews(); + $foundApplications = []; + $foundApplicationPreviews = []; + $foundDatabases = []; + $foundServices = []; + + foreach ($containers as $container) { + $labels = Arr::undot(data_get($container, 'labels')); + $containerStatus = data_get($container, 'state'); + $containerHealth = data_get($container, 'health_status', 'unhealthy'); + $containerStatus = "$containerStatus ($containerHealth)"; + $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 = $service_db->service->uuid; + $isPublic = data_get($service_db, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { + if ($this->server->isSwarm()) { + // TODO: fix this with sentinel + 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 = $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 = $containers->filter(function ($value, $key) use ($uuid) { + if ($this->server->isSwarm()) { + // TODO: fix this with sentinel + 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 = $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 ($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'); + $containerName = $name ? "$name, available at $fqdn" : $fqdn; + $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 = $previews->pluck('id')->diff($foundApplicationPreviews); + foreach ($notRunningApplicationPreviews as $previewId) { + $preview = $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 = $databases->pluck('id')->diff($foundDatabases); + foreach ($notRunningDatabases as $database) { + $database = $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 = $containers->filter(function ($value, $key) { + if ($this->server->isSwarm()) { + // TODO: fix this with sentinel + 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'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } catch (\Exception $e) { + // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage()); + ray($e->getMessage()); + return handleError($e); + } + } + private function old_way() + { + if ($this->server->isSwarm()) { + $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); + $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false); + } else { + // Precheck for containers + $containers = instant_remote_process(["docker container ls -q"], $this->server, false); + if (!$containers) { + return; + } + $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); + $containerReplicates = null; + } + if (is_null($containers)) { + return; + } + + $containers = format_docker_command_output_to_json($containers); + if ($containerReplicates) { + $containerReplicates = format_docker_command_output_to_json($containerReplicates); + foreach ($containerReplicates as $containerReplica) { + $name = data_get($containerReplica, 'Name'); + $containers = $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; + }); + } + } + $databases = $this->server->databases(); + $services = $this->server->services()->get(); + $previews = $this->server->previews(); + $foundApplications = []; + $foundApplicationPreviews = []; + $foundDatabases = []; + $foundServices = []; + + foreach ($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 = $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 = $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 = $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 = $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 ($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'); + $containerName = $name ? "$name, available at $fqdn" : $fqdn; + $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 = $previews->pluck('id')->diff($foundApplicationPreviews); + foreach ($notRunningApplicationPreviews as $previewId) { + $preview = $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 = $databases->pluck('id')->diff($foundDatabases); + foreach ($notRunningDatabases as $database) { + $database = $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 = $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/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php new file mode 100644 index 000000000..6f3c81d77 --- /dev/null +++ b/app/Actions/Server/StartSentinel.php @@ -0,0 +1,22 @@ +server, false); $this->latestVersion = get_latest_version_of_coolify(); $this->currentVersion = config('version'); - if ($settings->next_channel) { - ray('next channel enabled'); - $this->latestVersion = 'next'; - } + // if ($settings->next_channel) { + // ray('next channel enabled'); + // $this->latestVersion = 'next'; + // } if ($force) { $this->update(); } else { diff --git a/app/Console/Commands/AdminRemoveUser.php b/app/Console/Commands/AdminRemoveUser.php new file mode 100644 index 000000000..76af0a97f --- /dev/null +++ b/app/Console/Commands/AdminRemoveUser.php @@ -0,0 +1,54 @@ +argument('email'); + $confirm = $this->confirm('Are you sure you want to remove user with email: ' . $email . '?'); + if (!$confirm) { + $this->info('User removal cancelled.'); + return; + } + $this->info("Removing user with email: $email"); + $user = User::whereEmail($email)->firstOrFail(); + $teams = $user->teams; + foreach ($teams as $team) { + if ($team->members->count() > 1) { + $this->error('User is a member of a team with more than one member. Please remove user from team first.'); + return; + } + $team->delete(); + } + $user->delete(); + } catch (\Exception $e) { + $this->error('Failed to remove user.'); + $this->error($e->getMessage()); + return; + } + } +} diff --git a/app/Console/Commands/Cloud.php b/app/Console/Commands/Cloud.php deleted file mode 100644 index 1386b296c..000000000 --- a/app/Console/Commands/Cloud.php +++ /dev/null @@ -1,33 +0,0 @@ -whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended',true)->each(function($server){ - $this->info($server->name); - }); - } -} diff --git a/app/Console/Commands/RootResetPassword.php b/app/Console/Commands/RootResetPassword.php index df385002e..af2b1a45c 100644 --- a/app/Console/Commands/RootResetPassword.php +++ b/app/Console/Commands/RootResetPassword.php @@ -29,7 +29,6 @@ class RootResetPassword extends Command */ public function handle() { - // $this->info('You are about to reset the root password.'); $password = password('Give me a new password for root user: '); $passwordAgain = password('Again'); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 31760f2fd..23289f90e 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -9,6 +9,7 @@ use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\ContainerStatusJob; use App\Jobs\PullHelperImageJob; +use App\Jobs\PullSentinelImageJob; use App\Jobs\ServerStatusJob; use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; @@ -20,8 +21,10 @@ class Kernel extends ConsoleKernel { + private $all_servers; protected function schedule(Schedule $schedule): void { + $this->all_servers = Server::all(); if (isDev()) { // Instance Jobs $schedule->command('horizon:snapshot')->everyMinute(); @@ -55,35 +58,38 @@ protected function schedule(Schedule $schedule): void } private function pull_helper_image($schedule) { - $servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); + $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { - $schedule->job(new PullHelperImageJob($server))->everyTenMinutes()->onOneServer(); + if (config('coolify.is_sentinel_enabled')) { + $schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer(); + } + $schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer(); } } private function check_resources($schedule) { if (isCloud()) { - $servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); + $servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); $own = Team::find(0)->servers; $servers = $servers->merge($own); $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false); } else { - $servers = Server::all()->where('ip', '!=', '1.2.3.4'); + $servers = $this->all_servers->where('ip', '!=', '1.2.3.4'); $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false); } foreach ($containerServers as $server) { - $schedule->job(new ContainerStatusJob($server))->everyTwoMinutes()->onOneServer(); + $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); if ($server->isLogDrainEnabled()) { - $schedule->job(new CheckLogDrainContainerJob($server))->everyTwoMinutes()->onOneServer(); + $schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer(); } } foreach ($servers as $server) { - $schedule->job(new ServerStatusJob($server))->everyTwoMinutes()->onOneServer(); + $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer(); } } private function instance_auto_update($schedule) { - if (isDev()) { + if (isDev() || isCloud()) { return; } $settings = InstanceSettings::get(); @@ -134,7 +140,16 @@ private function check_scheduled_tasks($schedule) $scheduled_task->delete(); continue; } - + if ($application) { + if (str($application->status)->contains('running') === false) { + continue; + } + } + if ($service) { + if (str($service->status())->contains('running') === false) { + continue; + } + } if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 1241751f0..daba1cecb 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -37,7 +37,7 @@ public function verify() { public function email_verify(EmailVerificationRequest $request) { $request->fulfill(); $name = request()->user()?->name; - send_internal_notification("User {$name} verified their email address."); + // send_internal_notification("User {$name} verified their email address."); return redirect(RouteServiceProvider::HOME); } public function forgot_password(Request $request) { diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 485720c23..7b569c278 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -47,7 +47,7 @@ public function manual(Request $request) if ($x_bitbucket_event === 'repo:push') { $branch = data_get($payload, 'push.changes.0.new.name'); $full_name = data_get($payload, 'repository.full_name'); - + $commit = data_get($payload, 'push.changes.0.new.target.hash'); if (!$branch) { return response([ 'status' => 'failed', @@ -104,6 +104,7 @@ public function manual(Request $request) queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, + commit: $commit, force_rebuild: false, is_webhook: true ); diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 214843aab..baa23deec 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -129,6 +129,7 @@ public function manual(Request $request) application: $application, deployment_uuid: $deployment_uuid, force_rebuild: false, + commit: data_get($payload, 'after', 'HEAD'), is_webhook: true, ); $return_payloads->push([ @@ -177,6 +178,7 @@ public function manual(Request $request) pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, force_rebuild: false, + commit: data_get($payload, 'head.sha', 'HEAD'), is_webhook: true, git_type: 'github' ); @@ -338,6 +340,7 @@ public function normal(Request $request) queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, + commit: data_get($payload, 'after', 'HEAD'), force_rebuild: false, is_webhook: true, ); @@ -387,6 +390,7 @@ public function normal(Request $request) pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, force_rebuild: false, + commit: data_get($payload, 'head.sha', 'HEAD'), is_webhook: true, git_type: 'github' ); diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index 65ce9910b..dfa9394eb 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -38,6 +38,15 @@ public function manual(Request $request) $headers = $request->headers->all(); $x_gitlab_token = data_get($headers, 'x-gitlab-token.0'); $x_gitlab_event = data_get($payload, 'object_kind'); + $allowed_events = ['push', 'merge_request']; + if (!in_array($x_gitlab_event, $allowed_events)) { + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Event not allowed. Only push and merge_request events are allowed.', + ]); + return response($return_payloads); + } + if ($x_gitlab_event === 'push') { $branch = data_get($payload, 'ref'); $full_name = data_get($payload, 'project.path_with_namespace'); @@ -124,6 +133,7 @@ public function manual(Request $request) queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, + commit: data_get($payload, 'after', 'HEAD'), force_rebuild: false, is_webhook: true, ); @@ -173,6 +183,7 @@ public function manual(Request $request) application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, + commit: data_get($payload, 'object_attributes.last_commit.id', 'HEAD'), force_rebuild: false, is_webhook: true, git_type: 'gitlab' diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 1717c5d08..47319ac10 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Actions\Docker\GetContainersStatus; use App\Enums\ApplicationDeploymentStatus; use App\Enums\ProcessStatus; use App\Events\ApplicationStatusChanged; @@ -95,7 +96,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private ?string $buildTarget = null; private Collection $saved_outputs; private ?string $full_healthcheck_url = null; - private bool $custom_healthcheck_found = false; private string $serverUser = 'root'; private string $serverUserHomeDir = '/root'; @@ -107,6 +107,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private ?string $fullRepoUrl = null; private ?string $branch = null; + private ?string $coolify_variables = null; + public $tries = 1; public function __construct(int $application_deployment_queue_id) { @@ -303,7 +305,8 @@ private function post_deployment() { if ($this->server->isProxyShouldRun()) { - dispatch(new ContainerStatusJob($this->server)); + GetContainersStatus::dispatch($this->server); + // dispatch(new ContainerStatusJob($this->server)); } $this->next(ApplicationDeploymentStatus::FINISHED->value); if ($this->pull_request_id !== 0) { @@ -405,7 +408,7 @@ private function deploy_docker_compose_buildpack() ); } else { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true], ); } @@ -435,9 +438,9 @@ private function deploy_docker_compose_buildpack() } else { $this->write_deployment_configurations(); $server_workdir = $this->application->workdir(); - ray("SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"); + ray("{$this->coolify_variables} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"); $this->execute_remote_command( - ["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true], + ["{$this->coolify_variables} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true], ); } } else { @@ -448,7 +451,7 @@ private function deploy_docker_compose_buildpack() $this->write_deployment_configurations(); } else { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true], ); $this->write_deployment_configurations(); } @@ -710,10 +713,40 @@ private function check_image_locally_or_remotely() private function save_environment_variables() { $envs = collect([]); + $local_branch = $this->branch; + if ($this->pull_request_id !== 0) { + $local_branch = "pull/{$this->pull_request_id}/head"; + } + $sort = $this->application->settings->is_env_sorting_enabled; + if ($sort) { + $sorted_environment_variables = $this->application->environment_variables->sortBy('key'); + $sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('key'); + } else { + $sorted_environment_variables = $this->application->environment_variables->sortBy('id'); + $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"; - foreach ($this->application->environment_variables_preview as $env) { + // 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}"); + } + 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}"); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $envs->push("COOLIFY_BRANCH={$local_branch}"); + } + 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; @@ -734,20 +767,27 @@ private function save_environment_variables() if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { $envs->push("HOST=0.0.0.0"); } + } else { + $this->env_filename = ".env"; // Add SOURCE_COMMIT if not exists - if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) { + 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"); } } - $envs = $envs->sort(function ($a, $b) { - return strpos($a, '$') === false ? -1 : 1; - }); - } else { - $this->env_filename = ".env"; - foreach ($this->application->environment_variables as $env) { + if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { + $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://', ''); + $envs->push("COOLIFY_URL={$url}"); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $envs->push("COOLIFY_BRANCH={$local_branch}"); + } + foreach ($sorted_environment_variables as $env) { $real_value = $env->real_value; if ($env->version === '4.0.0-beta.239') { $real_value = $env->real_value; @@ -768,17 +808,6 @@ private function save_environment_variables() if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { $envs->push("HOST=0.0.0.0"); } - // 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"); - } - } - $envs = $envs->sort(function ($a, $b) { - return strpos($a, '$') === false ? -1 : 1; - }); } if ($envs->isEmpty()) { @@ -870,7 +899,7 @@ private function rolling_update() $this->write_deployment_configurations(); $this->server = $this->original_server; } - if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { + if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name) || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { $this->application_deployment_queue->addLogEntry("----------------------------------------"); if (count($this->application->ports_mappings_array) > 0) { $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); @@ -878,6 +907,9 @@ private function rolling_update() if ((bool) $this->application->settings->is_consistent_container_name_enabled) { $this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported."); } + if (isset($this->application->settings->custom_internal_name)) { + $this->application_deployment_queue->addLogEntry("Custom internal name is set, rolling update is not supported."); + } if ($this->pull_request_id !== 0) { $this->application->settings->is_consistent_container_name_enabled = true; $this->application_deployment_queue->addLogEntry("Pull request deployment, rolling update is not supported."); @@ -903,10 +935,13 @@ private function health_check() if ($this->server->isSwarm()) { // Implement healthcheck for swarm } else { - if ($this->application->isHealthcheckDisabled() && $this->custom_healthcheck_found === false) { + if ($this->application->isHealthcheckDisabled() && $this->application->custom_healthcheck_found === false) { $this->newVersionIsHealthy = true; return; } + if ($this->application->custom_healthcheck_found) { + $this->application_deployment_queue->addLogEntry("Custom healthcheck found, skipping default healthcheck."); + } // ray('New container name: ', $this->container_name); if ($this->container_name) { $counter = 1; @@ -914,6 +949,12 @@ private function health_check() if ($this->full_healthcheck_url) { $this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}"); } + $this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck."); + $sleeptime = 0; + while ($sleeptime < $this->application->health_check_start_period) { + Sleep::for(1)->seconds(); + $sleeptime++; + } while ($counter <= $this->application->health_check_retries) { $this->execute_remote_command( [ @@ -922,9 +963,23 @@ private function health_check() "save" => "health_check", "append" => false ], - + [ + "docker inspect --format='{{json .State.Health.Log}}' {$this->container_name}", + "hidden" => true, + "save" => "health_check_logs", + "append" => false + ], ); $this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}"); + $health_check_logs = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'Output', '(no logs)'); + if (empty($health_check_logs)) { + $health_check_logs = '(no logs)'; + } + $health_check_return_code = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'ExitCode', '(no return code)'); + if ($health_check_logs !== '(no logs)' || $health_check_return_code !== '(no return code)') { + $this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}"); + } + if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') { $this->newVersionIsHealthy = true; $this->application->update(['status' => 'running']); @@ -936,7 +991,11 @@ private function health_check() break; } $counter++; - Sleep::for($this->application->health_check_interval)->seconds(); + $sleeptime = 0; + while ($sleeptime < $this->application->health_check_interval) { + Sleep::for(1)->seconds(); + $sleeptime++; + } } } } @@ -954,6 +1013,7 @@ private function deploy_pull_request() $this->generate_image_names(); $this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->prepare_builder_image(); + $this->check_git_if_build_needed(); $this->clone_repository(); $this->set_base_dir(); $this->cleanup_git(); @@ -1005,7 +1065,9 @@ private function prepare_builder_image() "command" => "docker rm -f {$this->deployment_uuid}", "ignore_errors" => true, "hidden" => true - ], + ] + ); + $this->execute_remote_command( [ $runCommand, "hidden" => true, @@ -1061,9 +1123,30 @@ private function set_base_dir() { $this->application_deployment_queue->addLogEntry("Setting base directory to {$this->workdir}."); } + private function set_coolify_variables() + { + $this->coolify_variables = "SOURCE_COMMIT={$this->commit} "; + if ($this->pull_request_id === 0) { + $fqdn = $this->application->fqdn; + } else { + $fqdn = $this->preview->fqdn; + } + if (isset($fqdn)) { + $this->coolify_variables .= "COOLIFY_FQDN={$fqdn} "; + $url = str($fqdn)->replace('http://', '')->replace('https://', ''); + $this->coolify_variables .= "COOLIFY_URL={$url} "; + } + if (isset($this->application->git_branch)) { + $this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} "; + } + } private function check_git_if_build_needed() { $this->generate_git_import_commands(); + $local_branch = $this->branch; + if ($this->pull_request_id !== 0) { + $local_branch = "pull/{$this->pull_request_id}/head"; + } $private_key = data_get($this->application, 'private_key.private_key'); if ($private_key) { $private_key = base64_encode($private_key); @@ -1078,7 +1161,7 @@ private function check_git_if_build_needed() executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa") ], [ - executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$this->branch}"), + executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"), "hidden" => true, "save" => "git_commit_sha" ], @@ -1086,15 +1169,19 @@ private function check_git_if_build_needed() } else { $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$this->branch}"), + executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"), "hidden" => true, "save" => "git_commit_sha" ], ); } + ray("GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"); if ($this->saved_outputs->get('git_commit_sha') && !$this->rollback) { $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t"); + $this->application_deployment_queue->commit = $this->commit; + $this->application_deployment_queue->save(); } + $this->set_coolify_variables(); } private function clone_repository() { @@ -1104,12 +1191,29 @@ private function clone_repository() if ($this->pull_request_id !== 0) { $this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head."); } + ray($importCommands); $this->execute_remote_command( [ $importCommands, "hidden" => true ] ); $this->create_workdir(); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git log -1 {$this->commit} --pretty=%B"), + "hidden" => true, + "save" => "commit_message" + ] + ); + ray($this->saved_outputs->get('commit_message')); + raY($this->commit); + if ($this->saved_outputs->get('commit_message')) { + $commit_message = str($this->saved_outputs->get('commit_message'))->limit(47); + $this->application_deployment_queue->commit_message = $commit_message->value(); + ApplicationDeploymentQueue::whereCommit($this->commit)->whereApplicationId($this->application->id)->update( + ['commit_message' => $commit_message->value()] + ); + } } private function generate_git_import_commands() @@ -1197,6 +1301,7 @@ private function generate_nixpacks_env_variables() private function generate_env_variables() { $this->env_args = collect([]); + $this->env_args->put('SOURCE_COMMIT', $this->commit); if ($this->pull_request_id === 0) { foreach ($this->application->build_environment_variables as $env) { if (!is_null($env->real_value)) { @@ -1210,7 +1315,6 @@ private function generate_env_variables() } } } - $this->env_args->put('SOURCE_COMMIT', $this->commit); } private function generate_compose_file() @@ -1258,23 +1362,22 @@ private function generate_compose_file() if ($this->pull_request_id !== 0) { $labels = collect(generateLabelsApplication($this->application, $this->preview)); } - $labels = $labels->map(function ($value, $key) { - return escapeDollarSign($value); - }); + if ($this->application->settings->is_container_label_escape_enabled) { + $labels = $labels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); + // Check for custom HEALTHCHECK - $this->custom_healthcheck_found = false; if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile_from_repo', "ignore_errors" => true ]); $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n")); - if (str($dockerfile)->contains('HEALTHCHECK')) { - $this->custom_healthcheck_found = true; - } + $this->application->parseHealthcheckFromDockerfile($dockerfile); } $docker_compose = [ - 'version' => '3.8', 'services' => [ $this->container_name => [ 'image' => $this->production_image_name, @@ -1282,7 +1385,11 @@ private function generate_compose_file() 'restart' => RESTART_MODE, 'expose' => $ports, 'networks' => [ - $this->destination->network, + $this->destination->network => [ + 'aliases' => [ + $this->container_name + ] + ] ], 'mem_limit' => $this->application->limits_memory, 'memswap_limit' => $this->application->limits_memory_swap, @@ -1300,6 +1407,9 @@ private function generate_compose_file() ] ] ]; + if (isset($this->application->settings->custom_internal_name)) { + $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['aliases'][] = $this->application->settings->custom_internal_name; + } // if (str($this->saved_outputs->get('dotenv'))->isNotEmpty()) { // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) { // $docker_compose['services'][$this->container_name]['env_file'][] = '.env'; @@ -1317,18 +1427,17 @@ private function generate_compose_file() if (!is_null($this->env_filename)) { $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; } - if (!$this->custom_healthcheck_found) { - $docker_compose['services'][$this->container_name]['healthcheck'] = [ - 'test' => [ - 'CMD-SHELL', - $this->generate_healthcheck_commands() - ], - 'interval' => $this->application->health_check_interval . 's', - 'timeout' => $this->application->health_check_timeout . 's', - 'retries' => $this->application->health_check_retries, - 'start_period' => $this->application->health_check_start_period . 's' - ]; - } + $docker_compose['services'][$this->container_name]['healthcheck'] = [ + 'test' => [ + 'CMD-SHELL', + $this->generate_healthcheck_commands() + ], + 'interval' => $this->application->health_check_interval . 's', + 'timeout' => $this->application->health_check_timeout . 's', + 'retries' => $this->application->health_check_retries, + 'start_period' => $this->application->health_check_start_period . 's' + ]; + if (!is_null($this->application->limits_cpuset)) { data_set($docker_compose, 'services.' . $this->container_name . '.cpuset', $this->application->limits_cpuset); } @@ -1510,95 +1619,8 @@ private function generate_local_persistent_volumes_only_volume_names() return $local_persistent_volumes_names; } - /*private function generate_environment_variables($ports) - { - $environment_variables = collect(); - if ($this->pull_request_id === 0) { - foreach ($this->application->runtime_environment_variables as $env) { - // This is necessary because we have to escape the value of the environment variable - // but only if the environment variable is created after 4.0.0-beta.240 - // when I implemented the escaping feature. - - // Old environment variables are not escaped, because it could break the application - // as the application could expect the unescaped value. - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - if ($env->is_literal) { - $real_value = escapeDollarSign($real_value); - $environment_variables->push("$env->key='$real_value'"); - } else { - $environment_variables->push("$env->key=$real_value"); - } - } - foreach ($this->application->nixpacks_environment_variables as $env) { - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - if ($env->is_literal) { - $real_value = escapeDollarSign($real_value); - $environment_variables->push("$env->key='$real_value'"); - } else { - $environment_variables->push("$env->key=$real_value"); - } - } - } else { - foreach ($this->application->runtime_environment_variables_preview as $env) { - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - if ($env->is_literal) { - $real_value = escapeDollarSign($real_value); - $environment_variables->push("$env->key='$real_value'"); - } else { - $environment_variables->push("$env->key=$real_value"); - } - } - foreach ($this->application->nixpacks_environment_variables_preview as $env) { - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - if ($env->is_literal) { - $real_value = escapeDollarSign($real_value); - $environment_variables->push("$env->key='$real_value'"); - } else { - $environment_variables->push("$env->key=$real_value"); - } - } - } - // Add PORT if not exists, use the first port as default - if ($environment_variables->filter(fn ($env) => Str::of($env)->startsWith('PORT'))->isEmpty()) { - $environment_variables->push("PORT={$ports[0]}"); - } - // Add HOST if not exists - if ($environment_variables->filter(fn ($env) => Str::of($env)->startsWith('HOST'))->isEmpty()) { - $environment_variables->push("HOST=0.0.0.0"); - } - if ($environment_variables->filter(fn ($env) => Str::of($env)->startsWith('SOURCE_COMMIT'))->isEmpty()) { - if (!is_null($this->commit)) { - $environment_variables->push("SOURCE_COMMIT={$this->commit}"); - } else { - $environment_variables->push("SOURCE_COMMIT=unknown"); - } - } - ray($environment_variables->all()); - return $environment_variables->all(); - }*/ - private function generate_healthcheck_commands() { - if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') { - // TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl. - return 'exit 0'; - } if (!$this->application->health_check_port) { $health_check_port = $this->application->ports_exposes_array[0]; } else { @@ -1610,12 +1632,12 @@ private function generate_healthcheck_commands() if ($this->application->health_check_path) { $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}"; $generated_healthchecks_commands = [ - "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null" + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || exit 1" ]; } else { $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"; $generated_healthchecks_commands = [ - "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/" + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || exit 1" ]; } return implode(' ', $generated_healthchecks_commands); @@ -1804,12 +1826,17 @@ private function stop_running_container(bool $force = false) ["docker rm -f $containerName >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true], ); }); - if ($this->application->settings->is_consistent_container_name_enabled) { + if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) { $this->execute_remote_command( ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true], ); } } else { + if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') { + $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry("WARNING: Dockerfile or Docker Image based deployment detected. The healthcheck needs a curl or wget command to check the health of the application. Please make sure that it is available in the image or turn off healthcheck on Coolify's UI."); + $this->application_deployment_queue->addLogEntry("----------------------------------------"); + } $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container."); $this->application_deployment_queue->update([ 'status' => ApplicationDeploymentStatus::FAILED->value, @@ -1827,11 +1854,11 @@ private function build_by_compose_file() $this->application_deployment_queue->addLogEntry("Pulling latest images from the registry."); $this->execute_remote_command( [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], - [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} build"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), "hidden" => true], ); } else { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true], ); } $this->application_deployment_queue->addLogEntry("New images built."); @@ -1843,16 +1870,16 @@ private function start_by_compose_file() $this->application_deployment_queue->addLogEntry("Pulling latest images from the registry."); $this->execute_remote_command( [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], - [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], ); } else { if ($this->use_build_server) { $this->execute_remote_command( - ["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true], + ["{$this->coolify_variables} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true], ); } else { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true], ); } } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index a9cec009b..11e7013ee 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -2,15 +2,8 @@ namespace App\Jobs; -use App\Actions\Database\StartDatabaseProxy; -use App\Actions\Proxy\CheckProxy; -use App\Actions\Proxy\StartProxy; -use App\Actions\Shared\ComplexStatusCheck; -use App\Models\ApplicationPreview; +use App\Actions\Docker\GetContainersStatus; use App\Models\Server; -use App\Models\ServiceDatabase; -use App\Notifications\Container\ContainerRestarted; -use App\Notifications\Container\ContainerStopped; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; @@ -18,7 +11,6 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Arr; class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted { @@ -44,335 +36,6 @@ public function uniqueId(): int public function handle() { - if (!$this->server->isFunctional()) { - return 'Server is not ready.'; - }; - $applications = $this->server->applications(); - $skip_these_applications = collect([]); - foreach ($applications as $application) { - if ($application->additional_servers->count() > 0) { - $skip_these_applications->push($application); - ComplexStatusCheck::run($application); - $applications = $applications->filter(function ($value, $key) use ($application) { - return $value->id !== $application->id; - }); - } - } - $applications = $applications->filter(function ($value, $key) use ($skip_these_applications) { - return !$skip_these_applications->pluck('id')->contains($value->id); - }); - try { - if ($this->server->isSwarm()) { - $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); - $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false); - } else { - // Precheck for containers - $containers = instant_remote_process(["docker container ls -q"], $this->server, false); - if (!$containers) { - return; - } - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); - $containerReplicates = null; - } - if (is_null($containers)) { - return; - } - - $containers = format_docker_command_output_to_json($containers); - if ($containerReplicates) { - $containerReplicates = format_docker_command_output_to_json($containerReplicates); - foreach ($containerReplicates as $containerReplica) { - $name = data_get($containerReplica, 'Name'); - $containers = $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; - }); - } - } - $databases = $this->server->databases(); - $services = $this->server->services()->get(); - $previews = $this->server->previews(); - $foundApplications = []; - $foundApplicationPreviews = []; - $foundDatabases = []; - $foundServices = []; - - foreach ($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 = $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 = $service_db->service->uuid; - $isPublic = data_get($service_db, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $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 = $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 = $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 = $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 ($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'); - $containerName = $name ? "$name, available at $fqdn" : $fqdn; - $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 = $applications->pluck('id')->diff($foundApplications); - foreach ($notRunningApplications as $applicationId) { - $application = $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 = $previews->pluck('id')->diff($foundApplicationPreviews); - foreach ($notRunningApplicationPreviews as $previewId) { - $preview = $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 = $databases->pluck('id')->diff($foundDatabases); - foreach ($notRunningDatabases as $database) { - $database = $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 = $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); - } - } catch (\Throwable $e) { - send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage()); - ray($e->getMessage()); - return handleError($e); - } + GetContainersStatus::run($this->server); } } diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index befeffed0..ed9694536 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -289,7 +289,7 @@ public function handle(): void if ($this->backup->save_s3) { $this->upload_to_s3(); } - $this->team?->notify(new BackupSuccess($this->backup, $this->database)); + $this->team?->notify(new BackupSuccess($this->backup, $this->database, $database)); $this->backup_log->update([ 'status' => 'success', 'message' => $this->backup_output, @@ -305,8 +305,7 @@ public function handle(): void ]); } send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); - $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output)); - throw $e; + $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database)); } } } catch (\Throwable $e) { @@ -319,10 +318,15 @@ public function handle(): void private function backup_standalone_mongodb(string $databaseWithCollections): void { try { + ray($this->database->toArray()); $url = $this->database->get_db_url(useInternal: true); if ($databaseWithCollections === 'all') { $commands[] = "mkdir -p " . $this->backup_dir; - $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location"; + if (str($this->database->image)->startsWith('mongo:4.0')) { + $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location"; + } else { + $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location"; + } } else { if (str($databaseWithCollections)->contains(':')) { $databaseName = str($databaseWithCollections)->before(':'); @@ -333,9 +337,17 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi } $commands[] = "mkdir -p " . $this->backup_dir; if ($collectionsToExclude->count() === 0) { - $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location"; + if (str($this->database->image)->startsWith('mongo:4.0')) { + $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location"; + } else { + $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location"; + } } else { - $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location"; + if (str($this->database->image)->startsWith('mongo:4.0')) { + $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location"; + } else { + $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location"; + } } } $this->backup_output = instant_remote_process($commands, $this->server); diff --git a/app/Jobs/InstanceAutoUpdateJob.php b/app/Jobs/InstanceAutoUpdateJob.php index fa5c29421..dc35aa2b1 100644 --- a/app/Jobs/InstanceAutoUpdateJob.php +++ b/app/Jobs/InstanceAutoUpdateJob.php @@ -15,7 +15,8 @@ class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncr { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public $timeout = 120; + public $timeout = 600; + public $tries = 1; public function __construct(private bool $force = false) { diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php new file mode 100644 index 000000000..1c51928f6 --- /dev/null +++ b/app/Jobs/PullSentinelImageJob.php @@ -0,0 +1,56 @@ +server->uuid))]; + } + + public function uniqueId(): string + { + return $this->server->uuid; + } + public function __construct(public Server $server) + { + } + public function handle(): void + { + try { + $version = get_latest_sentinel_version(); + if (!$version) { + ray('Failed to get latest Sentinel version'); + return; + } + $local_version = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false); + if (empty($local_version)) { + $local_version = '0.0.0'; + } + if (version_compare($local_version, $version, '<')) { + StartSentinel::run($this->server, $version, true); + return; + } + ray('Sentinel image is up to date'); + } catch (\Throwable $e) { + send_internal_notification('PullSentinelImageJob failed with: ' . $e->getMessage()); + ray($e->getMessage()); + throw $e; + } + } +} diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 4a38a005b..a28f85901 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -8,14 +8,13 @@ use App\Models\Application; use App\Models\Service; use App\Models\Team; +use App\Notifications\ScheduledTask\TaskFailed; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Collection; -use Throwable; class ScheduledTaskJob implements ShouldQueue { @@ -77,8 +76,12 @@ public function handle(): void $this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'); } }); + $this->resource->databases()->get()->each(function ($database) { + if (str(data_get($database, 'status'))->contains('running')) { + $this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'); + } + }); } - if (count($this->containers) == 0) { throw new \Exception('ScheduledTaskJob failed: No containers running.'); } @@ -89,7 +92,7 @@ public function handle(): void foreach ($this->containers as $containerName) { if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) { - $cmd = 'sh -c "' . str_replace('"', '\"', $this->task->command) . '"'; + $cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'"; $exec = "docker exec {$containerName} {$cmd}"; $this->task_output = instant_remote_process([$exec], $this->server, true); $this->task_log->update([ @@ -110,6 +113,7 @@ public function handle(): void 'message' => $this->task_output ?? $e->getMessage(), ]); } + $this->team?->notify(new TaskFailed($this->task, $e->getMessage())); // send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage()); throw $e; } diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php index b9255baaa..ddd6bd271 100644 --- a/app/Jobs/SendMessageToDiscordJob.php +++ b/app/Jobs/SendMessageToDiscordJob.php @@ -20,11 +20,12 @@ class SendMessageToDiscordJob implements ShouldQueue, ShouldBeEncrypted * @var int */ public $tries = 5; + public $backoff = 10; /** * The maximum number of unhandled exceptions to allow before failing. */ - public int $maxExceptions = 3; + public int $maxExceptions = 5; public function __construct( public string $text, @@ -40,7 +41,6 @@ public function handle(): void $payload = [ 'content' => $this->text, ]; - ray($payload); Http::post($this->webhookUrl, $payload); } } diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php index 4785da669..4191b02fe 100644 --- a/app/Jobs/SendMessageToTelegramJob.php +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -57,7 +57,7 @@ public function handle(): void } } $payload = [ - 'parse_mode' => 'markdown', + // 'parse_mode' => 'markdown', 'reply_markup' => json_encode([ 'inline_keyboard' => [ [...$inlineButtons], diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php index 052260895..9d0e5db94 100644 --- a/app/Jobs/ServerLimitCheckJob.php +++ b/app/Jobs/ServerLimitCheckJob.php @@ -40,7 +40,7 @@ public function handle() try { $servers = $this->team->servers; $servers_count = $servers->count(); - $limit = $this->team->limits['serverLimit']; + $limit = data_get($this->team->limits, 'serverLimit', 2); $number_of_servers_to_disable = $servers_count - $limit; ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable); if ($number_of_servers_to_disable > 0) { diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php index 31683d097..449ab85a0 100644 --- a/app/Jobs/ServerStatusJob.php +++ b/app/Jobs/ServerStatusJob.php @@ -17,7 +17,7 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int|string|null $disk_usage = null; - public $tries = 4; + public $tries = 3; public function backoff(): int { return isDev() ? 1 : 3; @@ -43,6 +43,10 @@ public function handle() try { if ($this->server->isFunctional()) { $this->cleanup(notify: false); + $this->removeCoolifyYaml(); + if (config('coolify.is_sentinel_enabled')) { + $this->server->checkSentinel(); + } } } catch (\Throwable $e) { send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage()); @@ -50,6 +54,16 @@ public function handle() return handleError($e); } } + private function removeCoolifyYaml() + { + // 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); + } + } public function cleanup(bool $notify = false): void { $this->disk_usage = $this->server->getDiskUsage(); diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 2681b69e0..8f4e87090 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -52,6 +52,9 @@ class Index extends Component public function mount() { + if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) { + return redirect()->route('dashboard'); + } $this->privateKeyName = generate_random_name(); $this->remoteServerName = generate_random_name(); if (isDev()) { diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index 8aad8ccf0..88705437b 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -16,6 +16,7 @@ class Discord extends Component 'team.discord_notifications_deployments' => 'nullable|boolean', 'team.discord_notifications_status_changes' => 'nullable|boolean', 'team.discord_notifications_database_backups' => 'nullable|boolean', + 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean', ]; protected $validationAttributes = [ 'team.discord_webhook_url' => 'Discord Webhook', diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 343cbda3e..6ef9b2255 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -28,6 +28,7 @@ class Email extends Component 'team.smtp_notifications_deployments' => 'nullable|boolean', 'team.smtp_notifications_status_changes' => 'nullable|boolean', 'team.smtp_notifications_database_backups' => 'nullable|boolean', + 'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean', 'team.use_instance_email_settings' => 'boolean', 'team.resend_enabled' => 'nullable|boolean', 'team.resend_api_key' => 'nullable', diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index 35b868527..685c9e8eb 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -18,10 +18,12 @@ class Telegram extends Component 'team.telegram_notifications_deployments' => 'nullable|boolean', 'team.telegram_notifications_status_changes' => 'nullable|boolean', 'team.telegram_notifications_database_backups' => 'nullable|boolean', + 'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean', 'team.telegram_notifications_test_message_thread_id' => 'nullable|string', 'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string', 'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string', 'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string', + 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string', ]; protected $validationAttributes = [ 'team.telegram_token' => 'Token', diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index d35867e8f..45cb57ee3 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -21,6 +21,7 @@ class Advanced extends Component 'application.settings.is_gpu_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_consistent_container_name_enabled' => 'boolean|required', + 'application.settings.custom_internal_name' => 'string|nullable', 'application.settings.is_gzip_enabled' => 'boolean|required', 'application.settings.is_stripprefix_enabled' => 'boolean|required', 'application.settings.gpu_driver' => 'string|required', @@ -30,7 +31,8 @@ class Advanced extends Component 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', 'application.settings.connect_to_docker_network' => 'boolean|required', ]; - public function mount() { + public function mount() + { $this->is_force_https_enabled = $this->application->isForceHttpsEnabled(); $this->is_gzip_enabled = $this->application->isGzipEnabled(); $this->is_stripprefix_enabled = $this->application->isStripprefixEnabled(); @@ -65,7 +67,8 @@ public function instantSave() $this->dispatch('success', 'Settings saved.'); $this->dispatch('configurationChanged'); } - public function submit() { + public function submit() + { if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) { $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.'); $this->application->settings->gpu_count = null; @@ -76,6 +79,16 @@ public function submit() { $this->application->settings->save(); $this->dispatch('success', 'Settings saved.'); } + public function saveCustomName() + { + if (isset($this->application->settings->custom_internal_name)) { + $this->application->settings->custom_internal_name = str($this->application->settings->custom_internal_name)->slug()->value(); + } else { + $this->application->settings->custom_internal_name = null; + } + $this->application->settings->save(); + $this->dispatch('success', 'Custom name saved.'); + } public function render() { return view('livewire.project.application.advanced'); diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 6926e52cb..718312d2d 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -22,6 +22,7 @@ class General extends Component public ?string $git_commit_sha = null; public string $build_pack; public ?string $ports_exposes = null; + public bool $is_container_label_escape_enabled = true; public $customLabels; public bool $labelsChanged = false; @@ -30,7 +31,7 @@ class General extends Component public ?string $initialDockerComposeLocation = null; public ?string $initialDockerComposePrLocation = null; - public $parsedServices = []; + public null|Collection $parsedServices; public $parsedServiceDomains = []; protected $listeners = [ @@ -74,6 +75,7 @@ class General extends Component 'application.post_deployment_command_container' => 'nullable', 'application.settings.is_static' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required', + 'application.settings.is_container_label_escape_enabled' => 'boolean|required', 'application.watch_paths' => 'nullable', ]; protected $validationAttributes = [ @@ -109,12 +111,17 @@ class General extends Component 'application.docker_compose_custom_build_command' => 'Docker compose custom build command', 'application.settings.is_static' => 'Is static', 'application.settings.is_build_server_enabled' => 'Is build server enabled', + 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled', 'application.watch_paths' => 'Watch paths', ]; public function mount() { try { $this->parsedServices = $this->application->parseCompose(); + if (is_null($this->parsedServices) || empty($this->parsedServices)) { + $this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again."); + return; + } } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); } @@ -124,6 +131,7 @@ public function mount() } $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; $this->ports_exposes = $this->application->ports_exposes; + $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->customLabels = $this->application->parseContainerLabels(); if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); @@ -145,7 +153,7 @@ public function instantSave() $this->application->settings->save(); $this->dispatch('success', 'Settings saved.'); $this->application->refresh(); - if ($this->ports_exposes !== $this->application->ports_exposes) { + if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) { $this->resetDefaultLabels(false); } } @@ -156,6 +164,10 @@ public function loadComposeFile($isInit = false) return; } ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit); + if (is_null($this->parsedServices)) { + $this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again."); + return; + } $compose = $this->application->parseCompose(); $services = data_get($compose, 'services'); if ($services) { @@ -186,6 +198,7 @@ public function loadComposeFile($isInit = false) $this->dispatch('success', 'Docker compose file loaded.'); $this->dispatch('compose_loaded'); $this->dispatch('refresh_storages'); + $this->dispatch('refreshEnvs'); } catch (\Throwable $e) { $this->application->docker_compose_location = $this->initialDockerComposeLocation; $this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation; @@ -203,6 +216,9 @@ public function generateDomain(string $serviceName) $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); $this->application->save(); $this->dispatch('success', 'Domain generated.'); + if ($this->application->build_pack === 'dockercompose') { + $this->loadComposeFile(); + } return $domain; } public function updatedApplicationBaseDirectory() @@ -254,12 +270,14 @@ public function getWildcardDomain() } public function resetDefaultLabels() { - ray('resetDefaultLabels'); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->ports_exposes = $this->application->ports_exposes; - + $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); + if ($this->application->build_pack === 'dockercompose') { + $this->loadComposeFile(); + } } public function checkFqdns($showToaster = true) @@ -298,10 +316,13 @@ public function submit($showToaster = true) } if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) { - $this->loadComposeFile(); + $compose_return = $this->loadComposeFile(); + if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) { + return; + } } $this->validate(); - if ($this->ports_exposes !== $this->application->ports_exposes) { + if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) { $this->resetDefaultLabels(); } if (data_get($this->application, 'build_pack') === 'dockerimage') { diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 0717a51f0..619be693d 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -3,8 +3,8 @@ namespace App\Livewire\Project\Application; use App\Actions\Application\StopApplication; +use App\Actions\Docker\GetContainersStatus; use App\Events\ApplicationStatusChanged; -use App\Jobs\ComplexContainerStatusJob; use App\Jobs\ContainerStatusJob; use App\Jobs\ServerStatusJob; use App\Models\Application; @@ -14,6 +14,8 @@ class Heading extends Component { public Application $application; + public ?string $lastDeploymentInfo = null; + public ?string $lastDeploymentLink = null; public array $parameters; protected string $deploymentUuid; @@ -28,18 +30,23 @@ public function getListeners() public function mount() { $this->parameters = get_route_parameters(); + $lastDeployment = $this->application->get_last_successful_deployment(); + $this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7) . ' ' . data_get($lastDeployment, 'commit_message'); + $this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit')); } public function check_status($showNotification = false) { if ($this->application->destination->server->isFunctional()) { - dispatch(new ContainerStatusJob($this->application->destination->server)); + GetContainersStatus::dispatch($this->application->destination->server); + // dispatch(new ContainerStatusJob($this->application->destination->server)); } else { dispatch(new ServerStatusJob($this->application->destination->server)); } if ($showNotification) $this->dispatch('success', "Success", "Application status updated."); - $this->dispatch('configurationChanged'); + // Removed because it caused flickering + // $this->dispatch('configurationChanged'); } public function force_deploy_without_cache() diff --git a/app/Livewire/Project/Database/Backup/Execution.php b/app/Livewire/Project/Database/Backup/Execution.php index 1f790d643..000b6fb2b 100644 --- a/app/Livewire/Project/Database/Backup/Execution.php +++ b/app/Livewire/Project/Database/Backup/Execution.php @@ -35,11 +35,6 @@ public function mount() $this->executions = $executions; $this->s3s = currentTeam()->s3s; } - public function cleanupFailed() - { - $this->backup->executions()->where('status', 'failed')->delete(); - $this->dispatch('refreshBackupExecutions'); - } public function render() { return view('livewire.project.database.backup.execution'); diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index f5f476257..d7f7f5503 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -35,7 +35,7 @@ class BackupEdit extends Component public function mount() { $this->parameters = get_route_parameters(); - if (is_null($this->backup->s3_storage_id)) { + if (is_null(data_get($this->backup, 's3_storage_id'))) { $this->backup->s3_storage_id = 'default'; } } diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index b127a685c..101bb4593 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -2,9 +2,7 @@ namespace App\Livewire\Project\Database; -use Illuminate\Support\Facades\Storage; use Livewire\Component; -use Symfony\Component\HttpFoundation\StreamedResponse; class BackupExecutions extends Component { @@ -16,11 +14,15 @@ public function getListeners() $userId = auth()->user()->id; return [ "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions', - "refreshBackupExecutions", "deleteBackup" ]; } + public function cleanupFailed() + { + $this->backup?->executions()->where('status', 'failed')->delete(); + $this->refreshBackupExecutions(); + } public function deleteBackup($exeuctionId) { $execution = $this->backup->executions()->where('id', $exeuctionId)->first(); diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 960ff2689..d6a0fe087 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -11,6 +11,7 @@ use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartRedis; use App\Actions\Database\StopDatabase; +use App\Actions\Docker\GetContainersStatus; use App\Jobs\ContainerStatusJob; use Livewire\Component; @@ -44,7 +45,8 @@ public function activityFinished() public function check_status($showNotification = false) { - dispatch_sync(new ContainerStatusJob($this->database->destination->server)); + GetContainersStatus::run($this->database->destination->server); + // dispatch_sync(new ContainerStatusJob($this->database->destination->server)); $this->database->refresh(); if ($showNotification) $this->dispatch('success', 'Database status updated.'); } diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index 74e41056a..d435289fa 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -27,6 +27,7 @@ class Import extends Component public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB'; public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'; public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE'; + public string $mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive='; public function getListeners() { @@ -62,8 +63,7 @@ public function getContainers() $this->resource->getMorphClass() == 'App\Models\StandaloneRedis' || $this->resource->getMorphClass() == 'App\Models\StandaloneKeydb' || $this->resource->getMorphClass() == 'App\Models\StandaloneDragonfly' || - $this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse' || - $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb' + $this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse' ) { $this->unsupported = true; } @@ -101,6 +101,10 @@ public function runImport() $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; + case 'App\Models\StandaloneMongodb': + $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mongodbRestoreCommand}{$tmpPath}'"; + $this->importCommands[] = "rm {$tmpPath}"; + break; } $this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'"; diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 322fd4a4e..58e3fe586 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -150,7 +150,7 @@ public function submit() 'repository_project_id' => $this->selected_repository_id, 'git_repository' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}", 'git_branch' => $this->selected_branch_name, - 'build_pack' => 'nixpacks', + 'build_pack' => $this->build_pack, 'ports_exposes' => $this->port, 'publish_directory' => $this->publish_directory, 'environment_id' => $environment->id, @@ -162,6 +162,9 @@ public function submit() $application->settings->is_static = $this->is_static; $application->settings->save(); + if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') { + $application->health_check_enabled = false; + } $fqdn = generateFqdn($destination->server, $application->uuid); $application->fqdn = $fqdn; diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index ad52b9070..691b246fd 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -19,7 +19,7 @@ class GithubPrivateRepositoryDeployKey extends Component public $current_step = 'private_keys'; public $parameters; public $query; - public $private_keys =[]; + public $private_keys = []; public int $private_key_id; public int $port = 3000; @@ -125,7 +125,7 @@ public function submit() 'name' => generate_random_name(), 'git_repository' => $this->git_repository, 'git_branch' => $this->branch, - 'build_pack' => 'nixpacks', + 'build_pack' => $this->build_pack, 'ports_exposes' => $this->port, 'publish_directory' => $this->publish_directory, 'environment_id' => $environment->id, @@ -138,7 +138,7 @@ public function submit() 'name' => generate_random_name(), 'git_repository' => $this->git_repository, 'git_branch' => $this->branch, - 'build_pack' => 'nixpacks', + 'build_pack' => $this->build_pack, 'ports_exposes' => $this->port, 'publish_directory' => $this->publish_directory, 'environment_id' => $environment->id, @@ -149,7 +149,9 @@ public function submit() 'source_type' => $this->git_source->getMorphClass() ]; } - + if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') { + $application_init['health_check_enabled'] = false; + } $application = Application::create($application_init); $application->settings->is_static = $this->is_static; $application->settings->save(); diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 8bbb5b052..f4f3008d4 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -94,6 +94,18 @@ public function load_branch() $repository = str($this->repository_url)->after(':')->before('.git'); $this->repository_url = 'https://' . str($github_instance) . '/' . $repository; } + if ( + (str($this->repository_url)->startsWith('https://') || + str($this->repository_url)->startsWith('http://')) && + !str($this->repository_url)->endsWith('.git') && + (!str($this->repository_url)->contains('github.com') || + !str($this->repository_url)->contains('git.sr.ht')) + ) { + $this->repository_url = $this->repository_url . '.git'; + } + if (str($this->repository_url)->contains('github.com')) { + $this->repository_url = str($this->repository_url)->before('.git')->value(); + } } catch (\Throwable $e) { return handleError($e, $this); } @@ -170,7 +182,6 @@ public function submit() 'name' => generate_random_name(), 'git_repository' => $this->git_repository, 'git_branch' => $this->git_branch, - 'build_pack' => 'nixpacks', 'ports_exposes' => $this->port, 'publish_directory' => $this->publish_directory, 'environment_id' => $environment->id, @@ -183,7 +194,6 @@ public function submit() 'name' => generate_application_name($this->git_repository, $this->git_branch), 'git_repository' => $this->git_repository, 'git_branch' => $this->git_branch, - 'build_pack' => 'nixpacks', 'ports_exposes' => $this->port, 'publish_directory' => $this->publish_directory, 'environment_id' => $environment->id, @@ -195,7 +205,9 @@ public function submit() ]; } - + if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') { + $application_init['health_check_enabled'] = false; + } $application = Application::create($application_init); $application->settings->is_static = $this->is_static; diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index 55b48041a..172403a1a 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -70,6 +70,8 @@ public function submit() 'fqdn' => $fqdn ]); + $application->parseHealthcheckFromDockerfile(dockerfile: collect(str($this->dockerfile)->trim()->explode("\n")), isInit: true); + return redirect()->route('project.application.configuration', [ 'application_uuid' => $application->uuid, 'environment_name' => $environment->name, diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index 3705d6f93..8ea77950e 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -12,7 +12,6 @@ class Create extends Component public $type; public function mount() { - $services = getServiceTemplates(); $type = str(request()->query('type')); $destination_uuid = request()->query('destination'); $server_id = request()->query('server_id'); @@ -25,83 +24,87 @@ public function mount() if (!$environment) { return redirect()->route('dashboard'); } - if (in_array($type, DATABASE_TYPES)) { - if ($type->value() === "postgresql") { - $database = create_standalone_postgresql($environment->id, $destination_uuid); - } else if ($type->value() === 'redis') { - $database = create_standalone_redis($environment->id, $destination_uuid); - } else if ($type->value() === 'mongodb') { - $database = create_standalone_mongodb($environment->id, $destination_uuid); - } else if ($type->value() === 'mysql') { - $database = create_standalone_mysql($environment->id, $destination_uuid); - } else if ($type->value() === 'mariadb') { - $database = create_standalone_mariadb($environment->id, $destination_uuid); - } else if ($type->value() === 'keydb') { - $database = create_standalone_keydb($environment->id, $destination_uuid); - } else if ($type->value() === 'dragonfly') { - $database = create_standalone_dragonfly($environment->id, $destination_uuid); - } else if ($type->value() === 'clickhouse') { - $database = create_standalone_clickhouse($environment->id, $destination_uuid); - } - return redirect()->route('project.database.configuration', [ - 'project_uuid' => $project->uuid, - 'environment_name' => $environment->name, - 'database_uuid' => $database->uuid, - ]); - } - if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) { - $oneClickServiceName = $type->after('one-click-service-')->value(); - $oneClickService = data_get($services, "$oneClickServiceName.compose"); - $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null); - if ($oneClickDotEnvs) { - $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) { - return !empty($value); - }); - } - if ($oneClickService) { - $destination = StandaloneDocker::whereUuid($destination_uuid)->first(); - $service_payload = [ - 'name' => "$oneClickServiceName-" . str()->random(10), - 'docker_compose_raw' => base64_decode($oneClickService), - 'environment_id' => $environment->id, - 'service_type' => $oneClickServiceName, - 'server_id' => (int) $server_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]; - if ($oneClickServiceName === 'cloudflared') { - data_set($service_payload, 'connect_to_docker_network', true); + if (isset($type) && isset($destination_uuid) && isset($server_id)) { + $services = getServiceTemplates(); + + if (in_array($type, DATABASE_TYPES)) { + if ($type->value() === "postgresql") { + $database = create_standalone_postgresql($environment->id, $destination_uuid); + } else if ($type->value() === 'redis') { + $database = create_standalone_redis($environment->id, $destination_uuid); + } else if ($type->value() === 'mongodb') { + $database = create_standalone_mongodb($environment->id, $destination_uuid); + } else if ($type->value() === 'mysql') { + $database = create_standalone_mysql($environment->id, $destination_uuid); + } else if ($type->value() === 'mariadb') { + $database = create_standalone_mariadb($environment->id, $destination_uuid); + } else if ($type->value() === 'keydb') { + $database = create_standalone_keydb($environment->id, $destination_uuid); + } else if ($type->value() === 'dragonfly') { + $database = create_standalone_dragonfly($environment->id, $destination_uuid); + } else if ($type->value() === 'clickhouse') { + $database = create_standalone_clickhouse($environment->id, $destination_uuid); } - $service = Service::create($service_payload); - $service->name = "$oneClickServiceName-" . $service->uuid; - $service->save(); - if ($oneClickDotEnvs?->count() > 0) { - $oneClickDotEnvs->each(function ($value) use ($service) { - $key = str()->before($value, '='); - $value = str(str()->after($value, '=')); - $generatedValue = $value; - if ($value->contains('SERVICE_')) { - $command = $value->after('SERVICE_')->beforeLast('_'); - $generatedValue = generateEnvValue($command->value(), $service); - } - EnvironmentVariable::create([ - 'key' => $key, - 'value' => $generatedValue, - 'service_id' => $service->id, - 'is_build_time' => false, - 'is_preview' => false, - ]); - }); - } - $service->parse(isNew: true); - return redirect()->route('project.service.configuration', [ - 'service_uuid' => $service->uuid, - 'environment_name' => $environment->name, + return redirect()->route('project.database.configuration', [ 'project_uuid' => $project->uuid, + 'environment_name' => $environment->name, + 'database_uuid' => $database->uuid, ]); } + if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) { + $oneClickServiceName = $type->after('one-click-service-')->value(); + $oneClickService = data_get($services, "$oneClickServiceName.compose"); + $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null); + if ($oneClickDotEnvs) { + $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) { + return !empty($value); + }); + } + if ($oneClickService) { + $destination = StandaloneDocker::whereUuid($destination_uuid)->first(); + $service_payload = [ + 'name' => "$oneClickServiceName-" . str()->random(10), + 'docker_compose_raw' => base64_decode($oneClickService), + 'environment_id' => $environment->id, + 'service_type' => $oneClickServiceName, + 'server_id' => (int) $server_id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]; + if ($oneClickServiceName === 'cloudflared') { + data_set($service_payload, 'connect_to_docker_network', true); + } + $service = Service::create($service_payload); + $service->name = "$oneClickServiceName-" . $service->uuid; + $service->save(); + if ($oneClickDotEnvs?->count() > 0) { + $oneClickDotEnvs->each(function ($value) use ($service) { + $key = str()->before($value, '='); + $value = str(str()->after($value, '=')); + $generatedValue = $value; + if ($value->contains('SERVICE_')) { + $command = $value->after('SERVICE_')->beforeLast('_'); + $generatedValue = generateEnvValue($command->value(), $service); + } + EnvironmentVariable::create([ + 'key' => $key, + 'value' => $generatedValue, + 'service_id' => $service->id, + 'is_build_time' => false, + 'is_preview' => false, + ]); + }); + } + $service->parse(isNew: true); + return redirect()->route('project.service.configuration', [ + 'service_uuid' => $service->uuid, + 'environment_name' => $environment->name, + 'project_uuid' => $project->uuid, + ]); + } + } + $this->type = $type->value(); } - $this->type = $type->value(); } public function render() { diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index 2cbda4e02..86c9a8a31 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -2,7 +2,7 @@ namespace App\Livewire\Project\Service; -use App\Jobs\ContainerStatusJob; +use App\Actions\Docker\GetContainersStatus; use App\Models\Service; use Livewire\Component; @@ -64,7 +64,8 @@ public function restartDatabase($id) public function check_status() { try { - dispatch_sync(new ContainerStatusJob($this->service->server)); + GetContainersStatus::run($this->service->server); + // dispatch_sync(new ContainerStatusJob($this->service->server)); $this->dispatch('refresh')->self(); $this->dispatch('updateStatus'); } catch (\Exception $e) { diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php index 0f9c449f9..d6e867956 100644 --- a/app/Livewire/Project/Service/EditCompose.php +++ b/app/Livewire/Project/Service/EditCompose.php @@ -12,6 +12,7 @@ class EditCompose extends Component protected $rules = [ 'service.docker_compose_raw' => 'required', 'service.docker_compose' => 'required', + 'service.is_container_label_escape_enabled' => 'required', ]; public function mount() { @@ -23,6 +24,14 @@ public function saveEditedCompose() $this->dispatch('info', "Saving new docker compose..."); $this->dispatch('saveCompose', $this->service->docker_compose_raw); } + public function instantSave() + { + $this->validate([ + 'service.is_container_label_escape_enabled' => 'required', + ]); + $this->service->save(['is_container_label_escape_enabled' => $this->service->is_container_label_escape_enabled]); + $this->dispatch('success', "Service updated successfully"); + } public function render() { return view('livewire.project.service.edit-compose'); diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index fa19e8c42..2ccae47fd 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Shared; use App\Actions\Application\StopApplicationOneServer; +use App\Actions\Docker\GetContainersStatus; use App\Events\ApplicationStatusChanged; use App\Jobs\ContainerStatusJob; use App\Models\Server; @@ -90,7 +91,8 @@ public function promote(int $network_id, int $server_id) } public function refreshServers() { - ContainerStatusJob::dispatchSync($this->resource->destination->server); + GetContainersStatus::run($this->resource->destination->server); + // ContainerStatusJob::dispatchSync($this->resource->destination->server); $this->loadData(); $this->dispatch('refresh'); ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 6a6d94142..561d20d19 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -5,11 +5,11 @@ use App\Models\EnvironmentVariable; use Livewire\Component; use Visus\Cuid2\Cuid2; -use Illuminate\Support\Str; class All extends Component { public $resource; + public string $resourceClass; public bool $showPreview = false; public ?string $modalId = null; public ?string $variables = null; @@ -19,17 +19,44 @@ class All extends Component 'refreshEnvs', 'saveKey' => 'submit', ]; + protected $rules = [ + 'resource.settings.is_env_sorting_enabled' => 'required|boolean', + ]; + public function mount() { - $resourceClass = get_class($this->resource); + $this->resourceClass = get_class($this->resource); $resourceWithPreviews = ['App\Models\Application']; $simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile')); - if (Str::of($resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) { + if (str($this->resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) { $this->showPreview = true; } $this->modalId = new Cuid2(7); + $this->sortMe(); $this->getDevView(); } + + public function sortMe() + { + if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') { + if ($this->resource->settings->is_env_sorting_enabled) { + $this->resource->environment_variables = $this->resource->environment_variables->sortBy('key'); + $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('key'); + } else { + $this->resource->environment_variables = $this->resource->environment_variables->sortBy('id'); + $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('id'); + } + } + $this->getDevView(); + } + public function instantSave() + { + if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') { + $this->resource->settings->save(); + $this->dispatch('success', 'Environment variable settings updated.'); + $this->sortMe(); + } + } public function getDevView() { $this->variables = $this->resource->environment_variables->map(function ($item) { @@ -40,7 +67,7 @@ public function getDevView() return "$item->key=(multiline, edit in normal view)"; } return "$item->key=$item->value"; - })->sort()->join(' + })->join(' '); if ($this->showPreview) { $this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) { @@ -51,13 +78,18 @@ public function getDevView() return "$item->key=(multiline, edit in normal view)"; } return "$item->key=$item->value"; - })->sort()->join(' + })->join(' '); } } public function switch() { - $this->view = $this->view === 'normal' ? 'dev' : 'normal'; + if ($this->view === 'normal') { + $this->view = 'dev'; + } else { + $this->view = 'normal'; + } + $this->sortMe(); } public function saveVariables($isPreview) { @@ -66,6 +98,7 @@ public function saveVariables($isPreview) $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete(); } else { $variables = parseEnvFormatToArray($this->variables); + ray($variables, $this->variables); $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete(); } foreach ($variables as $key => $variable) { diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 52d628dc1..4fc8bb8c6 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -122,7 +122,7 @@ public function runCommand() if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } - $cmd = 'sh -c "if [ -f ~/.profile ]; then . ~/.profile; fi; ' . str_replace('"', '\"', $this->command) . '"'; + $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; " . str_replace("'", "'\''", $this->command) . "'"; if (!empty($this->workDir)) { $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}"; } else { diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 996131f37..e14cd6113 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -91,15 +91,35 @@ public function getLogs($refresh = false) if ($this->container) { if ($this->showTimeStamps) { if ($this->server->isSwarm()) { - $sshCommand = generateSshCommand($this->server, "docker service logs -n {$this->numberOfLines} -t {$this->container}"); + $command = "docker service logs -n {$this->numberOfLines} -t {$this->container}"; + if ($this->server->isNonRoot()) { + $command = parseCommandsByLineForSudo(collect($command), $this->server); + $command = $command[0]; + } + $sshCommand = generateSshCommand($this->server, $command); } else { - $sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}"); + $command = "docker logs -n {$this->numberOfLines} -t {$this->container}"; + if ($this->server->isNonRoot()) { + $command = parseCommandsByLineForSudo(collect($command), $this->server); + $command = $command[0]; + } + $sshCommand = generateSshCommand($this->server, $command); } } else { if ($this->server->isSwarm()) { - $sshCommand = generateSshCommand($this->server, "docker service logs -n {$this->numberOfLines} {$this->container}"); + $command = "docker service logs -n {$this->numberOfLines} {$this->container}"; + if ($this->server->isNonRoot()) { + $command = parseCommandsByLineForSudo(collect($command), $this->server); + $command = $command[0]; + } + $sshCommand = generateSshCommand($this->server, $command); } else { - $sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}"); + $command = "docker logs -n {$this->numberOfLines} {$this->container}"; + if ($this->server->isNonRoot()) { + $command = parseCommandsByLineForSudo(collect($command), $this->server); + $command = $command[0]; + } + $sshCommand = generateSshCommand($this->server, $command); } } if ($refresh) { diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php index 3bf507cab..56f5a2759 100644 --- a/app/Livewire/Project/Shared/HealthChecks.php +++ b/app/Livewire/Project/Shared/HealthChecks.php @@ -17,18 +17,17 @@ class HealthChecks extends Component 'resource.health_check_return_code' => 'integer', 'resource.health_check_scheme' => 'string', 'resource.health_check_response_text' => 'nullable|string', - 'resource.health_check_interval' => 'integer', - 'resource.health_check_timeout' => 'integer', - 'resource.health_check_retries' => 'integer', + 'resource.health_check_interval' => 'integer|min:1', + 'resource.health_check_timeout' => 'integer|min:1', + 'resource.health_check_retries' => 'integer|min:1', 'resource.health_check_start_period' => 'integer', + 'resource.custom_healthcheck_found' => 'boolean', ]; public function instantSave() { $this->resource->save(); $this->dispatch('success', 'Health check updated.'); - - } public function submit() { diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index 68e4e193e..f1d70bf28 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -27,7 +27,7 @@ class Logs extends Component public $query; public $status; public $serviceSubType; - + public $cpu; public function loadContainers($server_id) { try { @@ -49,6 +49,14 @@ public function loadContainers($server_id) return handleError($e, $this); } } + public function loadMetrics() + { + return; + $server = data_get($this->resource, 'destination.server'); + if ($server->isFunctional()) { + $this->cpu = $server->getMetrics(); + } + } public function mount() { try { @@ -95,6 +103,7 @@ public function mount() } } $this->containers = $this->containers->sort(); + $this->loadMetrics(); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php index 3a7a3fa23..c415ff3e4 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Add.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php @@ -2,11 +2,14 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use Illuminate\Support\Collection; use Livewire\Component; class Add extends Component { public $parameters; + public string $type; + public Collection $containerNames; public string $name; public string $command; public string $frequency; @@ -29,6 +32,9 @@ class Add extends Component public function mount() { $this->parameters = get_route_parameters(); + if ($this->containerNames->count() > 0) { + $this->container = $this->containerNames->first(); + } } public function submit() @@ -40,6 +46,11 @@ public function submit() $this->dispatch('error', 'Invalid Cron / Human expression.'); return; } + if (empty($this->container) || $this->container == 'null') { + if ($this->type == 'service') { + $this->container = $this->subServiceName; + } + } $this->dispatch('saveScheduledTask', [ 'name' => $this->name, 'command' => $this->command, diff --git a/app/Livewire/Project/Shared/ScheduledTask/All.php b/app/Livewire/Project/Shared/ScheduledTask/All.php index 975d695fa..e5ea66d13 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/All.php +++ b/app/Livewire/Project/Shared/ScheduledTask/All.php @@ -3,14 +3,13 @@ namespace App\Livewire\Project\Shared\ScheduledTask; use App\Models\ScheduledTask; +use Illuminate\Support\Collection; use Livewire\Component; -use Visus\Cuid2\Cuid2; -use Illuminate\Support\Str; class All extends Component { public $resource; - public string|null $modalId = null; + public Collection $containerNames; public ?string $variables = null; public array $parameters; protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit']; @@ -18,7 +17,18 @@ class All extends Component public function mount() { $this->parameters = get_route_parameters(); - $this->modalId = new Cuid2(7); + if ($this->resource->type() == 'service') { + $this->containerNames = $this->resource->applications()->pluck('name'); + $this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name')); + } elseif ($this->resource->type() == 'application') { + if ($this->resource->build_pack === 'dockercompose') { + $parsed = $this->resource->parseCompose(); + $containers = collect(data_get($parsed,'services'))->keys(); + $this->containerNames = $containers; + } else { + $this->containerNames = collect([]); + } + } } public function refreshTasks() { diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index 87b752509..7490c7055 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -17,6 +17,7 @@ class Show extends Component public string $type; protected $rules = [ + 'task.enabled' => 'required|boolean', 'task.name' => 'required|string', 'task.command' => 'required|string', 'task.frequency' => 'required|string', @@ -45,9 +46,18 @@ public function mount() $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); } + public function instantSave() + { + $this->validateOnly('task.enabled'); + $this->task->save(['enabled' => $this->task->enabled]); + $this->dispatch('success', 'Scheduled task updated.'); + $this->dispatch('refreshTasks'); + } public function submit() { $this->validate(); + $this->task->name = str($this->task->name)->trim()->value(); + $this->task->container = str($this->task->container)->trim()->value(); $this->task->save(); $this->dispatch('success', 'Scheduled task updated.'); $this->dispatch('refreshTasks'); @@ -60,11 +70,9 @@ public function delete() if ($this->type == 'application') { return redirect()->route('project.application.configuration', $this->parameters); - } - else { + } else { return redirect()->route('project.service.configuration', $this->parameters); } - } catch (\Exception $e) { return handleError($e); } diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 14a2809c7..c1dcd34ce 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -82,6 +82,7 @@ public function checkLocalhostConnection() $this->server->settings->is_reachable = true; $this->server->settings->is_usable = true; $this->server->settings->save(); + $this->dispatch('proxyStatusUpdated'); } else { $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: ' . $error); return; diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php index bd0ffe431..fbc16fde4 100644 --- a/app/Livewire/Server/Proxy/Status.php +++ b/app/Livewire/Server/Proxy/Status.php @@ -2,6 +2,7 @@ namespace App\Livewire\Server\Proxy; +use App\Actions\Docker\GetContainersStatus; use App\Actions\Proxy\CheckProxy; use App\Jobs\ContainerStatusJob; use App\Models\Server; @@ -49,7 +50,8 @@ public function checkProxy(bool $notification = false) public function getProxyStatus() { try { - dispatch_sync(new ContainerStatusJob($this->server)); + GetContainersStatus::run($this->server); + // dispatch_sync(new ContainerStatusJob($this->server)); $this->dispatch('proxyStatusUpdated'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php index 5281a1e01..54dbe1bdb 100644 --- a/app/Livewire/Settings/Configuration.php +++ b/app/Livewire/Settings/Configuration.php @@ -13,7 +13,7 @@ class Configuration extends Component public bool $is_auto_update_enabled; public bool $is_registration_enabled; public bool $is_dns_validation_enabled; - public bool $next_channel; + // public bool $next_channel; protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; protected Server $server; @@ -37,7 +37,7 @@ public function mount() $this->do_not_track = $this->settings->do_not_track; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; $this->is_registration_enabled = $this->settings->is_registration_enabled; - $this->next_channel = $this->settings->next_channel; + // $this->next_channel = $this->settings->next_channel; $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; } @@ -47,12 +47,12 @@ public function instantSave() $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; $this->settings->is_registration_enabled = $this->is_registration_enabled; $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; - if ($this->next_channel) { - $this->settings->next_channel = false; - $this->next_channel = false; - } else { - $this->settings->next_channel = $this->next_channel; - } + // if ($this->next_channel) { + // $this->settings->next_channel = false; + // $this->next_channel = false; + // } else { + // $this->settings->next_channel = $this->next_channel; + // } $this->settings->save(); $this->dispatch('success', 'Settings updated!'); } diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php index d1af807d5..1b2510f5d 100644 --- a/app/Livewire/Storage/Create.php +++ b/app/Livewire/Storage/Create.php @@ -16,13 +16,13 @@ class Create extends Component public string $endpoint; public S3Storage $storage; protected $rules = [ - 'name' => 'nullable|min:3|max:255', + 'name' => 'required|min:3|max:255', 'description' => 'nullable|min:3|max:255', 'region' => 'required|max:255', 'key' => 'required|max:255', 'secret' => 'required|max:255', 'bucket' => 'required|max:255', - 'endpoint' => 'nullable|url|max:255', + 'endpoint' => 'required|url|max:255', ]; protected $validationAttributes = [ 'name' => 'Name', diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php index 5c43edfb1..07034ed5d 100644 --- a/app/Livewire/Tags/Deployments.php +++ b/app/Livewire/Tags/Deployments.php @@ -26,6 +26,7 @@ public function get_deployments() "server_id", "status" ])->sortBy('id')->groupBy('server_name')->toArray(); + $this->dispatch('deployments', $this->deployments_per_tag_per_server); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php index d04bb53f9..c2b2a5928 100644 --- a/app/Livewire/Tags/Index.php +++ b/app/Livewire/Tags/Index.php @@ -20,6 +20,12 @@ class Index extends Component public $webhook = null; public $deployments_per_tag_per_server = []; + protected $listeners = ['deployments' => 'update_deployments']; + + public function update_deployments($deployments) + { + $this->deployments_per_tag_per_server = $deployments; + } public function tag_updated() { if ($this->tag == "") { @@ -39,14 +45,13 @@ public function tag_updated() public function redeploy_all() { try { - $message = collect([]); - $this->applications->each(function ($resource) use ($message) { + $this->applications->each(function ($resource){ $deploy = new Deploy(); - $message->push($deploy->deploy_resource($resource)); + $deploy->deploy_resource($resource); }); - $this->services->each(function ($resource) use ($message) { + $this->services->each(function ($resource) { $deploy = new Deploy(); - $message->push($deploy->deploy_resource($resource)); + $deploy->deploy_resource($resource); }); $this->dispatch('success', 'Mass deployment started.'); } catch (\Exception $e) { diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php new file mode 100644 index 000000000..12546ff1b --- /dev/null +++ b/app/Livewire/Team/AdminView.php @@ -0,0 +1,117 @@ +route('dashboard'); + } + $this->getUsers(); + } + public function submitSearch() + { + if ($this->search !== "") { + $this->users = User::where(function ($query) { + $query->where('name', 'like', "%{$this->search}%") + ->orWhere('email', 'like', "%{$this->search}%"); + })->get()->filter(function ($user) { + return $user->id !== auth()->id(); + }); + } else { + $this->getUsers(); + } + } + public function getUsers() + { + $this->users = User::where('id', '!=', auth()->id())->get(); + // $this->users = User::all(); + } + private function finalizeDeletion(User $user, Team $team) + { + $servers = $team->servers; + foreach ($servers as $server) { + $resources = $server->definedResources(); + foreach ($resources as $resource) { + ray("Deleting resource: " . $resource->name); + $resource->forceDelete(); + } + ray("Deleting server: " . $server->name); + $server->forceDelete(); + } + + $projects = $team->projects; + foreach ($projects as $project) { + ray("Deleting project: " . $project->name); + $project->forceDelete(); + } + $team->members()->detach($user->id); + ray('Deleting team: ' . $team->name); + $team->delete(); + } + public function delete($id) + { + $user = User::find($id); + $teams = $user->teams; + foreach ($teams as $team) { + ray($team->name); + $user_alone_in_team = $team->members->count() === 1; + if ($team->id === 0) { + if ($user_alone_in_team) { + ray('user is alone in the root team, do nothing'); + return $this->dispatch('error', 'User is alone in the root team, cannot delete'); + } + } + if ($user_alone_in_team) { + ray('user is alone in the team'); + $this->finalizeDeletion($user, $team); + continue; + } + ray('user is not alone in the team'); + if ($user->isOwner()) { + $found_other_owner_or_admin = $team->members->filter(function ($member) { + return $member->pivot->role === 'owner' || $member->pivot->role === 'admin'; + })->where('id', '!=', $user->id)->first(); + + if ($found_other_owner_or_admin) { + ray('found other owner or admin'); + $team->members()->detach($user->id); + continue; + } else { + $found_other_member_who_is_not_owner = $team->members->filter(function ($member) { + return $member->pivot->role === 'member'; + })->first(); + if ($found_other_member_who_is_not_owner) { + ray('found other member who is not owner'); + $found_other_member_who_is_not_owner->pivot->role = 'owner'; + $found_other_member_who_is_not_owner->pivot->save(); + $team->members()->detach($user->id); + } else { + // This should never happen as if the user is the only member in the team, the team should be deleted already. + ray('found no other member who is not owner'); + $this->finalizeDeletion($user, $team); + } + continue; + } + } else { + ray('user is not owner'); + $team->members()->detach($user->id); + } + } + ray("Deleting user: " . $user->name); + $user->delete(); + $this->getUsers(); + } + public function render() + { + return view('livewire.team.admin-view'); + } +} diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index 96ee76325..5ef966f43 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -3,7 +3,7 @@ namespace App\Livewire; use App\Actions\Server\UpdateCoolify; -use App\Models\InstanceSettings; + use Livewire\Component; use DanHarrin\LivewireRateLimiting\WithRateLimiting; @@ -11,6 +11,7 @@ class Upgrade extends Component { use WithRateLimiting; public bool $showProgress = false; + public bool $updateInProgress = false; public bool $isUpgradeAvailable = false; public string $latestVersion = ''; @@ -22,23 +23,17 @@ public function checkUpdate() if (isDev()) { $this->isUpgradeAvailable = true; } - $settings = InstanceSettings::get(); - if ($settings->next_channel) { - $this->isUpgradeAvailable = true; - $this->latestVersion = 'next'; - } } public function upgrade() { try { - if ($this->showProgress) { + if ($this->updateInProgress) { return; } - $this->rateLimit(1, 30); - $this->showProgress = true; + $this->rateLimit(1, 60); + $this->updateInProgress = true; UpdateCoolify::run(force: true, async: true); - $this->dispatch('success', "Updating Coolify to {$this->latestVersion} version..."); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Models/Application.php b/app/Models/Application.php index f28d389f4..0f3425dd6 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -113,6 +113,18 @@ public function link() } return null; } + public function failedTaskLink($task_uuid) + { + if (data_get($this, 'environment.project.uuid')) { + return 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 + ]); + } + return null; + } public function settings() { return $this->hasOne(ApplicationSetting::class); @@ -146,9 +158,13 @@ public function gitBranchLocation(): Attribute if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($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); + return "https://{$git_repository}/tree/{$this->git_branch}"; + } return $this->git_repository; } - ); } @@ -159,6 +175,11 @@ public function gitWebhook(): Attribute if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { return "{$this->source->html_url}/{$this->git_repository}/settings/hooks"; } + // Convert the SSH URL to HTTPS URL + if (strpos($this->git_repository, 'git@') === 0) { + $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + return "https://{$git_repository}/settings/hooks"; + } return $this->git_repository; } ); @@ -171,10 +192,29 @@ public function gitCommits(): Attribute if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { return "{$this->source->html_url}/{$this->git_repository}/commits/{$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); + return "https://{$git_repository}/commits/{$this->git_branch}"; + } return $this->git_repository; } ); } + public function gitCommitLink($link): string + { + if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { + if (str($this->source->html_url)->contains('bitbucket')) { + return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}"; + } + return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}"; + } + if (strpos($this->git_repository, 'git@') === 0) { + $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + return "https://{$git_repository}/commit/{$link}"; + } + return $this->git_repository; + } public function dockerfileLocation(): Attribute { return Attribute::make( @@ -429,6 +469,10 @@ public function isDeploymentInprogress() } return false; } + public function get_last_successful_deployment() + { + return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first(); + } public function get_last_days_deployments() { return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get(); @@ -847,7 +891,7 @@ function loadComposeFile($isInit = false) if (!$composeFileContent) { $this->docker_compose_location = $initialDockerComposeLocation; $this->save(); - throw new \RuntimeException("Could not load base compose file from $workdir$composeFile"); + throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile

Check if you used the right extension (.yaml or .yml) in the compose file name."); } else { $this->docker_compose_raw = $composeFileContent; $this->save(); @@ -963,4 +1007,52 @@ public function getFilesFromServer(bool $isInit = false) { getFilesystemVolumesFromServer($this, $isInit); } + + public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false) + { + if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) { + $healthcheckCommand = null; + $lines = $dockerfile->toArray(); + foreach ($lines as $line) { + $trimmedLine = trim($line); + if (str_starts_with($trimmedLine, 'HEALTHCHECK')) { + $healthcheckCommand .= trim($trimmedLine, '\\ '); + continue; + } + if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) { + $healthcheckCommand .= ' ' . trim($trimmedLine, '\\ '); + } + if (isset($healthcheckCommand) && !str_contains($trimmedLine, '\\') && !empty($healthcheckCommand)) { + $healthcheckCommand .= ' ' . $trimmedLine; + break; + } + } + if (str($healthcheckCommand)->isNotEmpty()) { + $interval = str($healthcheckCommand)->match('/--interval=(\d+)/'); + $timeout = str($healthcheckCommand)->match('/--timeout=(\d+)/'); + $start_period = str($healthcheckCommand)->match('/--start-period=(\d+)/'); + $start_interval = str($healthcheckCommand)->match('/--start-interval=(\d+)/'); + $retries = str($healthcheckCommand)->match('/--retries=(\d+)/'); + if ($interval->isNotEmpty()) { + $this->health_check_interval = $interval->toInteger(); + } + if ($timeout->isNotEmpty()) { + $this->health_check_timeout = $timeout->toInteger(); + } + if ($start_period->isNotEmpty()) { + $this->health_check_start_period = $start_period->toInteger(); + } + // if ($start_interval) { + // $this->health_check_start_interval = $start_interval->value(); + // } + if ($retries->isNotEmpty()) { + $this->health_check_retries = $retries->toInteger(); + } + if ($interval || $timeout || $start_period || $start_interval || $retries) { + $this->custom_healthcheck_found = true; + $this->save(); + } + } + } + } } diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 7f3f36d0a..c55f89e21 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -9,7 +9,8 @@ class ApplicationDeploymentQueue extends Model { protected $guarded = []; - public function setStatus(string $status) { + public function setStatus(string $status) + { $this->update([ 'status' => $status, ]); @@ -21,7 +22,13 @@ public function getOutput($name) } return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null; } - + public function commitMessage() + { + if (empty($this->commit_message) || is_null($this->commit_message)) { + return null; + } + return str($this->commit_message)->trim()->limit(50)->value(); + } public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false) { if ($type === 'error') { diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 7ed9e38e5..a1f3e4190 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -8,6 +8,18 @@ class Environment extends Model { protected $guarded = []; + + protected static function booted() + { + static::deleting(function ($environment) { + $shared_variables = $environment->environment_variables(); + foreach ($shared_variables as $shared_variable) { + ray('Deleting environment shared variable: ' . $shared_variable->name); + $shared_variable->delete(); + } + + }); + } public function isEmpty() { return $this->applications()->count() == 0 && diff --git a/app/Models/Project.php b/app/Models/Project.php index 2621d3da1..c2be8cc32 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -25,6 +25,11 @@ protected static function booted() static::deleting(function ($project) { $project->environments()->delete(); $project->settings()->delete(); + $shared_variables = $project->environment_variables(); + foreach ($shared_variables as $shared_variable) { + ray('Deleting project shared variable: ' . $shared_variable->name); + $shared_variable->delete(); + } }); } public function environment_variables() @@ -55,6 +60,7 @@ public function applications() return $this->hasManyThrough(Application::class, Environment::class); } + public function postgresqls() { return $this->hasManyThrough(StandalonePostgresql::class, Environment::class); @@ -91,4 +97,7 @@ public function resource_count() { return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count(); } + public function databases() { + return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get()); + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index bda044320..2f4c29080 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -3,11 +3,14 @@ namespace App\Models; use App\Actions\Server\InstallDocker; +use App\Actions\Server\StartSentinel; use App\Enums\ProxyTypes; +use App\Jobs\PullSentinelImageJob; use App\Notifications\Server\Revived; use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; @@ -239,7 +242,7 @@ public function setupDynamicProxyConfiguration() $dynamic_config_path = $this->proxyPath() . "/dynamic"; if ($this->proxyType() === 'TRAEFIK_V2') { $file = "$dynamic_config_path/coolify.yaml"; - if (empty($settings->fqdn)) { + if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) { instant_remote_process([ "rm -f $file", ], $this); @@ -358,7 +361,7 @@ public function setupDynamicProxyConfiguration() } } else if ($this->proxyType() === 'CADDY') { $file = "$dynamic_config_path/coolify.caddy"; - if (empty($settings->fqdn)) { + if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) { instant_remote_process([ "rm -f $file", ], $this); @@ -462,6 +465,36 @@ public function forceDisableServer() Storage::disk('ssh-keys')->delete($sshKeyFileLocation); Storage::disk('ssh-mux')->delete($this->muxFilename()); } + public function checkSentinel() + { + ray("Checking sentinel on server: {$this->name}"); + if ($this->is_metrics_enabled) { + $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this, false); + $sentinel_found = json_decode($sentinel_found, true); + $status = data_get($sentinel_found, '0.State.Status', 'exited'); + if ($status !== 'running') { + ray('Sentinel is not running, starting it...'); + PullSentinelImageJob::dispatch($this); + } else { + ray('Sentinel is running'); + } + } + } + public function getMetrics() + { + if ($this->is_metrics_enabled) { + $from = now()->subMinutes(5)->toIso8601ZuluString(); + $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + $cpu = str($cpu)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($cpu)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + list($time, $value) = explode(',', trim($line)); + return [(int) $time, (float) $value]; + }); + })->toArray(); + return $parsedCollection; + } + } public function isServerReady(int $tries = 3) { if ($this->skipServer()) { @@ -548,7 +581,36 @@ public function startUnmanaged($id) { return instant_remote_process(["docker start $id"], $this); } - public function loadUnmanagedContainers() + public function getContainers(): Collection + { + $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this, false); + $sentinel_found = json_decode($sentinel_found, true); + $status = data_get($sentinel_found, '0.State.Status', 'exited'); + if ($status === 'running') { + $containers = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/containers"'], $this, false); + if (is_null($containers)) { + return collect([]); + } + $containers = data_get(json_decode($containers, true), 'containers', []); + return collect($containers); + } else { + if ($this->isSwarm()) { + $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false); + } else { + $containers = instant_remote_process(["docker container ls -q"], $this, false); + if (!$containers) { + return collect([]); + } + $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false); + } + if (is_null($containers)) { + return collect([]); + } + + return format_docker_command_output_to_json($containers); + } + } + public function loadUnmanagedContainers(): Collection { if ($this->isFunctional()) { $containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this); diff --git a/app/Models/Service.php b/app/Models/Service.php index 41e61cbb8..ab40a761c 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -171,7 +171,7 @@ public function extraFields() ], ]); } - $fields->put('Tolgee', $data); + $fields->put('Tolgee', $data->toArray()); break; case str($image)?->contains('logto'): $data = collect([]); @@ -195,7 +195,7 @@ public function extraFields() ], ]); } - $fields->put('Logto', $data); + $fields->put('Logto', $data->toArray()); break; case str($image)?->contains('unleash-server'): $data = collect([]); @@ -218,7 +218,7 @@ public function extraFields() ], ]); } - $fields->put('Unleash', $data); + $fields->put('Unleash', $data->toArray()); break; case str($image)?->contains('grafana'): $data = collect([]); @@ -241,7 +241,7 @@ public function extraFields() ], ]); } - $fields->put('Grafana', $data); + $fields->put('Grafana', $data->toArray()); break; case str($image)?->contains('directus'): $data = collect([]); @@ -267,7 +267,7 @@ public function extraFields() ], ]); } - $fields->put('Directus', $data); + $fields->put('Directus', $data->toArray()); break; case str($image)?->contains('kong'): $data = collect([]); @@ -370,7 +370,7 @@ public function extraFields() ], ]); } - $fields->put('Weblate', $data); + $fields->put('Weblate', $data->toArray()); break; case str($image)?->contains('meilisearch'): $data = collect([]); @@ -384,7 +384,7 @@ public function extraFields() ], ]); } - $fields->put('Meilisearch', $data); + $fields->put('Meilisearch', $data->toArray()); break; case str($image)?->contains('ghost'): $data = collect([]); @@ -444,7 +444,33 @@ public function extraFields() ]); } - $fields->put('Ghost', $data); + $fields->put('Ghost', $data->toArray()); + break; + default: + $data = collect([]); + $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); + $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); + if ($admin_user) { + $data = $data->merge([ + 'User' => [ + 'key' => 'SERVICE_USER_ADMIN', + 'value' => data_get($admin_user, 'value', 'admin'), + 'readonly' => true, + 'rules' => 'required', + ], + ]); + } + if ($admin_password) { + $data = $data->merge([ + 'Password' => [ + 'key' => 'SERVICE_PASSWORD_ADMIN', + 'value' => data_get($admin_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Admin', $data->toArray()); break; case str($image)?->contains('vaultwarden'): $data = collect([]); @@ -723,6 +749,18 @@ public function link() } return null; } + public function failedTaskLink($task_uuid) + { + if (data_get($this, 'environment.project.uuid')) { + return route('project.service.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 + ]); + } + return null; + } public function documentation() { $services = getServiceTemplates(); @@ -749,6 +787,17 @@ public function server() { return $this->belongsTo(Server::class); } + public function byUuid(string $uuid) { + $app = $this->applications()->whereUuid($uuid)->first(); + if ($app) { + return $app; + } + $db = $this->databases()->whereUuid($uuid)->first(); + if ($db) { + return $db; + } + return null; + } public function byName(string $name) { $app = $this->applications()->whereName($name)->first(); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 3746a32f5..2197d51df 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -207,7 +207,4 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function database_name() { - return $this->clickhouse_db; - } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index adc1ea6cc..7b18666b8 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -207,7 +207,4 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function database_name() { - return '0'; - } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index ff91322a0..c2c1b98da 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -208,7 +208,4 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function database_name() { - return '0'; - } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 37d39f882..5e18bbfde 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -208,7 +208,4 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function database_name() { - return $this->mariadb_database; - } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 5538efe1a..8e4d327a3 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -223,7 +223,4 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function database_name() { - return $this->mongo_db; - } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 53e9b6f22..eede451d7 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -209,7 +209,4 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function database_name() { - return $this->mysql_database; - } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 6435c49de..cf449a815 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -208,7 +208,4 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function database_name() { - return $this->postgres_db; - } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index de18c8c07..da4701df9 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -204,7 +204,4 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function database_name() { - return '0'; - } } diff --git a/app/Models/Team.php b/app/Models/Team.php index 29e434a5d..81206019f 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -26,6 +26,34 @@ protected static function booted() throw new \Exception('You are not allowed to update this team.'); } }); + + static::deleting(function ($team) { + $keys = $team->privateKeys; + foreach ($keys as $key) { + ray('Deleting key: ' . $key->name); + $key->delete(); + } + $sources = $team->sources(); + foreach ($sources as $source) { + ray('Deleting source: ' . $source->name); + $source->delete(); + } + $tags = Tag::whereTeamId($team->id)->get(); + foreach ($tags as $tag) { + ray('Deleting tag: ' . $tag->name); + $tag->delete(); + } + $shared_variables = $team->environment_variables(); + foreach ($shared_variables as $shared_variable) { + ray('Deleting team shared variable: ' . $shared_variable->name); + $shared_variable->delete(); + } + $s3s = $team->s3s; + foreach ($s3s as $s3) { + ray('Deleting s3: ' . $s3->name); + $s3->delete(); + } + }); } public function routeNotificationForDiscord() diff --git a/app/Models/User.php b/app/Models/User.php index 0fa8ead2f..0e66fdaea 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -183,6 +183,7 @@ public function role() if (data_get($this, 'pivot')) { return $this->pivot->role; } - return auth()->user()->teams->where('id', currentTeam()->id)->first()->pivot->role; + $user = auth()->user()->teams->where('id', currentTeam()->id)->first(); + return data_get($user, 'pivot.role'); } } diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 1705deda1..05fe544d0 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -69,10 +69,10 @@ public function toMail(): MailMessage public function toDiscord(): string { if ($this->preview) { - $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: '; + $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' (' . $this->preview->fqdn . ') deployment failed: '; $message .= '[View Deployment Logs](' . $this->deployment_url . ')'; } else { - $message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): '; + $message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): '; $message .= '[View Deployment Logs](' . $this->deployment_url . ')'; } return $message; @@ -80,9 +80,9 @@ public function toDiscord(): string public function toTelegram(): array { if ($this->preview) { - $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: '; + $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' (' . $this->preview->fqdn . ') deployment failed: '; } else { - $message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): '; + $message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): '; } $buttons[] = [ "text" => "Deployment logs", diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 322df5cec..e138ac91e 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -45,13 +45,11 @@ public function via(object $notifiable): array { $channels = setNotificationChannels($notifiable, 'deployments'); if (isCloud()) { - $channels = array_filter($channels, function ($channel) { - return $channel !== 'App\Notifications\Channels\EmailChannel'; - }); + // TODO: Make batch notifications work with email + $channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']); } return $channels; } - public function toMail(): MailMessage { $mail = new MailMessage(); diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index 1401bb324..6101ef208 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -14,22 +14,27 @@ public function send($notifiable, $notification): void $buttons = data_get($data, 'buttons', []); $telegramToken = data_get($telegramData, 'token'); $chatId = data_get($telegramData, 'chat_id'); - $topicId = null; + $topicId = null; $topicsInstance = get_class($notification); switch ($topicsInstance) { - case 'App\Notifications\StatusChange': - $topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id'); - break; case 'App\Notifications\Test': $topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id'); break; - case 'App\Notifications\Deployment': + case 'App\Notifications\Application\StatusChanged': + $topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id'); + break; + case 'App\Notifications\Application\DeploymentSuccess': + case 'App\Notifications\Application\DeploymentFailed': $topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id'); break; - case 'App\Notifications\DatabaseBackup': + case 'App\Notifications\Database\BackupSuccess': + case 'App\Notifications\Database\BackupFailed': $topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id'); break; + case 'App\Notifications\ScheduledTask\TaskFailed': + $topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id'); + break; } if (!$telegramToken || !$chatId || !$message) { return; diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index 21dc799f8..d9c524da4 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -31,7 +31,7 @@ public function toMail(): MailMessage $mail->view('emails.container-restarted', [ 'containerName' => $this->name, 'serverName' => $this->server->name, - 'url' => $this->url , + 'url' => $this->url, ]); return $mail; } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index 3aa63ffd9..7cad486b3 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -15,21 +15,20 @@ class BackupFailed extends Notification implements ShouldQueue { use Queueable; - public $tries = 1; + public $backoff = 10; + public $tries = 2; public string $name; - public string $database_name; public string $frequency; - public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output) + public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output, public $database_name) { $this->name = $database->name; - $this->database_name = $database->database_name(); $this->frequency = $backup->frequency; } public function via(object $notifiable): array { - return [DiscordChannel::class, TelegramChannel::class, MailChannel::class]; + return setNotificationChannels($notifiable, 'database_backups'); } public function toMail(): MailMessage @@ -47,11 +46,11 @@ public function toMail(): MailMessage public function toDiscord(): string { - return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}"; + return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}"; } public function toTelegram(): array { - $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}"; + $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}"; return [ "message" => $message, ]; diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 9ca3234e1..c43a12276 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -12,15 +12,14 @@ class BackupSuccess extends Notification implements ShouldQueue { use Queueable; - public $tries = 1; + public $backoff = 10; + public $tries = 3; public string $name; - public string $database_name; public string $frequency; - public function __construct(ScheduledDatabaseBackup $backup, public $database) + public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name) { $this->name = $database->name; - $this->database_name = $database->database_name(); $this->frequency = $backup->frequency; } @@ -48,6 +47,7 @@ public function toDiscord(): string public function toTelegram(): array { $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; + ray($message); return [ "message" => $message, ]; diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php new file mode 100644 index 000000000..f61b1f573 --- /dev/null +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -0,0 +1,64 @@ +application) { + $this->url = $task->application->failedTaskLink($task->uuid); + } else if ($task->service) { + $this->url = $task->service->failedTaskLink($task->uuid); + } + } + + public function via(object $notifiable): array + { + + return setNotificationChannels($notifiable, 'scheduled_tasks'); + } + + public function toMail(): MailMessage + { + $mail = new MailMessage(); + $mail->subject("Coolify: [ACTION REQUIRED] Scheduled task ({$this->task->name}) failed."); + $mail->view('emails.scheduled-task-failed', [ + 'task' => $this->task, + 'url' => $this->url, + 'output' => $this->output, + ]); + return $mail; + } + + public function toDiscord(): string + { + return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}"; + } + public function toTelegram(): array + { + $message = "Coolify: Scheduled task ({$this->task->name}) failed with output: {$this->output}"; + if ($this->url) { + $buttons[] = [ + "text" => "Open task in Coolify", + "url" => (string) $this->url + ]; + } + return [ + "message" => $message, + ]; + } +} diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Revived.php index c670ded9a..36775976b 100644 --- a/app/Notifications/Server/Revived.php +++ b/app/Notifications/Server/Revived.php @@ -2,6 +2,7 @@ namespace App\Notifications\Server; +use App\Actions\Docker\GetContainersStatus; use App\Jobs\ContainerStatusJob; use App\Models\Server; use Illuminate\Bus\Queueable; @@ -22,7 +23,8 @@ public function __construct(public Server $server) if ($this->server->unreachable_notification_sent === false) { return; } - dispatch(new ContainerStatusJob($server)); + GetContainersStatus::dispatch($server); + // dispatch(new ContainerStatusJob($server)); } public function via(object $notifiable): array diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index c0aaf4abf..a1995c645 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -6,7 +6,6 @@ use App\Models\ApplicationDeploymentQueue; use App\Models\Server; use App\Models\StandaloneDocker; -use Illuminate\Support\Collection; use Spatie\Url\Url; function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index a70c85a72..a087c92c5 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -8,6 +8,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\Url\Url; +use Visus\Cuid2\Cuid2; function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection { @@ -272,7 +273,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, } return $labels->sort(); } -function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null) +function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false) { $labels = collect([]); $labels->push('traefik.enable=true'); @@ -313,7 +314,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ } foreach ($domains as $loop => $domain) { try { - // $uuid = new Cuid2(7); + if ($generate_unique_uuid) { + $uuid = new Cuid2(7); + } $url = Url::fromString($domain); $host = $url->getHost(); $path = $url->getPath(); diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 8e3c0337e..26a69222a 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -18,7 +18,7 @@ function collectRegex(string $name) } function replaceVariables($variable) { - return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', ''); + return $variable->before('}')->replaceFirst('$', '')->replaceFirst('{', ''); } function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false) @@ -27,7 +27,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli if ($oneService->getMorphClass() === 'App\Models\Application') { $workdir = $oneService->workdir(); $server = $oneService->destination->server; - } else{ + } else { $workdir = $oneService->service->workdir(); $server = $oneService->service->server; } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index b2c34900e..6453108eb 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -95,6 +95,9 @@ function currentTeam() function showBoarding(): bool { + if (auth()->user()?->isMember()) { + return false; + } return currentTeam()->show_boarding ?? false; } function refreshSession(?Team $team = null): void @@ -147,6 +150,18 @@ function get_route_parameters(): array return Route::current()->parameters(); } +function get_latest_sentinel_version(): string +{ + try { + $response = Http::get('https://cdn.coollabs.io/coolify/versions.json'); + $versions = $response->json(); + return data_get($versions, 'coolify.sentinel.version'); + } catch (\Throwable $e) { + //throw $e; + ray($e->getMessage()); + return '0.0.0'; + } +} function get_latest_version_of_coolify(): string { try { @@ -637,7 +652,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $allServices = getServiceTemplates(); $topLevelVolumes = collect(data_get($yaml, 'volumes', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', [])); - $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8'; $services = data_get($yaml, 'services'); $generatedServiceFQDNS = collect([]); @@ -988,20 +1002,18 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($fqdns_exploded->count() > 1) { continue; } - if ($resource->server->proxyType() === 'CADDY') { - $env = EnvironmentVariable::where([ - 'key' => $key, - 'service_id' => $resource->id, - ])->first(); - if ($env) { + $env = EnvironmentVariable::where([ + 'key' => $key, + 'service_id' => $resource->id, + ])->first(); + if ($env) { - $env_url = Url::fromString($savedService->fqdn); - $env_port = $env_url->getPort(); - if ($env_port !== $predefinedPort) { - $env_url = $env_url->withPort($predefinedPort); - $savedService->fqdn = $env_url->__toString(); - $savedService->save(); - } + $env_url = Url::fromString($savedService->fqdn); + $env_port = $env_url->getPort(); + if ($env_port !== $predefinedPort) { + $env_url = $env_url->withPort($predefinedPort); + $savedService->fqdn = $env_url->__toString(); + $savedService->save(); } } } @@ -1165,6 +1177,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal ] ]); } + if ($serviceLabels->count() > 0) { + if ($resource->is_container_label_escape_enabled) { + $serviceLabels = $serviceLabels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } + } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); if (!data_get($service, 'restart')) { @@ -1194,7 +1213,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $service; }); $finalServices = [ - 'version' => $dockerComposeVersion, 'services' => $services->toArray(), 'volumes' => $topLevelVolumes->toArray(), 'networks' => $topLevelNetworks->toArray(), @@ -1217,13 +1235,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal try { $yaml = Yaml::parse($resource->docker_compose_pr_raw); } catch (\Exception $e) { - throw new \Exception($e->getMessage()); + return; } } else { try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { - throw new \Exception($e->getMessage()); + return; } } $server = $resource->destination->server; @@ -1232,7 +1250,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $topLevelVolumes = collect([]); } $topLevelNetworks = collect(data_get($yaml, 'networks', [])); - $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8'; $services = data_get($yaml, 'services'); $generatedServiceFQDNS = collect([]); @@ -1252,6 +1269,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $servicePorts = collect(data_get($service, 'ports', [])); $serviceNetworks = collect(data_get($service, 'networks', [])); $serviceVariables = collect(data_get($service, 'environment', [])); + $serviceDependencies = collect(data_get($service, 'depends_on', [])); $serviceLabels = collect(data_get($service, 'labels', [])); $serviceBuildVariables = collect(data_get($service, 'build.args', [])); $serviceVariables = $serviceVariables->merge($serviceBuildVariables); @@ -1268,11 +1286,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceLabels->push("$removedLabelName=$removedLabel"); } } - if ($serviceLabels->count() > 0) { - $serviceLabels = $serviceLabels->map(function ($value, $key) { - return escapeDollarSign($value); - }); - } + $baseName = generateApplicationContainerName($resource, $pull_request_id); $containerName = "$serviceName-$baseName"; if (count($serviceVolumes) > 0) { @@ -1363,6 +1377,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal data_set($service, 'volumes', $serviceVolumes->toArray()); } + if ($pull_request_id !== 0 && count($serviceDependencies) > 0) { + $serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) { + return $dependency . "-pr-$pull_request_id"; + }); + data_set($service, 'depends_on', $serviceDependencies->toArray()); + } + // Decide if the service is a database $isDatabase = isDatabaseImage(data_get_str($service, 'image')); data_set($service, 'is_database', $isDatabase); @@ -1620,7 +1641,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( uuid: $resource->uuid, domains: $fqdns, - serviceLabels: $serviceLabels + serviceLabels: $serviceLabels, + generate_unique_uuid: $resource->build_pack === 'dockercompose' )); $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( network: $resource->destination->network, @@ -1644,6 +1666,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal ] ]); } + if ($serviceLabels->count() > 0) { + if ($resource->settings->is_container_label_escape_enabled) { + $serviceLabels = $serviceLabels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } + } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); if (!data_get($service, 'restart')) { @@ -1662,7 +1691,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal }); } $finalServices = [ - 'version' => $dockerComposeVersion, 'services' => $services->toArray(), 'volumes' => $topLevelVolumes->toArray(), 'networks' => $topLevelNetworks->toArray(), @@ -1842,7 +1870,7 @@ function validate_dns_entry(string $fqdn, Server $server) $dns_servers = data_get($settings, 'custom_dns_servers'); $dns_servers = str($dns_servers)->explode(','); if ($server->id === 0) { - $ip = data_get($settings, 'public_ipv4') || data_get($settings, 'public_ipv6') || $server->ip; + $ip = data_get($settings, 'public_ipv4', data_get($settings, 'public_ipv6', $server->ip)); } else { $ip = $server->ip; } @@ -1921,7 +1949,6 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null $naked_domain = str($domain)->value(); if ($domains->contains($naked_domain)) { if (data_get($resource, 'uuid')) { - ray($resource->uuid, $app->uuid); if ($resource->uuid !== $app->uuid) { throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); } diff --git a/config/constants.php b/config/constants.php index 091c60996..53f43ae5a 100644 --- a/config/constants.php +++ b/config/constants.php @@ -32,6 +32,7 @@ 'basic' => env('LIMIT_SERVER_BASIC', 2), 'pro' => env('LIMIT_SERVER_PRO', 10), 'ultimate' => env('LIMIT_SERVER_ULTIMATE', 25), + 'dynamic' => env('LIMIT_SERVER_DYNAMIC', 2), ], 'email' => [ 'zero' => true, @@ -39,6 +40,7 @@ 'basic' => true, 'pro' => true, 'ultimate' => true, + 'dynamic' => true, ], ], ]; diff --git a/config/coolify.php b/config/coolify.php index a6d6d8581..c7cfe6101 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -14,4 +14,5 @@ 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'), 'is_horizon_enabled' => env('HORIZON_ENABLED', true), 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), + 'is_sentinel_enabled' => env('SENTINEL_ENABLED', false), ]; diff --git a/config/sentry.php b/config/sentry.php index 2b48e1f14..693c33c3d 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ // 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.269', + 'release' => '4.0.0-beta.285', // 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 bf35359e8..e09c4fd6a 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ boolean('custom_healthcheck_found')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('custom_healthcheck_found'); + }); + } +}; diff --git a/database/migrations/2024_05_06_093236_add_custom_name_to_application_settings.php b/database/migrations/2024_05_06_093236_add_custom_name_to_application_settings.php new file mode 100644 index 000000000..e2d68d240 --- /dev/null +++ b/database/migrations/2024_05_06_093236_add_custom_name_to_application_settings.php @@ -0,0 +1,28 @@ +string('custom_internal_name')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('custom_internal_name'); + }); + } +}; diff --git a/database/migrations/2024_05_07_124019_add_server_metrics.php b/database/migrations/2024_05_07_124019_add_server_metrics.php new file mode 100644 index 000000000..40c74850b --- /dev/null +++ b/database/migrations/2024_05_07_124019_add_server_metrics.php @@ -0,0 +1,28 @@ +boolean('is_metrics_enabled')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('is_metrics_enabled'); + }); + } +}; diff --git a/database/migrations/2024_05_10_085215_make_stripe_comment_longer.php b/database/migrations/2024_05_10_085215_make_stripe_comment_longer.php new file mode 100644 index 000000000..a51896f42 --- /dev/null +++ b/database/migrations/2024_05_10_085215_make_stripe_comment_longer.php @@ -0,0 +1,28 @@ +longText('stripe_comment')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('subscriptions', function (Blueprint $table) { + $table->string('stripe_comment')->change(); + }); + } +}; diff --git a/database/migrations/2024_05_15_091757_add_commit_message_to_app_deployment_queue.php b/database/migrations/2024_05_15_091757_add_commit_message_to_app_deployment_queue.php new file mode 100644 index 000000000..78608f503 --- /dev/null +++ b/database/migrations/2024_05_15_091757_add_commit_message_to_app_deployment_queue.php @@ -0,0 +1,28 @@ +string('commit_message', 50)->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->dropColumn('commit_message'); + }); + } +}; diff --git a/database/migrations/2024_05_15_151236_add_container_escape_toggle.php b/database/migrations/2024_05_15_151236_add_container_escape_toggle.php new file mode 100644 index 000000000..aa1384518 --- /dev/null +++ b/database/migrations/2024_05_15_151236_add_container_escape_toggle.php @@ -0,0 +1,34 @@ +boolean('is_container_label_escape_enabled')->default(true); + }); + Schema::table('services', function (Blueprint $table) { + $table->boolean('is_container_label_escape_enabled')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_container_label_escape_enabled'); + }); + Schema::table('services', function (Blueprint $table) { + $table->dropColumn('is_container_label_escape_enabled'); + }); + } +}; diff --git a/database/migrations/2024_05_17_082012_add_env_sorting_toggle.php b/database/migrations/2024_05_17_082012_add_env_sorting_toggle.php new file mode 100644 index 000000000..d4e120e2b --- /dev/null +++ b/database/migrations/2024_05_17_082012_add_env_sorting_toggle.php @@ -0,0 +1,28 @@ +boolean('is_env_sorting_enabled')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_env_sorting_enabled'); + }); + } +}; diff --git a/database/migrations/2024_05_21_125739_add_scheduled_tasks_notification_to_teams.php b/database/migrations/2024_05_21_125739_add_scheduled_tasks_notification_to_teams.php new file mode 100644 index 000000000..0fcbb0655 --- /dev/null +++ b/database/migrations/2024_05_21_125739_add_scheduled_tasks_notification_to_teams.php @@ -0,0 +1,34 @@ +boolean('telegram_notifications_scheduled_tasks')->default(true); + $table->boolean('smtp_notifications_scheduled_tasks')->default(false)->after('smtp_notifications_status_changes'); + $table->boolean('discord_notifications_scheduled_tasks')->default(true)->after('discord_notifications_status_changes'); + $table->text('telegram_notifications_scheduled_tasks_thread_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('telegram_notifications_scheduled_tasks'); + $table->dropColumn('smtp_notifications_scheduled_tasks'); + $table->dropColumn('discord_notifications_scheduled_tasks'); + $table->dropColumn('telegram_notifications_scheduled_tasks_thread_id'); + }); + } +}; diff --git a/database/seeders/TestTeamSeeder.php b/database/seeders/TestTeamSeeder.php new file mode 100644 index 000000000..1d660c713 --- /dev/null +++ b/database/seeders/TestTeamSeeder.php @@ -0,0 +1,42 @@ +create([ + 'name' => '1 personal, 1 other team, owner, no other members', + 'email' => '1@example.com', + ]); + $team = Team::create([ + 'name' => "1@example.com", + 'personal_team' => false, + 'show_boarding' => true + ]); + $user->teams()->attach($team, ['role' => 'owner']); + + // User has 2 teams, 1 personal, 1 other where it is the owner and 1 other member is in the team + $user = User::factory()->create([ + 'name' => 'owner: 1 personal, 1 other team, owner, 1 other member', + 'email' => '2@example.com', + ]); + $team = Team::create([ + 'name' => "2@example.com", + 'personal_team' => false, + 'show_boarding' => true + ]); + $user->teams()->attach($team, ['role' => 'owner']); + $user = User::factory()->create([ + 'name' => 'member: 1 personal, 1 other team, owner, 1 other member', + 'email' => '3@example.com', + ]); + $team->members()->attach($user, ['role' => 'member']); + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6d76a9abd..91e90b989 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: coolify: build: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index f68b2c41c..f3dda9748 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,4 +1,3 @@ -version: '3.8' services: coolify: image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-latest}" diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index e35ece624..af5ecc0f7 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -1,4 +1,3 @@ -version: '3.8' services: coolify-testing-host: init: true diff --git a/docker-compose.yml b/docker-compose.yml index 6adfaf98a..8eed44f8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3.8' services: coolify: container_name: coolify @@ -11,7 +10,6 @@ services: depends_on: - postgres - redis - postgres: image: postgres:15-alpine container_name: coolify-db diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 5798c92bd..768d2ca89 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -2,15 +2,15 @@ FROM alpine:3.17 ARG TARGETPLATFORM # https://download.docker.com/linux/static/stable/ -ARG DOCKER_VERSION=24.0.9 +ARG DOCKER_VERSION=26.1.2 # https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.25.0 +ARG DOCKER_COMPOSE_VERSION=2.27.0 # https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.13.1 +ARG DOCKER_BUILDX_VERSION=0.14.0 # https://github.com/buildpacks/pack/releases ARG PACK_VERSION=0.33.2 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.21.2 +ARG NIXPACKS_VERSION=1.21.3 USER root WORKDIR /artifacts diff --git a/docker/dev-ssu/Dockerfile b/docker/dev-ssu/Dockerfile index 0c7ce2b2a..f0e353d28 100644 --- a/docker/dev-ssu/Dockerfile +++ b/docker/dev-ssu/Dockerfile @@ -2,7 +2,7 @@ FROM serversideup/php:8.2-fpm-nginx-v2.2.1 ARG TARGETPLATFORM # https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2024.2.1 +ARG CLOUDFLARED_VERSION=2024.4.1 ARG POSTGRES_VERSION=15 RUN apt-get update diff --git a/docker/prod-ssu/Dockerfile b/docker/prod-ssu/Dockerfile index dcc1c334d..2192f4f0e 100644 --- a/docker/prod-ssu/Dockerfile +++ b/docker/prod-ssu/Dockerfile @@ -15,7 +15,7 @@ FROM serversideup/php:8.2-fpm-nginx-v2.2.1 ARG TARGETPLATFORM # https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2024.2.1 +ARG CLOUDFLARED_VERSION=2024.4.1 ARG POSTGRES_VERSION=15 WORKDIR /var/www/html diff --git a/docker/testing-host/Dockerfile b/docker/testing-host/Dockerfile index 6d0d0d5c5..deb09eeba 100644 --- a/docker/testing-host/Dockerfile +++ b/docker/testing-host/Dockerfile @@ -2,11 +2,11 @@ FROM debian:12-slim ARG TARGETPLATFORM # https://download.docker.com/linux/static/stable/ -ARG DOCKER_VERSION=24.0.5 +ARG DOCKER_VERSION=26.1.2 # https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.21.0 +ARG DOCKER_COMPOSE_VERSION=2.27.0 # https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.11.2 +ARG DOCKER_BUILDX_VERSION=0.14.0 USER root WORKDIR /root diff --git a/lang/zh-cn.json b/lang/zh-cn.json new file mode 100644 index 000000000..70c457fa8 --- /dev/null +++ b/lang/zh-cn.json @@ -0,0 +1,30 @@ +{ + "auth.login": "登录", + "auth.login.azure": "使用 Microsoft 登录", + "auth.login.bitbucket": "使用 Bitbucket 登录", + "auth.login.github": "使用 GitHub 登录", + "auth.login.gitlab": "使用 Gitlab 登录", + "auth.login.google": "使用 Google 登录", + "auth.already_registered": "已经注册?", + "auth.confirm_password": "确认密码", + "auth.forgot_password": "忘记密码", + "auth.forgot_password_send_email": "发送密码重置邮件", + "auth.register_now": "注册", + "auth.logout": "退出登录", + "auth.register": "注册", + "auth.registration_disabled": "注册已禁用,请联系管理员", + "auth.reset_password": "重置密码", + "auth.failed": "这些凭据与我们的记录不符", + "auth.failed.callback": "处理第三方登录的回调时出错", + "auth.failed.password": "密码错误", + "auth.failed.email": "该账户未注册", + "auth.throttle": "登录次数过多,请在 :seconds 秒后重试", + "input.name": "用户名", + "input.email": "邮箱", + "input.password": "密码", + "input.password.again": "确认密码", + "input.code": "验证码", + "input.recovery_code": "恢复码", + "button.save": "保存", + "repository.url": "示例
对于公共代码仓库,请使用 https://...
对于私有代码仓库,请使用 git@...

https://github.com/coollabsio/coolify-examples main 分支将被选择
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify 分支将被选择。
https://gitea.com/sedlav/expressjs.git main 分支将被选择。
https://gitlab.com/andrasbacsai/nodejs-example.git main 分支将被选择" +} diff --git a/other/scripts/get-subs.php b/other/scripts/get-subs.php new file mode 100644 index 000000000..3a23fc073 --- /dev/null +++ b/other/scripts/get-subs.php @@ -0,0 +1,11 @@ +$handle = fopen("/tmp/export.csv", "w"); +App\Models\Team::chunk(100, function ($teams) use ($handle) { + foreach ($teams as $team) { + if ($team->subscription->stripe_invoice_paid == true) { + foreach ($team->members as $member) { + fputcsv($handle, [$member->email, $member->name], ","); + } + } + } +}); +fclose($handle); diff --git a/public/svgs/listmonk.svg b/public/svgs/listmonk.svg new file mode 100644 index 000000000..a4e5efd5f --- /dev/null +++ b/public/svgs/listmonk.svg @@ -0,0 +1,2 @@ + + diff --git a/public/svgs/twenty.svg b/public/svgs/twenty.svg new file mode 100644 index 000000000..eef3a382a --- /dev/null +++ b/public/svgs/twenty.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/vikunja.svg b/public/svgs/vikunja.svg new file mode 100644 index 000000000..53176d66e --- /dev/null +++ b/public/svgs/vikunja.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/css/app.css b/resources/css/app.css index 1f218490d..cae83b0de 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -32,7 +32,7 @@ .select { @apply block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset; } -.input[type='password'] { +.input[type="password"] { @apply pr-10; } @@ -41,7 +41,7 @@ option { } .button { - @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-black hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300; + @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300; } button[isError]:not(:disabled) { @@ -52,7 +52,6 @@ button[isHighlighted]:not(:disabled) { @apply text-white bg-coollabs hover:bg-coollabs-100; } - h1 { @apply text-2xl font-bold dark:text-white; } @@ -78,7 +77,7 @@ label { } table { - @apply min-w-full divide-y dark:divide-coolgray-200 divide-neutral-300 ; + @apply min-w-full divide-y dark:divide-coolgray-200 divide-neutral-300; } thead { @@ -117,7 +116,7 @@ .alert-error { @apply flex items-center gap-2 text-error; } .tag { - @apply px-2 py-1 cursor-pointer box-description dark:bg-coolgray-100 dark:hover:bg-coolgray-300 bg-neutral-100 hover:bg-neutral-200 + @apply px-2 py-1 cursor-pointer box-description dark:bg-coolgray-100 dark:hover:bg-coolgray-300 bg-neutral-100 hover:bg-neutral-200; } .add-tag { @apply flex items-center px-2 text-xs cursor-pointer dark:text-neutral-500/20 text-neutral-500 group-hover:text-neutral-700 group-hover:dark:text-white dark:hover:bg-coolgray-300 hover:bg-neutral-200; @@ -135,7 +134,6 @@ .badge { .badge-absolute { @apply absolute top-0 right-0 w-2 h-2 border-none rounded-t-none rounded-r-none; - } .badge-success { @@ -159,7 +157,7 @@ .menu { } .menu-item { - @apply flex items-center w-full gap-3 py-1 pl-2 dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300; + @apply flex items-center w-full gap-3 px-2 py-1 text-sm sm:pr-0 dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300 min-w-fit sm:min-w-64; } .menu-item-active { @@ -174,7 +172,6 @@ .icon { @apply w-6 h-6 dark:hover:text-white; } - .scrollbar { @apply scrollbar-thumb-coollabs-100 dark:scrollbar-track-coolgray-200 scrollbar-track-neutral-200 scrollbar-w-2; } @@ -188,7 +185,7 @@ .custom-modal { } .navbar-main { - @apply flex items-center h-10 gap-6 pb-2 border-b-2 border-solid dark:border-coolgray-200; + @apply flex flex-col gap-4 pb-2 border-b-2 border-solid h-fit md:flex-row justify-items-start sm:justify-between dark:border-coolgray-200 md:items-center; } .loading { @@ -203,20 +200,19 @@ .box { @apply relative flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 bg-white border text-black dark:text-white hover:text-black border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:no-underline; } .box-boarding { - @apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 dark:text-white bg-neutral-50 border border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:text-black hover:no-underline text-black ; + @apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 dark:text-white bg-neutral-50 border border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:text-black hover:no-underline text-black; } .box-without-bg { - @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] border border-neutral-200 dark:border-black; + @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] border border-neutral-200 dark:border-black; } .box-without-bg-without-border { - @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] ; + @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem]; } .on-box { @apply rounded hover:bg-neutral-300 dark:hover:bg-coolgray-500/20; } - .box-title { @apply font-bold text-black dark:text-white group-hover:dark:text-white; } diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index f57d8c6fc..e04d3633d 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -4,8 +4,7 @@ Coolify -
+

Create an account diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index 2c6c1ef49..04b4a41c6 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -41,6 +41,7 @@ class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:da wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="{{ $type }}" @disabled($disabled) + min="{{ $attributes->get('min') }}" max="{{ $attributes->get('max') }}" @if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"> @endif diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index aab4b6a7a..924625424 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -84,7 +84,7 @@

diff --git a/resources/views/components/resources/breadcrumbs.blade.php b/resources/views/components/resources/breadcrumbs.blade.php index 5b0956c93..efd36a83e 100644 --- a/resources/views/components/resources/breadcrumbs.blade.php +++ b/resources/views/components/resources/breadcrumbs.blade.php @@ -1,35 +1,38 @@ +@props([ + 'lastDeploymentInfo' => null, + 'lastDeploymentLink' => null, + 'resource' => null, +]) diff --git a/resources/views/components/security/navbar.blade.php b/resources/views/components/security/navbar.blade.php index 5ad4afb3c..e389ad6b7 100644 --- a/resources/views/components/security/navbar.blade.php +++ b/resources/views/components/security/navbar.blade.php @@ -1,12 +1,14 @@

Security

Security related settings.
- +
diff --git a/resources/views/components/server/navbar.blade.php b/resources/views/components/server/navbar.blade.php index 5afee652c..c24787e97 100644 --- a/resources/views/components/server/navbar.blade.php +++ b/resources/views/components/server/navbar.blade.php @@ -2,62 +2,52 @@

Server

- @if ( - $server->proxyType() !== 'NONE' && - $server->isFunctional() && - !$server->isSwarmWorker() && - !$server->settings->is_build_server) - - @endif +
{{ data_get($server, 'name') }}.
- +
+ diff --git a/resources/views/components/settings/navbar.blade.php b/resources/views/components/settings/navbar.blade.php index fc431d220..7b04b66b9 100644 --- a/resources/views/components/settings/navbar.blade.php +++ b/resources/views/components/settings/navbar.blade.php @@ -1,17 +1,19 @@

Settings

Instance wide settings for Coolify.
- +
diff --git a/resources/views/components/status/index.blade.php b/resources/views/components/status/index.blade.php index 4ee3319ff..0ef652897 100644 --- a/resources/views/components/status/index.blade.php +++ b/resources/views/components/status/index.blade.php @@ -1,9 +1,14 @@ +@props([ + 'lastDeploymentInfo' => null, + 'lastDeploymentLink' => null, + 'resource' => null, +]) @if (str($resource->status)->startsWith('running')) - + @elseif(str($resource->status)->startsWith('restarting') || str($resource->status)->startsWith('starting') || str($resource->status)->startsWith('degraded')) - + @else @endif diff --git a/resources/views/components/status/restarting.blade.php b/resources/views/components/status/restarting.blade.php index bb555c745..c24f9f213 100644 --- a/resources/views/components/status/restarting.blade.php +++ b/resources/views/components/status/restarting.blade.php @@ -1,12 +1,20 @@ @props([ 'status' => 'Restarting', + 'lastDeploymentInfo' => null, + 'lastDeploymentLink' => null, ])
-
- {{ str($status)->before(':')->headline() }} +
+ @if ($lastDeploymentLink) + + {{ str($status)->before(':')->headline() }} + + @else + {{ str($status)->before(':')->headline() }} + @endif
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
({{ str($status)->after(':') }})
diff --git a/resources/views/components/status/running.blade.php b/resources/views/components/status/running.blade.php index 52758303b..7332995a2 100644 --- a/resources/views/components/status/running.blade.php +++ b/resources/views/components/status/running.blade.php @@ -1,12 +1,20 @@ @props([ 'status' => 'Running', + 'lastDeploymentInfo' => null, + 'lastDeploymentLink' => null, ])
-
+
+ @if ($lastDeploymentLink) + + {{ str($status)->before(':')->headline() }} + + @else {{ str($status)->before(':')->headline() }} + @endif
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('(')) @if (str($status)->contains('unhealthy')) @@ -17,8 +25,6 @@ - {{-- @else -
({{ str($status)->after(':') }})
--}} @endif @endif diff --git a/resources/views/components/team/navbar.blade.php b/resources/views/components/team/navbar.blade.php index 2c38d9695..aa88aad51 100644 --- a/resources/views/components/team/navbar.blade.php +++ b/resources/views/components/team/navbar.blade.php @@ -6,14 +6,22 @@
Team wide configurations.
- +
diff --git a/resources/views/destination/all.blade.php b/resources/views/destination/all.blade.php index 7de97eb46..bb1b99bf5 100644 --- a/resources/views/destination/all.blade.php +++ b/resources/views/destination/all.blade.php @@ -11,22 +11,22 @@
@forelse ($destinations as $destination) @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker') - @endif @if ($destination->getMorphClass() === 'App\Models\SwarmDocker') - @endif @empty
diff --git a/resources/views/emails/backup-failed.blade.php b/resources/views/emails/backup-failed.blade.php index f50ff0c33..013c8bc98 100644 --- a/resources/views/emails/backup-failed.blade.php +++ b/resources/views/emails/backup-failed.blade.php @@ -1,5 +1,5 @@ -Database backup for {{ $name }} (db:{{$database_name}}) with frequency of {{ $frequency }} was FAILED. +Database backup for {{ $name }} @if($database_name)(db:{{ $database_name }})@endif with frequency of {{ $frequency }} was FAILED. ### Reason diff --git a/resources/views/emails/backup-success.blade.php b/resources/views/emails/backup-success.blade.php index e48df9e6a..d06bca6ce 100644 --- a/resources/views/emails/backup-success.blade.php +++ b/resources/views/emails/backup-success.blade.php @@ -1,3 +1,3 @@ -Database backup for {{ $name }} (db:{{ $database_name }}) with frequency of {{ $frequency }} was successful. +Database backup for {{ $name }} @if($database_name)(db:{{ $database_name }})@endif with frequency of {{ $frequency }} was successful. diff --git a/resources/views/emails/scheduled-task-failed.blade.php b/resources/views/emails/scheduled-task-failed.blade.php new file mode 100644 index 000000000..60e451823 --- /dev/null +++ b/resources/views/emails/scheduled-task-failed.blade.php @@ -0,0 +1,9 @@ + +Scheduled task ({{ $task->name }}) was FAILED with the following error: + +
+{{ $output }}
+
+ +Click [here]({{ $url }}) to view the task. +
diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 4d6f6596b..095def8a8 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -98,47 +98,6 @@ function changePasswordFieldType(event) { } } - function revive() { - if (checkHealthInterval) return true; - console.log('Checking server\'s health...') - checkHealthInterval = setInterval(() => { - fetch('/api/health') - .then(response => { - if (response.ok) { - window.toast('Coolify is back online. Reloading...', { - type: 'success', - }) - if (checkHealthInterval) clearInterval(checkHealthInterval); - setTimeout(() => { - window.location.reload(); - }, 5000) - } else { - console.log('Waiting for server to come back from dead...'); - } - }) - }, 2000); - } - - function upgrade() { - if (checkIfIamDeadInterval) return true; - console.log('Update initiated.') - checkIfIamDeadInterval = setInterval(() => { - fetch('/api/health') - .then(response => { - if (response.ok) { - console.log('It\'s alive. Waiting for server to be dead...'); - } else { - window.toast('Update done, restarting Coolify!', { - type: 'success', - }) - console.log('It\'s dead. Reviving... Standby... Bzz... Bzz...') - if (checkIfIamDeadInterval) clearInterval(checkIfIamDeadInterval); - revive(); - } - }) - }, 2000); - } - function copyToClipboard(text) { navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.'); } diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index 29bddbbaf..3df18ff92 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -57,8 +57,7 @@ class="flex flex-col items-center justify-center p-10 mx-2 mt-10 bg-white border Localhost is not reachable with the following public key.

Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for - user - 'root' or skip the boarding process and add a new private key manually to Coolify and to the + user or skip the boarding process and add a new private key manually to Coolify and to the server.
Check this documentation for further + help. Check @@ -229,10 +231,6 @@ class="font-bold underline" target="_blank" Continue - -

Username should be for now. We are working on to use - non-root users.

-
@elseif ($currentState === 'validate-server') diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index 55177d4b7..0670f5aaa 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -5,14 +5,14 @@

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 - subscription is activated.
Please be patient.
+ Your subscription has been activated! Welcome onboard!
It could take a few seconds before your + subscription is activated.
Please be patient.
@endif

Projects

@@ -23,23 +23,24 @@ @if (data_get($project, 'environments')->count() === 1) onclick="gotoProject('{{ data_get($project, 'uuid') }}', '{{ data_get($project, 'environments.0.name', 'production') }}')" @else onclick="window.location.href = '{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}'" @endif> -
-
{{ $project->name }}
-
- {{ $project->description }}
-
- - - + - Add Resource - - +
+
{{ $project->name }}
+
+ {{ $project->description }}
+
+
+
@endforeach
diff --git a/resources/views/livewire/layout-popups.blade.php b/resources/views/livewire/layout-popups.blade.php index 3f4436b8b..b2cc76f2f 100644 --- a/resources/views/livewire/layout-popups.blade.php +++ b/resources/views/livewire/layout-popups.blade.php @@ -15,7 +15,7 @@ checkPusherInterval = setInterval(() => { if (window.Echo && window.Echo.connector.pusher.connection.state !== 'connected') { checkNumber++; - if (checkNumber > 4) { + if (checkNumber > 5) { this.popups.realtime = true; console.error( 'Coolify could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/knowledge-base/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)' @@ -23,31 +23,36 @@ clearInterval(checkPusherInterval); } } - }, 1000); + }, 2000); } } }"> @auth - - - WARNING: Realtime Error?! - - - Coolify could not connect to its real-time service.
This will cause unusual problems on the UI - if - not fixed!

- Please ensure that you have opened the - required ports, - check the - related documentation or get - help on Discord.
-
- - Acknowledge & Disable This Popup - -
+ @if (!isCloud()) + + + WARNING: Realtime Error?! + + + Coolify could not connect to its real-time service.
This will cause unusual problems on the + UI + if + not fixed!

+ Please ensure that you have opened the + required ports, + check the + related documentation or get + help on Discord. +
+
+ + Acknowledge & Disable This Popup + +
+ @endif
@endauth diff --git a/resources/views/livewire/notifications/discord.blade.php b/resources/views/livewire/notifications/discord.blade.php index 95fc9aae9..d85f59600 100644 --- a/resources/views/livewire/notifications/discord.blade.php +++ b/resources/views/livewire/notifications/discord.blade.php @@ -32,6 +32,8 @@ label="Application Deployments" /> +
@endif
diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 9a89057a7..cccd4c26e 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -111,6 +111,8 @@ label="Application Deployments" /> + @endif diff --git a/resources/views/livewire/notifications/telegram.blade.php b/resources/views/livewire/notifications/telegram.blade.php index 4925b9f75..01d8b1329 100644 --- a/resources/views/livewire/notifications/telegram.blade.php +++ b/resources/views/livewire/notifications/telegram.blade.php @@ -7,7 +7,7 @@ Save @if ($team->telegram_enabled) - Send Test Notifications @@ -18,44 +18,57 @@
@if (data_get($team, 'telegram_enabled'))

Subscribe to events

-
+
@if (isDev()) -
+
+

Test Notification

+ label="Enabled" />
@endif -
+
+

Container Status Changes

+ label="Enabled" />
-
+
+

Application Deployments

+ label="Enabled" />
-
+
+

Database Backup Status

+ label="Enabled" />
+
+

Scheduled Tasks Status

+ + +
+
@endif diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index 5d2922a7e..ff96b228a 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -1,10 +1,10 @@
-
+

Advanced

Advanced configuration for your application.
-
+

General

@if ($application->git_based()) - @@ -30,13 +26,22 @@ label="Raw Compose Deployment" helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the documentation." /> @endif +

Container Names

+ +
+ + Save + @if ($application->build_pack === 'dockercompose')

Network

-
- -
+ @endif @if (!$application->settings->is_raw_compose_deployment_enabled)

Logs

@@ -60,16 +65,14 @@ @endif
@if ($application->build_pack !== 'dockercompose') -
- - @if ($application->settings->is_gpu_enabled) -
GPU Settings
+ + @if ($application->settings->is_gpu_enabled) +
GPU Settings
- Save - @endif -
+ Save + @endif @endif @if ($application->settings->is_gpu_enabled)
diff --git a/resources/views/livewire/project/application/configuration.blade.php b/resources/views/livewire/project/application/configuration.blade.php index 4aaf96569..89f771e7f 100644 --- a/resources/views/livewire/project/application/configuration.blade.php +++ b/resources/views/livewire/project/application/configuration.blade.php @@ -2,8 +2,8 @@

Configuration

-
-
+
+
General @if ($application->destination->server->isSwarm()) @@ -78,7 +78,7 @@ @click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
-
+
diff --git a/resources/views/livewire/project/application/deployment/index.blade.php b/resources/views/livewire/project/application/deployment/index.blade.php index 9c5e44080..ea7262a27 100644 --- a/resources/views/livewire/project/application/deployment/index.blade.php +++ b/resources/views/livewire/project/application/deployment/index.blade.php @@ -27,18 +27,15 @@ class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> @endif @forelse ($deployments as $deployment) - - data_get($deployment, 'status') === 'queued', - 'border-warning hover:bg-warning hover:text-black' => +
data_get($deployment, 'status') === 'in_progress' || data_get($deployment, 'status') === 'cancelled-by-user', - 'border-error dark:hover:bg-error hover:bg-neutral-200' => - data_get($deployment, 'status') === 'failed', - 'border-success dark:hover:bg-success hover:bg-neutral-200' => - data_get($deployment, 'status') === 'finished', - ]) href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}"> + 'border-error' => data_get($deployment, 'status') === 'failed', + 'border-success' => data_get($deployment, 'status') === 'finished', + ]) + x-on:click.stop="goto('{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}')">
{{ $deployment->created_at }} UTC @@ -64,11 +61,27 @@ class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> @endif
@else -
- Manual +
+ @if (data_get($deployment, 'rollback') === true) + Rollback + @else + Manual + @endif + @if (data_get($deployment, 'commit')) +
+
+ @if ($deployment->commitMessage()) + ({{data_get_str($deployment, 'commit')->limit(7)}} - {{ $deployment->commitMessage() }}) + @else + {{ data_get_str($deployment, 'commit')->limit(7) }} + @endif +
+
+ @endif
@endif - @if (data_get($deployment, 'server_name')) + @if (data_get($deployment, 'server_name') && $application->additional_servers->count() > 0)
Server: {{ data_get($deployment, 'server_name') }}
@@ -85,15 +98,19 @@ class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> 0s
-
+
@empty
No deployments found
@endforelse + @if ($deployments_count > 0) - @endscript + Start + + @endif + @script + + @endscript +
diff --git a/resources/views/livewire/project/database/postgresql/general.blade.php b/resources/views/livewire/project/database/postgresql/general.blade.php index 1a9651155..a5f59d6d0 100644 --- a/resources/views/livewire/project/database/postgresql/general.blade.php +++ b/resources/views/livewire/project/database/postgresql/general.blade.php @@ -20,7 +20,7 @@ Save
-
+
@if ($database->started_at) -
+
@forelse($database->scheduledBackups as $backup) @if ($type == 'database') - + @else -
+
data_get($backup, 'id') === data_get($selectedBackup, 'id'), 'flex flex-col border-l-2 border-transparent', - ]) wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')"> + ])>
Frequency: {{ $backup->frequency }}
Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}
Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}
diff --git a/resources/views/livewire/project/environment-edit.blade.php b/resources/views/livewire/project/environment-edit.blade.php index 6439a128d..842692296 100644 --- a/resources/views/livewire/project/environment-edit.blade.php +++ b/resources/views/livewire/project/environment-edit.blade.php @@ -6,22 +6,30 @@