diff --git a/app/Http/Livewire/Project/Application/Heading.php b/app/Http/Livewire/Project/Application/Heading.php index 14bdf1db0..3d5af34da 100644 --- a/app/Http/Livewire/Project/Application/Heading.php +++ b/app/Http/Livewire/Project/Application/Heading.php @@ -39,6 +39,26 @@ class Heading extends Component $this->deploy(force_rebuild: true); } + public function deployNew() + { + if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) { + $this->emit('error', 'Please load a Compose file first.'); + return; + } + $this->setDeploymentUuid(); + queue_application_deployment( + application_id: $this->application->id, + deployment_uuid: $this->deploymentUuid, + force_rebuild: false, + is_new_deployment: true, + ); + return redirect()->route('project.application.deployment', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'application_uuid' => $this->parameters['application_uuid'], + 'deployment_uuid' => $this->deploymentUuid, + 'environment_name' => $this->parameters['environment_name'], + ]); + } public function deploy(bool $force_rebuild = false) { if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) { @@ -72,6 +92,22 @@ class Heading extends Component $this->application->save(); $this->application->refresh(); } + public function restartNew() + { + $this->setDeploymentUuid(); + queue_application_deployment( + application_id: $this->application->id, + deployment_uuid: $this->deploymentUuid, + restart_only: true, + is_new_deployment: true, + ); + return redirect()->route('project.application.deployment', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'application_uuid' => $this->parameters['application_uuid'], + 'deployment_uuid' => $this->deploymentUuid, + 'environment_name' => $this->parameters['environment_name'], + ]); + } public function restart() { $this->setDeploymentUuid(); diff --git a/app/Jobs/ApplicationDeploymentNewJob.php b/app/Jobs/ApplicationDeploymentNewJob.php new file mode 100644 index 000000000..b082e82fa --- /dev/null +++ b/app/Jobs/ApplicationDeploymentNewJob.php @@ -0,0 +1,196 @@ +mainServer = data_get($this->application, 'destination.server'); + $this->deploymentUuid = data_get($this->deployment, 'deployment_uuid'); + $this->pullRequestId = data_get($this->deployment, 'pull_request_id', 0); + $this->gitType = data_get($this->deployment, 'git_type'); + + $this->basedir = $this->application->generateBaseDir($this->deploymentUuid); + $this->workdir = $this->basedir . rtrim($this->application->base_directory, '/'); + } + public function handle() + { + try { + ray()->clearAll(); + $this->deployment->setStatus(ApplicationDeploymentStatus::IN_PROGRESS->value); + + $hostIpMappings = $this->mainServer->getHostIPMappings($this->application->destination->network); + if ($this->application->dockerfile_target_build) { + $buildTarget = " --target {$this->application->dockerfile_target_build} "; + } + + // Get the git repository and port (custom port or default port) + [ + 'repository' => $this->gitRepository, + 'port' => $this->gitPort + ] = $this->application->customRepository(); + + // Get the git branch and git import commands + [ + 'commands' => $this->gitImportCommands, + 'branch' => $this->gitBranch, + 'fullRepoUrl' => $this->gitFullRepoUrl + ] = $this->application->generateGitImportCommands($this->deploymentUuid, $this->pullRequestId, $this->gitType); + + $this->servers = $this->application->servers(); + + if ($this->deployment->restart_only) { + if ($this->application->build_pack === 'dockerimage') { + throw new \Exception('Restart only is not supported for docker image based deployments'); + } + $this->deployment->addLogEntry("Starting deployment of {$this->application->name}."); + $this->servers->each(function ($server) { + $this->deployment->addLogEntry("Restarting {$this->application->name} on {$server->name}."); + $this->restartOnly($server); + }); + } + $this->next(ApplicationDeploymentStatus::FINISHED->value); + ApplicationDeploymentFinished::dispatch($this->application, $this->deployment); + } catch (Throwable $exception) { + $this->fail($exception); + } finally { + $this->servers->each(function ($server) { + $this->deployment->addLogEntry("Cleaning up temporary containers on {$server->name}."); + $server->executeRemoteCommand( + commands: collect([])->push([ + "command" => "docker rm -f {$this->deploymentUuid}", + "hidden" => true, + "ignoreErrors" => true, + ]), + loggingModel: $this->deployment + ); + }); + } + } + public function restartOnly(Server $server) + { + $server->executeRemoteCommand( + commands: $this->application->prepareHelperImage($this->deploymentUuid), + loggingModel: $this->deployment + ); + + $privateKey = data_get($this->application, 'private_key.private_key', null); + $gitLsRemoteCommand = collect([]); + if ($privateKey) { + $privateKey = base64_decode($privateKey); + $gitLsRemoteCommand + ->push([ + "command" => executeInDocker($this->deploymentUuid, "mkdir -p /root/.ssh") + ]) + ->push([ + "command" => executeInDocker($this->deploymentUuid, "echo '{$privateKey}' | base64 -d > /root/.ssh/id_rsa") + ]) + ->push([ + "command" => executeInDocker($this->deploymentUuid, "chmod 600 /root/.ssh/id_rsa") + ]) + ->push([ + "name" => "git_commit_sha", + "command" => executeInDocker($this->deploymentUuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->gitPort} -o Port={$this->gitPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->gitFullRepoUrl} {$this->gitBranch}"), + "hidden" => true, + ]); + } else { + $gitLsRemoteCommand->push([ + "name" => "git_commit_sha", + "command" => executeInDocker($this->deploymentUuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->gitPort} -o Port={$this->gitPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->gitFullRepoUrl} {$this->gitBranch}"), + "hidden" => true, + ]); + } + $this->deployment->addLogEntry("Checking if there is any new commit on {$this->gitBranch} branch."); + + $server->executeRemoteCommand( + commands: $gitLsRemoteCommand, + loggingModel: $this->deployment + ); + $commit = str($this->deployment->getOutput('git_commit_sha'))->before("\t"); + + [ + 'productionImageName' => $productionImageName + ] = $this->application->generateImageNames($commit, $this->pullRequestId); + + $this->deployment->addLogEntry("Checking if the image {$productionImageName} already exists."); + $server->checkIfDockerImageExists($productionImageName, $this->deployment); + + if (str($this->deployment->getOutput('local_image_found'))->isNotEmpty()) { + $this->deployment->addLogEntry("Image {$productionImageName} already exists. Skipping the build."); + + $server->createWorkDirForDeployment($this->workdir, $this->deployment); + + $this->application->generateDockerComposeFile($server, $this->deployment, $this->workdir); + $this->application->rollingUpdateApplication($server, $this->deployment, $this->workdir); + return; + } + throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.'); + } + public function failed(Throwable $exception): void + { + ray($exception); + $this->next(ApplicationDeploymentStatus::FAILED->value); + } + private function next(string $status) + { + // If the deployment is cancelled by the user, don't update the status + if ($this->deployment->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + $this->deployment->update([ + 'status' => $status, + ]); + } + queue_next_deployment($this->application, isNew: true); + } +} diff --git a/app/Models/Application.php b/app/Models/Application.php index 341d6faf5..10f27e207 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Collection; use Spatie\Activitylog\Models\Activity; use Illuminate\Support\Str; use RuntimeException; @@ -48,6 +49,65 @@ class Application extends BaseModel $application->environment_variables_preview()->delete(); }); } + // Build packs / deployment types + + + public function servers(): Collection + { + $mainServer = data_get($this, 'destination.server'); + $additionalDestinations = data_get($this, 'additional_destinations', null); + $additionalServers = collect([]); + if ($this->isMultipleServerDeployment()) { + ray('asd'); + if (str($additionalDestinations)->isNotEmpty()) { + $additionalDestinations = str($additionalDestinations)->explode(','); + foreach ($additionalDestinations as $destinationId) { + $destination = StandaloneDocker::find($destinationId)->whereNot('id', $mainServer->id)->first(); + $server = data_get($destination, 'server'); + $additionalServers->push($server); + } + } + } + return collect([$mainServer])->merge($additionalServers); + } + + public function generateImageNames(string $commit, int $pullRequestId) + { + if ($this->dockerfile) { + if ($this->docker_registry_image_name) { + $buildImageName = Str::lower("{$this->docker_registry_image_name}:build"); + $productionImageName = Str::lower("{$this->docker_registry_image_name}:latest"); + } else { + $buildImageName = Str::lower("{$this->uuid}:build"); + $productionImageName = Str::lower("{$this->uuid}:latest"); + } + } else if ($this->build_pack === 'dockerimage') { + $productionImageName = Str::lower("{$this->docker_registry_image_name}:{$this->docker_registry_image_tag}"); + } else if ($pullRequestId === 0) { + $dockerImageTag = str($commit)->substr(0, 128); + if ($this->docker_registry_image_name) { + $buildImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}-build"); + $productionImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}"); + } else { + $buildImageName = Str::lower("{$this->uuid}:{$dockerImageTag}-build"); + $productionImageName = Str::lower("{$this->uuid}:{$dockerImageTag}"); + } + } else if ($pullRequestId !== 0) { + if ($this->docker_registry_image_name) { + $buildImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}-build"); + $productionImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}"); + } else { + $buildImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}-build"); + $productionImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}"); + } + } + return [ + 'buildImageName' => $buildImageName, + 'productionImageName' => $productionImageName, + ]; + } + // End of build packs / deployment types + public function link() { if (data_get($this, 'environment.project.uuid')) { @@ -385,9 +445,7 @@ class Application extends BaseModel } public function isMultipleServerDeployment() { - if (isDev()) { - return true; - } + return false; if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) { return true; } @@ -431,6 +489,294 @@ class Application extends BaseModel { return "/artifacts/{$uuid}"; } + function generateHealthCheckCommands() + { + if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') { + // TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl. + return 'exit 0'; + } + if (!$this->health_check_port) { + $health_check_port = $this->ports_exposes_array[0]; + } else { + $health_check_port = $this->health_check_port; + } + if ($this->health_check_path) { + $this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}"; + $generated_healthchecks_commands = [ + "curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path} > /dev/null" + ]; + } else { + $this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/"; + $generated_healthchecks_commands = [ + "curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/" + ]; + } + return implode(' ', $generated_healthchecks_commands); + } + function generateLocalPersistentVolumes(int $pullRequestId) + { + $persistentStorages = []; + $volumeNames = []; + foreach ($this->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + if ($pullRequestId !== 0) { + $volume_name = $volume_name . '-pr-' . $pullRequestId; + } + $persistentStorages[] = $volume_name . ':' . $persistentStorage->mount_path; + + if ($persistentStorage->host_path) { + continue; + } + + $name = $persistentStorage->name; + + if ($pullRequestId !== 0) { + $name = $name . '-pr-' . $pullRequestId; + } + + $volumeNames[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + + return [ + 'persistentStorages' => $persistentStorages, + 'volumeNames' => $volumeNames, + ]; + } + public function generateEnvironmentVariables($ports) + { + $environmentVariables = collect(); + // ray('Generate Environment Variables')->green(); + if ($this->pull_request_id === 0) { + // ray($this->runtime_environment_variables)->green(); + foreach ($this->runtime_environment_variables as $env) { + $environmentVariables->push("$env->key=$env->value"); + } + foreach ($this->nixpacks_environment_variables as $env) { + $environmentVariables->push("$env->key=$env->value"); + } + } else { + // ray($this->runtime_environment_variables_preview)->green(); + foreach ($this->runtime_environment_variables_preview as $env) { + $environmentVariables->push("$env->key=$env->value"); + } + foreach ($this->nixpacks_environment_variables_preview as $env) { + $environmentVariables->push("$env->key=$env->value"); + } + } + // Add PORT if not exists, use the first port as default + if ($environmentVariables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) { + $environmentVariables->push("PORT={$ports[0]}"); + } + return $environmentVariables->all(); + } + function generateDockerComposeFile(Server $server, ApplicationDeploymentQueue $deployment, string $workdir) + { + $pullRequestId = $deployment->pull_request_id; + $ports = $this->settings->is_static ? [80] : $this->ports_exposes_array; + $container_name = generateApplicationContainerName($this, $this->pull_request_id); + $commit = str($deployment->getOutput('git_commit_sha'))->before("\t"); + + [ + 'productionImageName' => $productionImageName + ] = $this->generateImageNames($commit, $pullRequestId); + + [ + 'persistentStorages' => $persistentStorages, + 'volumeNames' => $volumeNames + ] = $this->generateLocalPersistentVolumes($pullRequestId); + + $environmentVariables = $this->generateEnvironmentVariables($ports); + + if (data_get($this, 'custom_labels')) { + $labels = collect(str($this->custom_labels)->explode(',')); + $labels = $labels->filter(function ($value, $key) { + return !Str::startsWith($value, 'coolify.'); + }); + $this->custom_labels = $labels->implode(','); + $this->save(); + } else { + $labels = collect(generateLabelsApplication($this, $this->preview)); + } + if ($this->pull_request_id !== 0) { + $labels = collect(generateLabelsApplication($this, $this->preview)); + } + $labels = $labels->merge(defaultLabels($this->id, $this->uuid, $this->pull_request_id))->toArray(); + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $container_name => [ + 'image' => $productionImageName, + 'container_name' => $container_name, + 'restart' => RESTART_MODE, + 'environment' => $environmentVariables, + 'expose' => $ports, + 'networks' => [ + $this->destination->network, + ], + 'healthcheck' => [ + 'test' => [ + 'CMD-SHELL', + $this->generateHealthCheckCommands() + ], + 'interval' => $this->health_check_interval . 's', + 'timeout' => $this->health_check_timeout . 's', + 'retries' => $this->health_check_retries, + 'start_period' => $this->health_check_start_period . 's' + ], + 'mem_limit' => $this->limits_memory, + 'memswap_limit' => $this->limits_memory_swap, + 'mem_swappiness' => $this->limits_memory_swappiness, + 'mem_reservation' => $this->limits_memory_reservation, + 'cpus' => (int) $this->limits_cpus, + 'cpuset' => $this->limits_cpuset, + 'cpu_shares' => $this->limits_cpu_shares, + ] + ], + 'networks' => [ + $this->destination->network => [ + 'external' => true, + 'name' => $this->destination->network, + 'attachable' => true + ] + ] + ]; + if ($server->isSwarm()) { + data_forget($docker_compose, 'services.' . $container_name . '.container_name'); + data_forget($docker_compose, 'services.' . $container_name . '.expose'); + data_forget($docker_compose, 'services.' . $container_name . '.restart'); + + data_forget($docker_compose, 'services.' . $container_name . '.mem_limit'); + data_forget($docker_compose, 'services.' . $container_name . '.memswap_limit'); + data_forget($docker_compose, 'services.' . $container_name . '.mem_swappiness'); + data_forget($docker_compose, 'services.' . $container_name . '.mem_reservation'); + data_forget($docker_compose, 'services.' . $container_name . '.cpus'); + data_forget($docker_compose, 'services.' . $container_name . '.cpuset'); + data_forget($docker_compose, 'services.' . $container_name . '.cpu_shares'); + + $docker_compose['services'][$container_name]['deploy'] = [ + 'placement' => [ + 'constraints' => [ + 'node.role == worker' + ] + ], + 'mode' => 'replicated', + 'replicas' => 1, + 'update_config' => [ + 'order' => 'start-first' + ], + 'rollback_config' => [ + 'order' => 'start-first' + ], + 'labels' => $labels, + 'resources' => [ + 'limits' => [ + 'cpus' => $this->limits_cpus, + 'memory' => $this->limits_memory, + ], + 'reservations' => [ + 'cpus' => $this->limits_cpus, + 'memory' => $this->limits_memory, + ] + ] + ]; + } else { + $docker_compose['services'][$container_name]['labels'] = $labels; + } + if ($server->isLogDrainEnabled() && $this->isLogDrainEnabled()) { + $docker_compose['services'][$container_name]['logging'] = [ + 'driver' => 'fluentd', + 'options' => [ + 'fluentd-address' => "tcp://127.0.0.1:24224", + 'fluentd-async' => "true", + 'fluentd-sub-second-precision' => "true", + ] + ]; + } + if ($this->settings->is_gpu_enabled) { + $docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'] = [ + [ + 'driver' => data_get($this, 'settings.gpu_driver', 'nvidia'), + 'capabilities' => ['gpu'], + 'options' => data_get($this, 'settings.gpu_options', []) + ] + ]; + if (data_get($this, 'settings.gpu_count')) { + $count = data_get($this, 'settings.gpu_count'); + if ($count === 'all') { + $docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count; + } else { + $docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count; + } + } else if (data_get($this, 'settings.gpu_device_ids')) { + $docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this, 'settings.gpu_device_ids'); + } + } + if ($this->isHealthcheckDisabled()) { + data_forget($docker_compose, 'services.' . $container_name . '.healthcheck'); + } + if (count($this->ports_mappings_array) > 0 && $this->pull_request_id === 0) { + $docker_compose['services'][$container_name]['ports'] = $this->ports_mappings_array; + } + if (count($persistentStorages) > 0) { + $docker_compose['services'][$container_name]['volumes'] = $persistentStorages; + } + if (count($volumeNames) > 0) { + $docker_compose['volumes'] = $volumeNames; + } + + $docker_compose['services'][$this->uuid] = $docker_compose['services'][$container_name]; + + data_forget($docker_compose, 'services.' . $container_name); + + $docker_compose = Yaml::dump($docker_compose, 10); + $docker_compose_base64 = base64_encode($docker_compose); + $server->executeRemoteCommand( + commands: collect([])->push([ + 'command' => executeInDocker($deployment->deployment_uuid, "echo '{$docker_compose_base64}' | base64 -d > {$workdir}/docker-compose.yml"), + 'hidden' => true, + 'ignoreErrors' => true + ]), + loggingModel: $deployment + ); + } + function rollingUpdateApplication(Server $server, ApplicationDeploymentQueue $deployment, string $workdir) + { + $pullRequestId = $deployment->pull_request_id; + $containerName = generateApplicationContainerName($this, $pullRequestId); + // if (count($this->ports_mappings_array) > 0) { + // $deployment->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.'); + $containers = getCurrentApplicationContainerStatus($server, $this->id, $pullRequestId); + ray($containers); + // if ($pullRequestId === 0) { + // $containers = $containers->filter(function ($container) use ($containerName) { + // return data_get($container, 'Names') !== $containerName; + // }); + // } + $containers->each(function ($container) use ($server, $deployment) { + $removingContainerName = data_get($container, 'Names'); + $server->executeRemoteCommand( + commands: collect([])->push([ + 'command' => "docker rm -f $removingContainerName", + 'hidden' => true, + 'ignoreErrors' => true + ]), + loggingModel: $deployment + ); + }); + // } + $server->executeRemoteCommand( + commands: collect([])->push([ + 'command' => executeInDocker($deployment->deployment_uuid, "docker compose --project-directory {$workdir} up --build -d"), + 'hidden' => true, + 'ignoreErrors' => true + ]), + loggingModel: $deployment + ); + $deployment->addLogEntry("New container started."); + } function setGitImportSettings(string $deployment_uuid, string $git_clone_command) { $baseDir = $this->generateBaseDir($deployment_uuid); diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 99aed3750..7f3f36d0a 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -9,6 +9,11 @@ class ApplicationDeploymentQueue extends Model { protected $guarded = []; + public function setStatus(string $status) { + $this->update([ + 'status' => $status, + ]); + } public function getOutput($name) { if (!$this->logs) { diff --git a/app/Models/Server.php b/app/Models/Server.php index 439bba990..1806f5ceb 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -67,7 +67,7 @@ class Server extends BaseModel { $teamId = currentTeam()->id; $selectArray = collect($select)->concat(['id']); - return Server::whereTeamId($teamId)->with('settings','swarmDockers','standaloneDockers')->select($selectArray->all())->orderBy('name'); + return Server::whereTeamId($teamId)->with('settings', 'swarmDockers', 'standaloneDockers')->select($selectArray->all())->orderBy('name'); } static public function isUsable() @@ -86,7 +86,8 @@ class Server extends BaseModel { return $this->hasOne(ServerSetting::class); } - public function addInitialNetwork() { + public function addInitialNetwork() + { if ($this->id === 0) { if ($this->isSwarm()) { SwarmDocker::create([ @@ -536,4 +537,74 @@ class Server extends BaseModel ); }); } + public function getHostIPMappings($network) + { + $addHosts = null; + $allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $this); + if (!is_null($allContainers)) { + $allContainers = format_docker_command_output_to_json($allContainers); + $ips = collect([]); + if (count($allContainers) > 0) { + $allContainers = $allContainers[0]; + foreach ($allContainers as $container) { + $containerName = data_get($container, 'Name'); + if ($containerName === 'coolify-proxy') { + continue; + } + $containerIp = data_get($container, 'IPv4Address'); + if ($containerName && $containerIp) { + $containerIp = str($containerIp)->before('/'); + $ips->put($containerName, $containerIp->value()); + } + } + } + $addHosts = $ips->map(function ($ip, $name) { + return "--add-host $name:$ip"; + })->implode(' '); + } + return $addHosts; + } + public function checkIfDockerImageExists(string $imageName, ApplicationDeploymentQueue $deployment) + { + $this->executeRemoteCommand( + commands: collect([ + [ + "name" => "local_image_found", + "command" => "docker images -q {$imageName} 2>/dev/null", + "hidden" => true, + ] + ]), + loggingModel: $deployment + ); + if (str($deployment->getOutput('local_image_found'))->isEmpty()) { + $this->executeRemoteCommand( + commands: collect([ + [ + "command" => "docker pull {$imageName} 2>/dev/null", + "ignoreErrors" => true, + "hidden" => true + ], + [ + "name" => "local_image_found", + "command" => "docker images -q {$imageName} 2>/dev/null", + "hidden" => true, + ] + ]), + loggingModel: $deployment + ); + } + } + public function createWorkDirForDeployment(string $workdir, ApplicationDeploymentQueue $deployment) + { + $this->executeRemoteCommand( + commands: collect([ + [ + "command" => executeInDocker($deployment->deployment_uuid, "mkdir -p {$workdir}"), + "ignoreErrors" => true, + "hidden" => true + ], + ]), + loggingModel: $deployment + ); + } } diff --git a/app/Providers/ApplicationDeploymentFinished.php b/app/Providers/ApplicationDeploymentFinished.php new file mode 100644 index 000000000..a87dc06b8 --- /dev/null +++ b/app/Providers/ApplicationDeploymentFinished.php @@ -0,0 +1,27 @@ +> */ protected $listen = [ - Registered::class => [ - SendEmailVerificationNotification::class, + // Registered::class => [ + // SendEmailVerificationNotification::class, + // ], + ApplicationDeploymentFinished::class => [ + SendDeploymentNotification::class, ], ]; diff --git a/app/Providers/SendDeploymentNotification.php b/app/Providers/SendDeploymentNotification.php new file mode 100644 index 000000000..b4f3d6b46 --- /dev/null +++ b/app/Providers/SendDeploymentNotification.php @@ -0,0 +1,28 @@ + $application_id, @@ -36,19 +37,32 @@ function queue_application_deployment(int $application_id, string $deployment_uu if ($running_deployments->count() > 0) { return; } - dispatch(new ApplicationDeploymentJob( - application_deployment_queue_id: $deployment->id, - ))->onConnection('long-running')->onQueue('long-running'); - + if ($is_new_deployment) { + dispatch(new ApplicationDeploymentNewJob( + deployment: $deployment, + application: Application::find($application_id) + ))->onConnection('long-running')->onQueue('long-running'); + } else { + dispatch(new ApplicationDeploymentJob( + application_deployment_queue_id: $deployment->id, + ))->onConnection('long-running')->onQueue('long-running'); + } } -function queue_next_deployment(Application $application) +function queue_next_deployment(Application $application, bool $isNew = false) { $next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first(); if ($next_found) { - dispatch(new ApplicationDeploymentJob( - application_deployment_queue_id: $next_found->id, - ))->onConnection('long-running')->onQueue('long-running'); + if ($isNew) { + dispatch(new ApplicationDeploymentNewJob( + deployment: $next_found, + application: $application + ))->onConnection('long-running')->onQueue('long-running'); + } else { + dispatch(new ApplicationDeploymentJob( + application_deployment_queue_id: $next_found->id, + ))->onConnection('long-running')->onQueue('long-running'); + } } } // Deployment things diff --git a/resources/views/components/applications/navbar.blade.php b/resources/views/components/applications/navbar.blade.php index 9bd048e26..98ddb8d5f 100644 --- a/resources/views/components/applications/navbar.blade.php +++ b/resources/views/components/applications/navbar.blade.php @@ -45,6 +45,19 @@ Restart + @if(isDev()) + + @endif @endif + @if (isDev()) + + @endif @endif @endif diff --git a/resources/views/livewire/project/shared/destination.blade.php b/resources/views/livewire/project/shared/destination.blade.php index d1c8472cd..1ff1abd98 100644 --- a/resources/views/livewire/project/shared/destination.blade.php +++ b/resources/views/livewire/project/shared/destination.blade.php @@ -7,7 +7,8 @@ server {{ data_get($resource, 'destination.server.name') }} in {{ data_get($resource, 'destination.network') }} network. - {{-- {{$resource->additional_destinations}} --}} + Additonal Destinations: + {{$resource->additional_destinations}} {{-- @if (count($servers) > 0)