diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 2c74f09dc..601b8e991 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -3,6 +3,8 @@ namespace App\Actions\Application; use App\Models\Application; +use App\Models\StandaloneDocker; +use App\Notifications\Application\StatusChanged; use Lorisleiva\Actions\Concerns\AsAction; class StopApplication @@ -10,13 +12,20 @@ class StopApplication use AsAction; public function handle(Application $application) { - $server = $application->destination->server; - if (!$server->isFunctional()) { - return 'Server is not functional'; + if ($application->destination->server->isSwarm()) { + instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server); + return; } - if ($server->isSwarm()) { - instant_remote_process(["docker stack rm {$application->uuid}" ], $server); - } else { + + $servers = collect([]); + $servers->push($application->destination->server); + $application->additional_servers->map(function ($server) use ($servers) { + $servers->push($server); + }); + foreach ($servers as $server) { + if (!$server->isFunctional()) { + return 'Server is not functional'; + } $containers = getCurrentApplicationContainerStatus($server, $application->id, 0); if ($containers->count() > 0) { foreach ($containers as $container) { @@ -28,20 +37,7 @@ class StopApplication ); } } - // TODO: make notification for application - // $application->environment->project->team->notify(new StatusChanged($application)); - } - // Delete Preview Deployments - $previewDeployments = $application->previews; - foreach ($previewDeployments as $previewDeployment) { - $containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id); - foreach ($containers as $container) { - $name = str_replace('/', '', $container['Names']); - instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false); - } } } - - } } diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php new file mode 100644 index 000000000..1945a94bd --- /dev/null +++ b/app/Actions/Application/StopApplicationOneServer.php @@ -0,0 +1,38 @@ +destination->server->isSwarm()) { + return; + } + if (!$server->isFunctional()) { + return 'Server is not functional'; + } + try { + $containers = getCurrentApplicationContainerStatus($server, $application->id, 0); + if ($containers->count() > 0) { + foreach ($containers as $container) { + $containerName = data_get($container, 'Names'); + if ($containerName) { + instant_remote_process( + ["docker rm -f {$containerName}"], + $server + ); + } + } + } + } catch (\Exception $e) { + ray($e->getMessage()); + return $e->getMessage(); + } + } +} diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index e1ce9eba1..343b6d364 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -10,24 +10,31 @@ class StopService use AsAction; public function handle(Service $service) { - $server = $service->destination->server; - if (!$server->isFunctional()) { - return 'Server is not functional'; + try { + $server = $service->destination->server; + if (!$server->isFunctional()) { + return 'Server is not functional'; + } + ray('Stopping service: ' . $service->name); + $applications = $service->applications()->get(); + foreach ($applications as $application) { + instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server); + $application->update(['status' => 'exited']); + } + $dbs = $service->databases()->get(); + foreach ($dbs as $db) { + instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server); + $db->update(['status' => 'exited']); + } + instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false); + instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false); + // TODO: make notification for databases + // $service->environment->project->team->notify(new StatusChanged($service)); + } catch (\Exception $e) { + echo $e->getMessage(); + ray($e->getMessage()); + return $e->getMessage(); } - ray('Stopping service: ' . $service->name); - $applications = $service->applications()->get(); - foreach ($applications as $application) { - instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server); - $application->update(['status' => 'exited']); - } - $dbs = $service->databases()->get(); - foreach ($dbs as $db) { - instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server); - $db->update(['status' => 'exited']); - } - instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false); - instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false); - // TODO: make notification for databases - // $service->environment->project->team->notify(new StatusChanged($service)); + } } diff --git a/app/Actions/Shared/ComplexStatusCheck.php b/app/Actions/Shared/ComplexStatusCheck.php new file mode 100644 index 000000000..7987257f2 --- /dev/null +++ b/app/Actions/Shared/ComplexStatusCheck.php @@ -0,0 +1,55 @@ +additional_servers; + $servers->push($application->destination->server); + foreach ($servers as $server) { + $is_main_server = $application->destination->server->id === $server->id; + if (!$server->isFunctional()) { + if ($is_main_server) { + $application->update(['status' => 'exited:unhealthy']); + continue; + } else { + $application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']); + continue; + } + } + $container = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false); + $container = format_docker_command_output_to_json($container); + if ($container->count() === 1) { + $container = $container->first(); + $containerStatus = data_get($container, 'State.Status'); + $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + if ($is_main_server) { + $statusFromDb = $application->status; + if ($statusFromDb !== $containerStatus) { + $application->update(['status' => "$containerStatus:$containerHealth"]); + } + } else { + $additional_server = $application->additional_servers()->wherePivot('server_id', $server->id); + $statusFromDb = $additional_server->first()->pivot->status; + if ($statusFromDb !== $containerStatus) { + $additional_server->updateExistingPivot($server->id, ['status' => "$containerStatus:$containerHealth"]); + } + } + } else { + if ($is_main_server) { + $application->update(['status' => 'exited:unhealthy']); + continue; + } else { + $application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']); + continue; + } + } + } + } +} diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index 1674c01d8..38d87e24b 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -20,7 +20,8 @@ class CleanupStuckedResources extends Command public function handle() { - echo "Running cleanup stucked...\n"; + ray('Running cleanup stucked resources.'); + echo "Running cleanup stucked resources.\n"; $this->cleanup_stucked_resources(); } private function cleanup_stucked_resources() @@ -113,18 +114,18 @@ class CleanupStuckedResources extends Command $applications = Application::all(); foreach ($applications as $application) { if (!data_get($application, 'environment')) { - echo 'Application without environment: ' . $application->name . ' soft deleting\n'; - $application->delete(); + echo 'Application without environment: ' . $application->name . '\n'; + $application->forceDelete(); continue; } if (!$application->destination()) { - echo 'Application without destination: ' . $application->name . ' soft deleting\n'; - $application->delete(); + echo 'Application without destination: ' . $application->name . '\n'; + $application->forceDelete(); continue; } if (!data_get($application, 'destination.server')) { - echo 'Application without server: ' . $application->name . ' soft deleting\n'; - $application->delete(); + echo 'Application without server: ' . $application->name . '\n'; + $application->forceDelete(); continue; } } @@ -135,18 +136,18 @@ class CleanupStuckedResources extends Command $postgresqls = StandalonePostgresql::all()->where('id', '!=', 0); foreach ($postgresqls as $postgresql) { if (!data_get($postgresql, 'environment')) { - echo 'Postgresql without environment: ' . $postgresql->name . ' soft deleting\n'; - $postgresql->delete(); + echo 'Postgresql without environment: ' . $postgresql->name . '\n'; + $postgresql->forceDelete(); continue; } if (!$postgresql->destination()) { - echo 'Postgresql without destination: ' . $postgresql->name . ' soft deleting\n'; - $postgresql->delete(); + echo 'Postgresql without destination: ' . $postgresql->name . '\n'; + $postgresql->forceDelete(); continue; } if (!data_get($postgresql, 'destination.server')) { - echo 'Postgresql without server: ' . $postgresql->name . ' soft deleting\n'; - $postgresql->delete(); + echo 'Postgresql without server: ' . $postgresql->name . '\n'; + $postgresql->forceDelete(); continue; } } @@ -157,18 +158,18 @@ class CleanupStuckedResources extends Command $redis = StandaloneRedis::all(); foreach ($redis as $redis) { if (!data_get($redis, 'environment')) { - echo 'Redis without environment: ' . $redis->name . ' soft deleting\n'; - $redis->delete(); + echo 'Redis without environment: ' . $redis->name . '\n'; + $redis->forceDelete(); continue; } if (!$redis->destination()) { - echo 'Redis without destination: ' . $redis->name . ' soft deleting\n'; - $redis->delete(); + echo 'Redis without destination: ' . $redis->name . '\n'; + $redis->forceDelete(); continue; } if (!data_get($redis, 'destination.server')) { - echo 'Redis without server: ' . $redis->name . ' soft deleting\n'; - $redis->delete(); + echo 'Redis without server: ' . $redis->name . '\n'; + $redis->forceDelete(); continue; } } @@ -180,18 +181,18 @@ class CleanupStuckedResources extends Command $mongodbs = StandaloneMongodb::all(); foreach ($mongodbs as $mongodb) { if (!data_get($mongodb, 'environment')) { - echo 'Mongodb without environment: ' . $mongodb->name . ' soft deleting\n'; - $mongodb->delete(); + echo 'Mongodb without environment: ' . $mongodb->name . '\n'; + $mongodb->forceDelete(); continue; } if (!$mongodb->destination()) { - echo 'Mongodb without destination: ' . $mongodb->name . ' soft deleting\n'; - $mongodb->delete(); + echo 'Mongodb without destination: ' . $mongodb->name . '\n'; + $mongodb->forceDelete(); continue; } if (!data_get($mongodb, 'destination.server')) { - echo 'Mongodb without server: ' . $mongodb->name . ' soft deleting\n'; - $mongodb->delete(); + echo 'Mongodb without server: ' . $mongodb->name . '\n'; + $mongodb->forceDelete(); continue; } } @@ -203,18 +204,18 @@ class CleanupStuckedResources extends Command $mysqls = StandaloneMysql::all(); foreach ($mysqls as $mysql) { if (!data_get($mysql, 'environment')) { - echo 'Mysql without environment: ' . $mysql->name . ' soft deleting\n'; - $mysql->delete(); + echo 'Mysql without environment: ' . $mysql->name . '\n'; + $mysql->forceDelete(); continue; } if (!$mysql->destination()) { - echo 'Mysql without destination: ' . $mysql->name . ' soft deleting\n'; - $mysql->delete(); + echo 'Mysql without destination: ' . $mysql->name . '\n'; + $mysql->forceDelete(); continue; } if (!data_get($mysql, 'destination.server')) { - echo 'Mysql without server: ' . $mysql->name . ' soft deleting\n'; - $mysql->delete(); + echo 'Mysql without server: ' . $mysql->name . '\n'; + $mysql->forceDelete(); continue; } } @@ -226,18 +227,18 @@ class CleanupStuckedResources extends Command $mariadbs = StandaloneMariadb::all(); foreach ($mariadbs as $mariadb) { if (!data_get($mariadb, 'environment')) { - echo 'Mariadb without environment: ' . $mariadb->name . ' soft deleting\n'; - $mariadb->delete(); + echo 'Mariadb without environment: ' . $mariadb->name . '\n'; + $mariadb->forceDelete(); continue; } if (!$mariadb->destination()) { - echo 'Mariadb without destination: ' . $mariadb->name . ' soft deleting\n'; - $mariadb->delete(); + echo 'Mariadb without destination: ' . $mariadb->name . '\n'; + $mariadb->forceDelete(); continue; } if (!data_get($mariadb, 'destination.server')) { - echo 'Mariadb without server: ' . $mariadb->name . ' soft deleting\n'; - $mariadb->delete(); + echo 'Mariadb without server: ' . $mariadb->name . '\n'; + $mariadb->forceDelete(); continue; } } @@ -249,18 +250,18 @@ class CleanupStuckedResources extends Command $services = Service::all(); foreach ($services as $service) { if (!data_get($service, 'environment')) { - echo 'Service without environment: ' . $service->name . ' soft deleting\n'; - $service->delete(); + echo 'Service without environment: ' . $service->name . '\n'; + $service->forceDelete(); continue; } if (!$service->destination()) { - echo 'Service without destination: ' . $service->name . ' soft deleting\n'; - $service->delete(); + echo 'Service without destination: ' . $service->name . '\n'; + $service->forceDelete(); continue; } if (!data_get($service, 'server')) { - echo 'Service without server: ' . $service->name . ' soft deleting\n'; - $service->delete(); + echo 'Service without server: ' . $service->name . '\n'; + $service->forceDelete(); continue; } } @@ -271,8 +272,8 @@ class CleanupStuckedResources extends Command $serviceApplications = ServiceApplication::all(); foreach ($serviceApplications as $service) { if (!data_get($service, 'service')) { - echo 'ServiceApplication without service: ' . $service->name . ' soft deleting\n'; - $service->delete(); + echo 'ServiceApplication without service: ' . $service->name . '\n'; + $service->forceDelete(); continue; } } @@ -283,8 +284,8 @@ class CleanupStuckedResources extends Command $serviceDatabases = ServiceDatabase::all(); foreach ($serviceDatabases as $service) { if (!data_get($service, 'service')) { - echo 'ServiceDatabase without service: ' . $service->name . ' soft deleting\n'; - $service->delete(); + echo 'ServiceDatabase without service: ' . $service->name . '\n'; + $service->forceDelete(); continue; } } diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index c4dc20988..315f256c0 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -14,39 +14,44 @@ use Illuminate\Support\Facades\Http; class Init extends Command { - protected $signature = 'app:init {--cleanup}'; + protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments}'; protected $description = 'Cleanup instance related stuffs'; public function handle() { $this->alive(); - $cleanup = $this->option('cleanup'); - if ($cleanup) { - echo "Running cleanups...\n"; - $this->call('cleanup:stucked-resources'); + $full_cleanup = $this->option('full-cleanup'); + $cleanup_deployments = $this->option('cleanup-deployments'); + if ($cleanup_deployments) { + echo "Running cleanup deployments.\n"; + $this->cleanup_in_progress_application_deployments(); + return; + } + if ($full_cleanup) { // Required for falsely deleted coolify db $this->restore_coolify_db_backup(); - - // $this->cleanup_ssh(); - } - $this->cleanup_in_progress_application_deployments(); - $this->cleanup_stucked_helper_containers(); - - try { - setup_dynamic_configuration(); - } catch (\Throwable $e) { - echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; - } - - $settings = InstanceSettings::get(); - if (!is_null(env('AUTOUPDATE', null))) { - if (env('AUTOUPDATE') == true) { - $settings->update(['is_auto_update_enabled' => true]); - } else { - $settings->update(['is_auto_update_enabled' => false]); + $this->cleanup_in_progress_application_deployments(); + $this->cleanup_stucked_helper_containers(); + $this->call('cleanup:queue'); + $this->call('cleanup:stucked-resources'); + try { + setup_dynamic_configuration(); + } catch (\Throwable $e) { + echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; } + + $settings = InstanceSettings::get(); + if (!is_null(env('AUTOUPDATE', null))) { + if (env('AUTOUPDATE') == true) { + $settings->update(['is_auto_update_enabled' => true]); + } else { + $settings->update(['is_auto_update_enabled' => false]); + } + } + return; } - $this->call('cleanup:queue'); + $this->cleanup_stucked_helper_containers(); + $this->call('cleanup:stucked-resources'); } private function restore_coolify_db_backup() { @@ -120,8 +125,10 @@ class Init extends Command // Cleanup any failed deployments try { - $halted_deployments = ApplicationDeploymentQueue::where('status', '==', ApplicationDeploymentStatus::IN_PROGRESS)->where('status', '==', ApplicationDeploymentStatus::QUEUED)->get(); - foreach ($halted_deployments as $deployment) { + $queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get(); + foreach ($queued_inprogress_deployments as $deployment) { + ray($deployment->id, $deployment->status); + echo "Cleaning up deployment: {$deployment->id}\n"; $deployment->status = ApplicationDeploymentStatus::FAILED->value; $deployment->save(); } @@ -129,5 +136,4 @@ class Init extends Command echo "Error: {$e->getMessage()}\n"; } } - } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index c56feb902..37266ca5d 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -4,6 +4,7 @@ namespace App\Console; use App\Jobs\CheckLogDrainContainerJob; use App\Jobs\CleanupInstanceStuffsJob; +use App\Jobs\ComplexContainerStatusJob; use App\Jobs\DatabaseBackupJob; use App\Jobs\ScheduledTaskJob; use App\Jobs\InstanceAutoUpdateJob; @@ -91,7 +92,6 @@ class Kernel extends ConsoleKernel { $scheduled_backups = ScheduledDatabaseBackup::all(); if ($scheduled_backups->isEmpty()) { - ray('no scheduled backups'); return; } foreach ($scheduled_backups as $scheduled_backup) { @@ -117,7 +117,6 @@ class Kernel extends ConsoleKernel { $scheduled_tasks = ScheduledTask::all(); if ($scheduled_tasks->isEmpty()) { - ray('no scheduled tasks'); return; } foreach ($scheduled_tasks as $scheduled_task) { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 9d0e069ce..1b14e7e12 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -123,6 +123,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); } $this->server = Server::find($this->application_deployment_queue->server_id); + $this->timeout = $this->server->settings->dynamic_timeout; $this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first(); $this->server = $this->mainServer = $this->destination->server; $this->serverUser = $this->server->user; @@ -248,9 +249,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted dispatch(new ContainerStatusJob($this->server)); } // Otherwise built image needs to be pushed before from the build server. - if (!$this->use_build_server) { - $this->push_to_docker_registry(); - } + // ray($this->use_build_server); + // if (!$this->use_build_server) { + // if ($this->application->additional_servers->count() > 0) { + // $this->push_to_docker_registry(forceFail: true); + // } else { + // $this->push_to_docker_registry(); + // } + // } $this->next(ApplicationDeploymentStatus::FINISHED->value); if ($this->pull_request_id !== 0) { if ($this->application->is_github_based()) { @@ -288,162 +294,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } } - private function write_deployment_configurations() - { - if (isset($this->docker_compose_base64)) { - if ($this->use_build_server) { - $this->server = $this->original_server; - } - $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); - $composeFileName = "$this->configuration_dir/docker-compose.yml"; - if ($this->pull_request_id !== 0) { - $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml"; - } - $this->execute_remote_command( - [ - "mkdir -p $this->configuration_dir" - ], - [ - "echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName", - ], - [ - "echo '{$readme}' > $this->configuration_dir/README.md", - ] - ); - if ($this->use_build_server) { - $this->server = $this->build_server; - } - } - } - private function push_to_docker_registry($forceFail = false) - { - if ( - $this->application->docker_registry_image_name && - $this->application->build_pack !== 'dockerimage' && - !$this->application->destination->server->isSwarm() && - !$this->restart_only && - !(str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) - ) { - try { - instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); - $this->application_deployment_queue->addLogEntry("----------------------------------------"); - $this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name})."); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true - ], - ); - if ($this->application->docker_registry_image_tag) { - // Tag image with latest - $this->application_deployment_queue->addLogEntry("Tagging and pushing image with latest tag."); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true - ], - [ - executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true - ], - ); - } - $this->application_deployment_queue->addLogEntry("Image pushed to docker registry."); - } catch (Exception $e) { - $this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information."); - if ($forceFail) { - throw $e; - } - ray($e); - } - } - } - private function generate_image_names() - { - if ($this->application->dockerfile) { - if ($this->application->docker_registry_image_name) { - $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build"); - $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest"); - } else { - $this->build_image_name = Str::lower("{$this->application->uuid}:build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); - } - } else if ($this->application->build_pack === 'dockerimage') { - $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); - } else if ($this->pull_request_id !== 0) { - if ($this->application->docker_registry_image_name) { - $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build"); - $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}"); - } else { - $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); - } - } else { - $this->dockerImageTag = str($this->commit)->substr(0, 128); - if ($this->application->docker_registry_image_name) { - $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build"); - $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}"); - } else { - $this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}"); - } - } - } - private function just_restart() - { - $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); - $this->prepare_builder_image(); - $this->check_git_if_build_needed(); - $this->set_base_dir(); - $this->generate_image_names(); - $this->check_image_locally_or_remotely(); - if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { - $this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container."); - $this->create_workdir(); - $this->generate_compose_file(); - $this->rolling_update(); - return; - } - throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.'); - } - private function check_image_locally_or_remotely() - { - $this->execute_remote_command([ - "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" - ]); - if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) { - $this->execute_remote_command([ - "docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true - ]); - $this->execute_remote_command([ - "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" - ]); - } - } - private function save_environment_variables() - { - $envs = collect([]); - if ($this->pull_request_id !== 0) { - foreach ($this->application->environment_variables_preview as $env) { - $envs->push($env->key . '=' . $env->real_value); - } - } else { - foreach ($this->application->environment_variables as $env) { - $envs->push($env->key . '=' . $env->real_value); - } - } - $envs_base64 = base64_encode($envs->implode("\n")); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") - ], - ); - } - private function deploy_simple_dockerfile() { if ($this->use_build_server) { $this->server = $this->build_server; } $dockerfile_base64 = base64_encode($this->application->dockerfile); - $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}."); $this->prepare_builder_image(); $this->execute_remote_command( [ @@ -451,19 +308,30 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ], ); $this->generate_image_names(); + if (!$this->force_rebuild) { + $this->check_image_locally_or_remotely(); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { + $this->create_workdir(); + $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); + $this->generate_compose_file(); + $this->push_to_docker_registry(); + $this->rolling_update(); + return; + } + } $this->generate_compose_file(); $this->generate_build_env_variables(); $this->add_build_env_variables_to_dockerfile(); $this->build_image(); + $this->push_to_docker_registry(); $this->rolling_update(); } - private function deploy_dockerimage_buildpack() { $this->dockerImage = $this->application->docker_registry_image_name; $this->dockerImageTag = $this->application->docker_registry_image_tag; - ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"); - $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}."); + ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'"); + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); $this->generate_image_names(); $this->prepare_builder_image(); $this->generate_compose_file(); @@ -481,9 +349,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command; } if ($this->pull_request_id === 0) { - $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}."); } else { - $this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}."); + $this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}."); } $this->prepare_builder_image(); $this->check_git_if_build_needed(); @@ -551,25 +419,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if (data_get($this->application, 'dockerfile_location')) { $this->dockerfile_location = $this->application->dockerfile_location; } - $this->application_deployment_queue->addLogEntry("Starting 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->generate_image_names(); - $this->cleanup_git(); - $this->generate_compose_file(); - $this->generate_build_env_variables(); - $this->add_build_env_variables_to_dockerfile(); - $this->build_image(); - $this->rolling_update(); - } - private function deploy_nixpacks_buildpack() - { - if ($this->use_build_server) { - $this->server = $this->build_server; - } - $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}."); $this->prepare_builder_image(); $this->check_git_if_build_needed(); $this->set_base_dir(); @@ -580,6 +430,38 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->create_workdir(); $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); $this->generate_compose_file(); + $this->push_to_docker_registry(); + $this->rolling_update(); + return; + } + } + $this->clone_repository(); + $this->cleanup_git(); + $this->generate_compose_file(); + $this->generate_build_env_variables(); + $this->add_build_env_variables_to_dockerfile(); + $this->build_image(); + $this->push_to_docker_registry(); + $this->rolling_update(); + } + private function deploy_nixpacks_buildpack() + { + if ($this->use_build_server) { + $this->server = $this->build_server; + } + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}."); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->set_base_dir(); + $this->generate_image_names(); + if (!$this->force_rebuild) { + $this->check_image_locally_or_remotely(); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { + $this->create_workdir(); + $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); + $this->generate_compose_file(); + ray('pushing to docker registry'); + $this->push_to_docker_registry(); $this->rolling_update(); return; } @@ -592,8 +474,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->generate_nixpacks_confs(); $this->generate_compose_file(); $this->generate_build_env_variables(); - // $this->add_build_env_variables_to_dockerfile(); $this->build_image(); + $this->push_to_docker_registry(); $this->rolling_update(); } private function deploy_static_buildpack() @@ -601,18 +483,202 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->use_build_server) { $this->server = $this->build_server; } - $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}."); $this->prepare_builder_image(); $this->check_git_if_build_needed(); $this->set_base_dir(); $this->generate_image_names(); + if (!$this->force_rebuild) { + $this->check_image_locally_or_remotely(); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { + $this->create_workdir(); + $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); + $this->generate_compose_file(); + $this->push_to_docker_registry(); + $this->rolling_update(); + return; + } + } $this->clone_repository(); $this->cleanup_git(); - $this->build_image(); $this->generate_compose_file(); + $this->build_image(); + $this->push_to_docker_registry(); $this->rolling_update(); } + private function write_deployment_configurations() + { + if (isset($this->docker_compose_base64)) { + if ($this->use_build_server) { + $this->server = $this->original_server; + } + $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); + $composeFileName = "$this->configuration_dir/docker-compose.yml"; + if ($this->pull_request_id !== 0) { + $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml"; + } + $this->execute_remote_command( + [ + "mkdir -p $this->configuration_dir" + ], + [ + "echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName", + ], + [ + "echo '{$readme}' > $this->configuration_dir/README.md", + ] + ); + if ($this->use_build_server) { + $this->server = $this->build_server; + } + } + } + private function push_to_docker_registry() + { + $forceFail = true; + if (str($this->application->docker_registry_image_name)->isEmpty()) { + ray('empty docker_registry_image_name'); + return; + } + if ($this->restart_only) { + ray('restart_only'); + return; + } + if ($this->application->build_pack === 'dockerimage') { + ray('dockerimage'); + return; + } + if ($this->use_build_server) { + ray('use_build_server'); + $forceFail = true; + } + if ($this->server->isSwarm() && $this->build_pack !== 'dockerimage') { + ray('isSwarm'); + $forceFail = true; + } + if ($this->application->additional_servers->count() > 0) { + ray('additional_servers'); + $forceFail = true; + } + if ($this->application->additional_servers()->wherePivot('server_id', $this->server->id)->count() > 0) { + ray('this is an additional_servers, no pushy pushy'); + return; + } + ray('push_to_docker_registry noww: ' . $this->production_image_name); + try { + instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); + $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name})."); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true + ], + ); + if ($this->application->docker_registry_image_tag) { + // Tag image with latest + $this->application_deployment_queue->addLogEntry("Tagging and pushing image with latest tag."); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + ], + [ + executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + ], + ); + } + $this->application_deployment_queue->addLogEntry("Image pushed to docker registry."); + } catch (Exception $e) { + $this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information."); + if ($forceFail) { + throw new RuntimeException($e->getMessage(), 69420); + } + ray($e); + } + } + private function generate_image_names() + { + if ($this->application->dockerfile) { + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); + } + } else if ($this->application->build_pack === 'dockerimage') { + $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); + } else if ($this->pull_request_id !== 0) { + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); + } + } else { + $this->dockerImageTag = str($this->commit)->substr(0, 128); + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}"); + } + } + } + private function just_restart() + { + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}."); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->set_base_dir(); + $this->generate_image_names(); + $this->check_image_locally_or_remotely(); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { + $this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container."); + $this->create_workdir(); + $this->generate_compose_file(); + $this->rolling_update(); + return; + } + throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.'); + } + private function check_image_locally_or_remotely() + { + $this->execute_remote_command([ + "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + ]); + if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) { + $this->execute_remote_command([ + "docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true + ]); + $this->execute_remote_command([ + "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + ]); + } + } + private function save_environment_variables() + { + $envs = collect([]); + if ($this->pull_request_id !== 0) { + foreach ($this->application->environment_variables_preview as $env) { + $envs->push($env->key . '=' . $env->real_value); + } + } else { + foreach ($this->application->environment_variables as $env) { + $envs->push($env->key . '=' . $env->real_value); + } + } + $envs_base64 = base64_encode($envs->implode("\n")); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") + ], + ); + } + + private function framework_based_notification() { // Laravel old env variables @@ -630,9 +696,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private function rolling_update() { if ($this->server->isSwarm()) { - if ($this->build_pack !== 'dockerimage') { - $this->push_to_docker_registry(forceFail: true); - } $this->application_deployment_queue->addLogEntry("Rolling update started."); $this->execute_remote_command( [ @@ -642,7 +705,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->application_deployment_queue->addLogEntry("Rolling update completed."); } else { if ($this->use_build_server) { - $this->push_to_docker_registry(forceFail: true); $this->write_deployment_configurations(); $this->server = $this->original_server; } @@ -787,10 +849,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } private function deploy_to_additional_destinations() { - if (str($this->application->additional_destinations)->isEmpty()) { + if ($this->application->additional_networks->count() === 0) { return; } - $destination_ids = collect(str($this->application->additional_destinations)->explode(',')); + $destination_ids = $this->application->additional_networks->pluck('id'); if ($this->server->isSwarm()) { $this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode."); return; @@ -1529,7 +1591,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); return; } if ($status === ApplicationDeploymentStatus::FINISHED->value) { - // $this->deploy_to_additional_destinations(); + $this->deploy_to_additional_destinations(); $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); } } @@ -1542,10 +1604,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } if ($this->application->build_pack !== 'dockercompose') { - $this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr'); - $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] - ); + $code = $exception->getCode(); + if ($code !== 69420) { + // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one + $this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr'); + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] + ); + } } $this->next(ApplicationDeploymentStatus::FAILED->value); diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 86c879a3b..1612e2191 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -5,6 +5,7 @@ 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\Models\Server; use App\Notifications\Container\ContainerRestarted; @@ -42,6 +43,19 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted public function handle() { + $applications = $this->server->applications(); + foreach ($applications as $application) { + if ($application->additional_servers->count() > 0) { + $is_main_server = $application->destination->server->id === $this->server->id; + if ($is_main_server) { + ComplexStatusCheck::run($application); + $applications = $applications->filter(function ($value, $key) use ($application) { + return $value->id !== $application->id; + }); + } + } + } + if (!$this->server->isFunctional()) { return 'Server is not ready.'; }; @@ -83,7 +97,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted }); } } - $applications = $this->server->applications(); $databases = $this->server->databases(); $services = $this->server->services()->get(); $previews = $this->server->previews(); @@ -160,10 +173,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted // Notify user that this container should not be there. } } - if (data_get($container,'Name') === '/coolify-db') { + if (data_get($container, 'Name') === '/coolify-db') { $foundDatabases[] = 0; } - } $serviceLabelId = data_get($labels, 'coolify.serviceId'); if ($serviceLabelId) { @@ -209,7 +221,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted } $exitedServices = $exitedServices->unique('id'); foreach ($exitedServices as $exitedService) { - if ($exitedService->status === 'exited') { + if (str($exitedService->status)->startsWith('exited')) { continue; } $name = data_get($exitedService, 'name'); @@ -231,7 +243,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted $notRunningApplications = $applications->pluck('id')->diff($foundApplications); foreach ($notRunningApplications as $applicationId) { $application = $applications->where('id', $applicationId)->first(); - if ($application->status === 'exited') { + if (str($application->status)->startsWith('exited')) { continue; } $application->update(['status' => 'exited']); @@ -256,7 +268,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); foreach ($notRunningApplicationPreviews as $previewId) { $preview = $previews->where('id', $previewId)->first(); - if ($preview->status === 'exited') { + if (str($preview->status)->startsWith('exited')) { continue; } $preview->update(['status' => 'exited']); @@ -281,7 +293,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); foreach ($notRunningDatabases as $database) { $database = $databases->where('id', $database)->first(); - if ($database->status === 'exited') { + if (str($database->status)->startsWith('exited')) { continue; } $database->update(['status' => 'exited']); diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 35d98412f..b84f66dfa 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -19,6 +19,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Artisan; class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted { @@ -49,8 +50,11 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted break; } } catch (\Throwable $e) { + ray($e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); throw $e; + } finally { + Artisan::queue('cleanup:stucked-resources'); } } } diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php index 0afcb4bb3..b327c3a7c 100644 --- a/app/Jobs/ServerStatusJob.php +++ b/app/Jobs/ServerStatusJob.php @@ -41,6 +41,15 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted throw new \RuntimeException('Server is not ready.'); }; try { + // $this->server->validateConnection(); + // $this->server->validateOS(); + // $docker_installed = $this->server->validateDockerEngine(); + // if (!$docker_installed) { + // $this->server->installDocker(); + // $this->server->validateDockerEngine(); + // } + + // $this->server->validateDockerEngineVersion(); if ($this->server->isFunctional()) { $this->cleanup(notify: false); } diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php new file mode 100644 index 000000000..3c13cd771 --- /dev/null +++ b/app/Livewire/Admin/Index.php @@ -0,0 +1,41 @@ +user()->id !== 0 && session('adminToken') === null) { + return redirect()->route('dashboard'); + } + $this->users = User::whereHas('teams', function ($query) { + $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); + })->get(); + } + public function switchUser(int $user_id) + { + $user = User::find($user_id); + auth()->login($user); + + if ($user_id === 0) { + session()->forget('adminToken'); + } else { + $token_payload = [ + 'valid' => true, + ]; + $token = Crypt::encrypt($token_payload); + session(['adminToken' => $token]); + } + return refreshSession(); + } + public function render() + { + return view('livewire.admin.index'); + } +} diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index b282cb175..696ae09c4 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -6,6 +6,7 @@ use App\Models\ApplicationDeploymentQueue; use App\Models\Project; use App\Models\Server; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Artisan; use Livewire\Component; class Dashboard extends Component @@ -19,6 +20,12 @@ class Dashboard extends Component $this->projects = Project::ownedByCurrentTeam()->get(); $this->get_deployments(); } + public function cleanup_queue() + { + Artisan::queue('app:init', [ + '--cleanup-deployments' => 'true' + ]); + } public function get_deployments() { $this->deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $this->servers->pluck("id"))->get([ diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index 9ba0e7a27..7a397f277 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -48,6 +48,8 @@ class DeploymentNavbar extends Component { try { $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; + $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; + $server = Server::find($server_id); if ($this->application_deployment_queue->logs) { $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); @@ -63,8 +65,8 @@ class DeploymentNavbar extends Component $this->application_deployment_queue->update([ 'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR), ]); - instant_remote_process([$kill_command], $this->server); } + instant_remote_process([$kill_command], $server); } catch (\Throwable $e) { ray($e); return handleError($e, $this); diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 0f6d61957..c11dbfe4b 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -243,9 +243,11 @@ class General extends Component return str($domain)->trim()->lower(); }); $domains = $domains->unique(); - foreach ($domains as $domain) { - if (!validate_dns_entry($domain, $this->application->destination->server)) { - $showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.","Make sure you have added the DNS records correctly.

Check this documentation for further help."); + if ($this->application->additional_servers->count() === 0) { + foreach ($domains as $domain) { + if (!validate_dns_entry($domain, $this->application->destination->server)) { + $showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.","Make sure you have added the DNS records correctly.

Check this documentation for further help."); + } } } check_fqdn_usage($this->application); diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 8f7a22ec8..1b19c445a 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -3,6 +3,8 @@ namespace App\Livewire\Project\Application; use App\Actions\Application\StopApplication; +use App\Events\ApplicationStatusChanged; +use App\Jobs\ComplexContainerStatusJob; use App\Jobs\ContainerStatusJob; use App\Jobs\ServerStatusJob; use App\Models\Application; @@ -31,13 +33,14 @@ class Heading extends Component { if ($this->application->destination->server->isFunctional()) { dispatch(new ContainerStatusJob($this->application->destination->server)); - $this->application->refresh(); - $this->application->previews->each(function ($preview) { - $preview->refresh(); - }); + // $this->application->refresh(); + // $this->application->previews->each(function ($preview) { + // $preview->refresh(); + // }); } else { dispatch(new ServerStatusJob($this->application->destination->server)); } + if ($showNotification) $this->dispatch('success', "Application status updated."); } @@ -49,15 +52,19 @@ class Heading extends Component public function deploy(bool $force_rebuild = false) { if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) { - $this->dispatch('error', 'Please load a Compose file first.'); + $this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.'); return; } - if ($this->application->destination->server->isSwarm() && is_null($this->application->docker_registry_image_name)) { - $this->dispatch('error', 'To deploy to a Swarm cluster you must set a Docker image name first.'); + if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) { + $this->dispatch('error', 'Failed to deploy', 'To deploy to a Swarm cluster you must set a Docker image name first.'); return; } - if (data_get($this->application, 'settings.is_build_server_enabled') && is_null($this->application->docker_registry_image_name)) { - $this->dispatch('error', 'To use a build server you must set a Docker image name first.
More information here: documentation'); + if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) { + $this->dispatch('error', 'Failed to deploy', 'To use a build server, you must first set a Docker image.
More information here: documentation'); + return; + } + if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) { + $this->dispatch('error', 'Failed to deploy', 'To deploy to more than one server, you must first set a Docker image.
More information here: documentation'); return; } $this->setDeploymentUuid(); @@ -85,26 +92,20 @@ class Heading extends Component StopApplication::run($this->application); $this->application->status = 'exited'; $this->application->save(); - $this->application->refresh(); - } - public function restartNew() - { - $this->setDeploymentUuid(); - queue_application_deployment( - application: $this->application, - deployment_uuid: $this->deploymentUuid, - restart_only: true, - is_new_deployment: true, - ); - return redirect()->route('project.application.deployment.show', [ - 'project_uuid' => $this->parameters['project_uuid'], - 'application_uuid' => $this->parameters['application_uuid'], - 'deployment_uuid' => $this->deploymentUuid, - 'environment_name' => $this->parameters['environment_name'], - ]); + if ($this->application->additional_servers->count() > 0) { + $this->application->additional_servers->each(function ($server) { + $server->pivot->status = "exited:unhealthy"; + $server->pivot->save(); + }); + } + ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } public function restart() { + if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) { + $this->dispatch('error', 'Failed to deploy', 'To deploy to more than one server, you must first set a Docker image.
More information here: documentation'); + return; + } $this->setDeploymentUuid(); queue_application_deployment( application: $this->application, diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 5c99630ee..cf5e2632f 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -2,11 +2,80 @@ namespace App\Livewire\Project\Shared; +use App\Actions\Application\StopApplicationOneServer; +use App\Events\ApplicationStatusChanged; +use App\Models\Server; +use App\Models\StandaloneDocker; use Livewire\Component; +use Visus\Cuid2\Cuid2; class Destination extends Component { public $resource; - public $servers = []; - public $additional_servers = []; + public $networks = []; + + public function getListeners() + { + $teamId = auth()->user()->currentTeam()->id; + return [ + "echo-private:team.{$teamId},ApplicationStatusChanged" => 'loadData', + ]; + } + public function mount() + { + $this->loadData(); + } + public function loadData() + { + $all_networks = collect([]); + $all_networks = $all_networks->push($this->resource->destination); + $all_networks = $all_networks->merge($this->resource->additional_networks); + + $this->networks = Server::isUsable()->get()->map(function ($server) { + return $server->standaloneDockers; + })->flatten(); + $this->networks = $this->networks->reject(function ($network) use ($all_networks) { + return $all_networks->pluck('id')->contains($network->id); + }); + + } + public function redeploy(int $network_id, int $server_id) + { + $deployment_uuid = new Cuid2(7); + $server = Server::find($server_id); + $destination = StandaloneDocker::find($network_id); + queue_application_deployment( + deployment_uuid: $deployment_uuid, + application: $this->resource, + server: $server, + destination: $destination, + no_questions_asked: true, + ); + return redirect()->route('project.application.deployment.show', [ + 'project_uuid' => data_get($this->resource, 'environment.project.uuid'), + 'application_uuid' => data_get($this->resource, 'uuid'), + 'deployment_uuid' => $deployment_uuid, + 'environment_name' => data_get($this->resource, 'environment.name'), + ]); + } + public function addServer(int $network_id, int $server_id) + { + $this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]); + $this->resource->load(['additional_networks']); + ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); + $this->loadData(); + } + public function removeServer(int $network_id, int $server_id) + { + if ($this->resource->destination->server->id == $server_id) { + $this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.'); + return; + } + $server = Server::find($server_id); + StopApplicationOneServer::run($this->resource, $server); + $this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]); + $this->resource->load(['additional_networks']); + ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); + $this->loadData(); + } } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index c30011a4a..28aac7ce3 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -171,7 +171,7 @@ class All extends Component } $environment->save(); $this->refreshEnvs(); - $this->dispatch('success', 'Environment variable added successfully.'); + $this->dispatch('success', 'Environment variable added.'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 936f8e724..bb9c7c3a1 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -28,6 +28,7 @@ class Form extends Component 'server.settings.is_swarm_worker' => 'required|boolean', 'server.settings.is_build_server' => 'required|boolean', 'server.settings.concurrent_builds' => 'required|integer|min:1', + 'server.settings.dynamic_timeout' => 'required|integer|min:1', 'wildcard_domain' => 'nullable|url', ]; protected $validationAttributes = [ @@ -42,6 +43,8 @@ class Form extends Component 'server.settings.is_swarm_worker' => 'Swarm Worker', 'server.settings.is_build_server' => 'Build Server', 'server.settings.concurrent_builds' => 'Concurrent Builds', + 'server.settings.dynamic_timeout' => 'Dynamic Timeout', + ]; public function mount() diff --git a/app/Models/Application.php b/app/Models/Application.php index cf3141ed7..959d06d7f 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -15,7 +15,6 @@ class Application extends BaseModel { use SoftDeletes; protected $guarded = []; - protected static function booted() { static::saving(function ($application) { @@ -53,6 +52,16 @@ class Application extends BaseModel }); } + public function additional_servers() + { + return $this->belongsToMany(Server::class, 'additional_destinations') + ->withPivot('standalone_docker_id', 'status'); + } + public function additional_networks() + { + return $this->belongsToMany(StandaloneDocker::class, 'additional_destinations') + ->withPivot('server_id', 'status'); + } public function is_github_based(): bool { if (data_get($this, 'source')) { @@ -203,6 +212,79 @@ class Application extends BaseModel ); } + public function realStatus() + { + return $this->getRawOriginal('status'); + } + public function status(): Attribute + { + return Attribute::make( + set: function ($value) { + if ($this->additional_servers->count() === 0) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + } else { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + } + }, + get: function ($value) { + if ($this->additional_servers->count() === 0) { + //running (healthy) + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + } else { + $complex_status = null; + $complex_health = null; + $complex_status = $main_server_status = str($value)->before(':')->value(); + $complex_health = $main_server_health = str($value)->after(':')->value() ?? 'unhealthy'; + $additional_servers_status = $this->additional_servers->pluck('pivot.status'); + foreach ($additional_servers_status as $status) { + $server_status = str($status)->before(':')->value(); + $server_health = str($status)->after(':')->value() ?? 'unhealthy'; + if ($server_status !== 'running') { + if ($main_server_status !== $server_status) { + $complex_status = 'degraded'; + } + } + if ($server_health !== 'healthy') { + if ($main_server_health !== $server_health) { + $complex_health = 'unhealthy'; + } + } + } + return "$complex_status:$complex_health"; + } + }, + ); + } public function portsExposesArray(): Attribute { @@ -216,7 +298,8 @@ class Application extends BaseModel { return $this->morphToMany(Tag::class, 'taggable'); } - public function project() { + public function project() + { return data_get($this, 'environment.project'); } public function team() @@ -435,7 +518,7 @@ class Application extends BaseModel { return "/artifacts/{$uuid}"; } - function setGitImportSettings(string $deployment_uuid, string $git_clone_command) + function setGitImportSettings(string $deployment_uuid, string $git_clone_command) { $baseDir = $this->generateBaseDir($deployment_uuid); if ($this->git_commit_sha !== 'HEAD') { diff --git a/app/Models/Server.php b/app/Models/Server.php index b3f4abc61..bdbb0309d 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -9,6 +9,7 @@ use App\Notifications\Server\Revived; use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Support\Facades\DB; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Illuminate\Support\Str; @@ -248,9 +249,17 @@ class Server extends BaseModel } public function applications() { - return $this->destinations()->map(function ($standaloneDocker) { + $applications = $this->destinations()->map(function ($standaloneDocker) { return $standaloneDocker->applications; })->flatten(); + $additionalApplicationIds = DB::table('additional_destinations')->where('server_id', $this->id)->get('application_id'); + $additionalApplicationIds = collect($additionalApplicationIds)->map(function ($item) { + return $item->application_id; + }); + Application::whereIn('id', $additionalApplicationIds)->get()->each(function ($application) use ($applications) { + $applications->push($application); + }); + return $applications; } public function dockerComposeBasedApplications() { @@ -300,7 +309,8 @@ class Server extends BaseModel { $standalone_docker = $this->hasMany(StandaloneDocker::class)->get(); $swarm_docker = $this->hasMany(SwarmDocker::class)->get(); - return $standalone_docker->concat($swarm_docker); + $asd = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get(); + return $standalone_docker->concat($swarm_docker)->concat($asd); } public function standaloneDockers() diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 18c36203e..1143b018e 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -43,7 +43,41 @@ class StandaloneMariadb extends BaseModel $database->tags()->detach(); }); } - + public function realStatus() + { + return $this->getRawOriginal('status'); + } + public function status(): Attribute + { + return Attribute::make( + set: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + get: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + ); + } public function tags() { return $this->morphToMany(Tag::class, 'taggable'); diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 939af0974..610323f74 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -46,7 +46,41 @@ class StandaloneMongodb extends BaseModel $database->tags()->detach(); }); } - + public function realStatus() + { + return $this->getRawOriginal('status'); + } + public function status(): Attribute + { + return Attribute::make( + set: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + get: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + ); + } public function tags() { return $this->morphToMany(Tag::class, 'taggable'); diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 8bcc0d9fe..fa6bbe28f 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -43,7 +43,41 @@ class StandaloneMysql extends BaseModel $database->tags()->detach(); }); } - + public function realStatus() + { + return $this->getRawOriginal('status'); + } + public function status(): Attribute + { + return Attribute::make( + set: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + get: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + ); + } public function tags() { return $this->morphToMany(Tag::class, 'taggable'); diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index fb6ad944d..bcc43843b 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -43,7 +43,41 @@ class StandalonePostgresql extends BaseModel $database->tags()->detach(); }); } - + public function realStatus() + { + return $this->getRawOriginal('status'); + } + public function status(): Attribute + { + return Attribute::make( + set: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + get: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + ); + } public function tags() { return $this->morphToMany(Tag::class, 'taggable'); diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 73fa61a6c..59c53f882 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -38,7 +38,41 @@ class StandaloneRedis extends BaseModel $database->tags()->detach(); }); } - + public function realStatus() + { + return $this->getRawOriginal('status'); + } + public function status(): Attribute + { + return Attribute::make( + set: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + get: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } else if (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + return "$status:$health"; + }, + ); + } public function tags() { return $this->morphToMany(Tag::class, 'taggable'); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 23ee3fa06..7ffc6a72c 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -104,7 +104,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n ray($error); if ($error instanceof TooManyRequestsException) { if (isset($livewire)) { - return $livewire->dispatch('error', "Too many requests.","Please try again in {$error->secondsUntilAvailable} seconds."); + return $livewire->dispatch('error', "Too many requests.", "Please try again in {$error->secondsUntilAvailable} seconds."); } return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."; } @@ -1690,7 +1690,7 @@ function check_fqdn_usage(ServiceApplication|Application $own_resource) $naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value(); if ($domains->contains($naked_domain)) { if ($app->uuid !== $own_resource->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource."); + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
{$app->name}."); } } } diff --git a/config/queue.php b/config/queue.php index 2ef618584..a46085a9f 100644 --- a/config/queue.php +++ b/config/queue.php @@ -65,7 +65,7 @@ return [ 'driver' => 'redis', 'connection' => 'default', 'queue' => env('REDIS_QUEUE', 'default'), - 'retry_after' => 3600, + 'retry_after' => 86400, 'block_for' => null, 'after_commit' => true, ], diff --git a/config/sentry.php b/config/sentry.php index 37faa38ae..5f2ea6fb6 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.211', + 'release' => '4.0.0-beta.212', // 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 814a56b4a..01e2a62f9 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ string('taggable_type'); $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); $table->unique(['tag_id', 'taggable_id', 'taggable_type'], 'taggable_unique'); // Composite unique index - }); } diff --git a/database/migrations/2024_02_06_132748_add_additional_destinations.php b/database/migrations/2024_02_06_132748_add_additional_destinations.php new file mode 100644 index 000000000..32e7f5b18 --- /dev/null +++ b/database/migrations/2024_02_06_132748_add_additional_destinations.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('application_id')->constrained()->onDelete('cascade'); + $table->foreignId('server_id')->constrained()->onDelete('cascade'); + $table->string('status')->default('exited'); + $table->foreignId('standalone_docker_id')->constrained()->onDelete('cascade'); + $table->timestamps(); + }); + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('additional_destinations'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('additional_destinations'); + Schema::table('applications', function (Blueprint $table) { + $table->string('additional_destinations')->nullable()->after('destination'); + }); + } +}; diff --git a/database/migrations/2024_02_08_112304_add_dynamic_timeout_for_deployments.php b/database/migrations/2024_02_08_112304_add_dynamic_timeout_for_deployments.php new file mode 100644 index 000000000..f45bd7a2a --- /dev/null +++ b/database/migrations/2024_02_08_112304_add_dynamic_timeout_for_deployments.php @@ -0,0 +1,28 @@ +integer('dynamic_timeout')->default(3600); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('dynamic_timeout'); + }); + } +}; diff --git a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up index b563c067f..ea960df95 100644 --- a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up +++ b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up @@ -1,3 +1,3 @@ #!/command/execlineb -P s6-setuidgid webuser -php /var/www/html/artisan app:init --cleanup +php /var/www/html/artisan app:init --full-cleanup diff --git a/resources/views/components/databases/navbar.blade.php b/resources/views/components/databases/navbar.blade.php index 9f3dfad3b..962c3d22e 100644 --- a/resources/views/components/databases/navbar.blade.php +++ b/resources/views/components/databases/navbar.blade.php @@ -22,7 +22,7 @@ @endif
- @if ($database->status !== 'exited') + @if (!str($database->status)->startsWith('exited')) diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index 881ad1bd8..e4cafb6b9 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -1,5 +1,5 @@ @auth -