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 @@ class StartClickhouse $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 @@ class StartDatabaseProxy 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 @@ class StartDragonfly $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 @@ class StartKeydb $this->add_custom_keydb(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -96,7 +95,7 @@ class StartKeydb 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 @@ class StartKeydb } 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 @@ class StartMariadb $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 @@ class StartMariadb 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 @@ class StartMariadb } 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..99403c2c1 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -35,7 +35,6 @@ class StartMongodb $this->add_custom_mongo_conf(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -97,7 +96,7 @@ class StartMongodb 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 +177,7 @@ class StartMongodb } 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 @@ class StartMysql $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 @@ class StartMysql 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 @@ class StartMysql } 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 @@ class StartPostgresql $this->add_custom_conf(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -78,7 +77,6 @@ class StartPostgresql 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 @@ class StartPostgresql ]; } } - 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 @@ class StartPostgresql 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 @@ class StartPostgresql } 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 @@ class StartRedis $this->add_custom_redis(); $docker_compose = [ - 'version' => '3.8', 'services' => [ $container_name => [ 'image' => $this->database->image, @@ -100,7 +99,7 @@ class StartRedis 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 @@ class StartRedis } 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..667a8c92e --- /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 = $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 = $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 @@ +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) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index e3221e091..69bfa29d0 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; @@ -302,7 +303,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted { 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) { @@ -1020,7 +1022,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted "command" => "docker rm -f {$this->deployment_uuid}", "ignore_errors" => true, "hidden" => true - ], + ] + ); + $this->execute_remote_command( [ $runCommand, "hidden" => true, @@ -1287,7 +1291,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->application->parseHealthcheckFromDockerfile($dockerfile); } $docker_compose = [ - 'version' => '3.8', 'services' => [ $this->container_name => [ 'image' => $this->production_image_name, diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index a9cec009b..9ffd68f11 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\Foundation\Bus\Dispatchable; 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,337 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted 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; - } + GetContainersStatus::run($this->server); + return; + // 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 = []; + // $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'); + // 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 ($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']); - } + // 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']); + // $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'); + // $name = data_get($application, 'name'); + // $fqdn = data_get($application, 'fqdn'); - $containerName = $name ? "$name ($fqdn)" : $fqdn; + // $containerName = $name ? "$name ($fqdn)" : $fqdn; - $projectUuid = data_get($application, 'environment.project.uuid'); - $applicationUuid = data_get($application, 'uuid'); - $environment = data_get($application, 'environment.name'); + // $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; - } + // 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']); + // $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'); + // $name = data_get($preview, 'name'); + // $fqdn = data_get($preview, 'fqdn'); - $containerName = $name ? "$name ($fqdn)" : $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'); + // $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; - } + // 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']); + // $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'); + // $name = data_get($database, 'name'); + // $fqdn = data_get($database, 'fqdn'); - $containerName = $name; + // $containerName = $name; - $projectUuid = data_get($database, 'environment.project.uuid'); - $environmentName = data_get($database, 'environment.name'); - $databaseUuid = data_get($database, 'uuid'); + // $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)); - } + // 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); - } + // // 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); + // } } } 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/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php index 6402b81cf..449ab85a0 100644 --- a/app/Jobs/ServerStatusJob.php +++ b/app/Jobs/ServerStatusJob.php @@ -44,6 +44,9 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted 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()); diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 9adee7003..06b9a3c3c 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Application; use App\Actions\Application\StopApplication; +use App\Actions\Docker\GetContainersStatus; use App\Events\ApplicationStatusChanged; use App\Jobs\ContainerStatusJob; use App\Jobs\ServerStatusJob; @@ -32,7 +33,8 @@ class Heading extends Component 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)); } 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\StartMysql; 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 @@ class Heading extends Component 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/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 @@ class GithubPrivateRepository extends Component '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 @@ class GithubPrivateRepository extends Component $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 @@ class GithubPrivateRepositoryDeployKey extends Component '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 @@ class GithubPrivateRepositoryDeployKey extends Component '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 @@ class GithubPrivateRepositoryDeployKey extends Component '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 b71a0b670..f4f3008d4 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -205,6 +205,9 @@ class PublicGitRepository extends Component ]; } + 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/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index 2cbda4e02..0b26af22f 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Service; +use App\Actions\Docker\GetContainersStatus; use App\Jobs\ContainerStatusJob; use App\Models\Service; use Livewire\Component; @@ -64,7 +65,8 @@ class Configuration extends Component 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/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 @@ class Destination extends Component } 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/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 @@ class ExecuteContainerCommand extends Component 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/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 @@ class Logs extends Component 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 @@ class Logs extends Component } } $this->containers = $this->containers->sort(); + $this->loadMetrics(); } catch (\Exception $e) { return handleError($e, $this); } 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 @@ class Status extends Component 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/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 @@ class Deployments extends Component "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 @@ class Index extends Component 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/Models/Server.php b/app/Models/Server.php index 19da8d784..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; @@ -462,6 +465,36 @@ $schema://$host { 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 @@ $schema://$host { { 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 5de1a9745..cd8e578d6 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -171,7 +171,7 @@ class Service extends BaseModel ], ]); } - $fields->put('Tolgee', $data); + $fields->put('Tolgee', $data->toArray()); break; case str($image)?->contains('logto'): $data = collect([]); @@ -195,7 +195,7 @@ class Service extends BaseModel ], ]); } - $fields->put('Logto', $data); + $fields->put('Logto', $data->toArray()); break; case str($image)?->contains('unleash-server'): $data = collect([]); @@ -218,7 +218,7 @@ class Service extends BaseModel ], ]); } - $fields->put('Unleash', $data); + $fields->put('Unleash', $data->toArray()); break; case str($image)?->contains('grafana'): $data = collect([]); @@ -241,7 +241,7 @@ class Service extends BaseModel ], ]); } - $fields->put('Grafana', $data); + $fields->put('Grafana', $data->toArray()); break; case str($image)?->contains('directus'): $data = collect([]); @@ -267,7 +267,7 @@ class Service extends BaseModel ], ]); } - $fields->put('Directus', $data); + $fields->put('Directus', $data->toArray()); break; case str($image)?->contains('kong'): $data = collect([]); @@ -370,7 +370,7 @@ class Service extends BaseModel ], ]); } - $fields->put('Weblate', $data); + $fields->put('Weblate', $data->toArray()); break; case str($image)?->contains('meilisearch'): $data = collect([]); @@ -384,7 +384,7 @@ class Service extends BaseModel ], ]); } - $fields->put('Meilisearch', $data); + $fields->put('Meilisearch', $data->toArray()); break; case str($image)?->contains('ghost'): $data = collect([]); @@ -444,7 +444,31 @@ class Service extends BaseModel ]); } - $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(); + $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; } } 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 @@ class Revived extends Notification implements ShouldQueue 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/shared.php b/bootstrap/helpers/shared.php index d5cacb06c..b93b0ec3d 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -147,6 +147,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 +649,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([]); @@ -1192,7 +1203,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $service; }); $finalServices = [ - 'version' => $dockerComposeVersion, 'services' => $services->toArray(), 'volumes' => $topLevelVolumes->toArray(), 'networks' => $topLevelNetworks->toArray(), @@ -1230,7 +1240,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([]); @@ -1661,7 +1670,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal }); } $finalServices = [ - 'version' => $dockerComposeVersion, 'services' => $services->toArray(), 'volumes' => $topLevelVolumes->toArray(), 'networks' => $topLevelNetworks->toArray(), 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 @@ return [ '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 f36eb7818..e65072dcc 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.276', + 'release' => '4.0.0-beta.277', // 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 ae9fc8db5..fea91e1f0 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ 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/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/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/resources/css/app.css b/resources/css/app.css index 5858f5cc9..bb05b783b 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -203,7 +203,7 @@ tr td:first-child { @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]; diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index 28850d5e9..0670f5aaa 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -9,10 +9,10 @@ + d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> Your subscription has been activated! Welcome onboard!
It could take a few seconds before your - subscription is activated.
Please be patient. + 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 }}
+
+
+ + + + Add Resource + + Settings - +
+ @endforeach diff --git a/resources/views/livewire/project/new/github-private-repository.blade.php b/resources/views/livewire/project/new/github-private-repository.blade.php index 21ffb2f67..f5df598db 100644 --- a/resources/views/livewire/project/new/github-private-repository.blade.php +++ b/resources/views/livewire/project/new/github-private-repository.blade.php @@ -33,14 +33,16 @@ +
+ +
@endforeach @endif @if ($current_step === 'repository') @if ($repositories->count() > 0)
- + @foreach ($repositories as $repo) @if ($loop->first)
@endif + {{--
+
+
+ + --}} diff --git a/resources/views/livewire/project/shared/resource-operations.blade.php b/resources/views/livewire/project/shared/resource-operations.blade.php index 380b5ba91..8853c938d 100644 --- a/resources/views/livewire/project/shared/resource-operations.blade.php +++ b/resources/views/livewire/project/shared/resource-operations.blade.php @@ -6,7 +6,7 @@
@foreach ($servers->sortBy('id') as $server) -
Server: {{ $server->name }}
+
Server: {{ $server->name }}
@foreach ($server->destinations() as $destination) @@ -33,7 +33,7 @@
@forelse ($projects as $project) -
Project: {{ $project->name }}
+
Project: {{ $project->name }}
@foreach ($project->environments as $environment) diff --git a/resources/views/livewire/project/show.blade.php b/resources/views/livewire/project/show.blade.php index f4c79c7e8..f180787c9 100644 --- a/resources/views/livewire/project/show.blade.php +++ b/resources/views/livewire/project/show.blade.php @@ -11,18 +11,26 @@ @forelse ($project->environments as $environment) @empty

No environments found.

diff --git a/resources/views/livewire/server/resources.blade.php b/resources/views/livewire/server/resources.blade.php index a5ff69ea1..29648cafe 100644 --- a/resources/views/livewire/server/resources.blade.php +++ b/resources/views/livewire/server/resources.blade.php @@ -1,7 +1,7 @@
-
-
+
+
Managed +
@forelse ($deployments_per_tag_per_server as $server_name => $deployments)

{{ $server_name }}

-
+
@foreach ($deployments as $deployment) - @empty diff --git a/resources/views/livewire/tags/index.blade.php b/resources/views/livewire/tags/index.blade.php index 1fd62cefe..f91d4f00e 100644 --- a/resources/views/livewire/tags/index.blade.php +++ b/resources/views/livewire/tags/index.blade.php @@ -2,7 +2,7 @@

Tags

Tags help you to perform actions on multiple resources.
-
+
@if ($tags->count() === 0)
No tags yet defined yet. Go to a resource and add a tag there.
@else diff --git a/resources/views/livewire/upgrade.blade.php b/resources/views/livewire/upgrade.blade.php index 7447189ac..1e3ad014b 100644 --- a/resources/views/livewire/upgrade.blade.php +++ b/resources/views/livewire/upgrade.blade.php @@ -10,7 +10,7 @@ - Inprogress + In progress @else