diff --git a/app/Http/Livewire/Project/Application/DeploymentNavbar.php b/app/Http/Livewire/Project/Application/DeploymentNavbar.php index fb4692158..cbc280fde 100644 --- a/app/Http/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Http/Livewire/Project/Application/DeploymentNavbar.php @@ -5,6 +5,7 @@ use App\Enums\ApplicationDeploymentStatus; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Process; use Livewire\Component; use Illuminate\Support\Str; @@ -29,15 +30,26 @@ public function show_debug() public function cancel() { try { + $kill_command = "kill -9 {$this->application_deployment_queue->current_process_id}"; $application = Application::find($this->application_deployment_queue->application_id); $server = $application->destination->server; if ($this->application_deployment_queue->current_process_id) { $process = Process::run("ps -p {$this->application_deployment_queue->current_process_id} -o command --no-headers"); if (Str::of($process->output())->contains([$server->ip, 'EOF-COOLIFY-SSH'])) { - Process::run("kill -9 {$this->application_deployment_queue->current_process_id}"); + Process::run($kill_command); } - // TODO: Cancelling text in logs + $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); + $new_log_entry = [ + 'command' => $kill_command, + 'output' => "Deployment cancelled by user.", + 'type' => 'stderr', + 'order' => count($previous_logs) + 1, + 'timestamp' => Carbon::now('UTC'), + 'hidden' => false, + ]; + $previous_logs[] = $new_log_entry; $this->application_deployment_queue->update([ + 'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR), 'current_process_id' => null, 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, ]); diff --git a/app/Jobs/ApplicationDeploymentJobNew.php b/app/Jobs/ApplicationDeploymentJobNew.php index 05469b29a..992165388 100644 --- a/app/Jobs/ApplicationDeploymentJobNew.php +++ b/app/Jobs/ApplicationDeploymentJobNew.php @@ -3,6 +3,7 @@ namespace App\Jobs; use App\Enums\ApplicationDeploymentStatus; +use App\Enums\ProxyTypes; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; @@ -11,18 +12,22 @@ use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; +use App\Traits\ExecuteRemoteCommand; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Collection; use Spatie\Url\Url; +use Illuminate\Support\Str; +use Symfony\Component\Yaml\Yaml; use Throwable; use Visus\Cuid2\Cuid2; class ApplicationDeploymentJobNew implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand; public static int $batch_counter = 0; @@ -43,11 +48,20 @@ class ApplicationDeploymentJobNew implements ShouldQueue private string $container_name; private string $workdir; + private string $build_image_name; + private string $production_image_name; private bool $is_debug_enabled; + private $build_args; + private $env_args; + private $docker_compose; + + private $log_model; + private Collection $saved_outputs; public function __construct(int $application_deployment_queue_id) { $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); + $this->log_model = $this->application_deployment_queue; $this->application = Application::find($this->application_deployment_queue->application_id); $this->application_deployment_queue_id = $application_deployment_queue_id; @@ -66,6 +80,7 @@ public function __construct(int $application_deployment_queue_id) $this->container_name = generate_container_name($this->application->uuid); $this->private_key_location = save_private_key_for_server($this->server); + $this->saved_outputs = collect(); // Set preview fqdn if ($this->pull_request_id !== 0) { @@ -99,23 +114,22 @@ public function handle(): void $this->deploy(); } } catch (\Exception $e) { - // $this->execute_now([ - // "echo '\nOops something is not okay, are you okay? 😢'", - // "echo '\n\n{$e->getMessage()}'", - // ]); + ray($e); + $this->execute_remote_command([ + ["echo '\nOops something is not okay, are you okay? 😢'"], + ["echo '\n\n{$e->getMessage()}'"] + ]); $this->fail($e->getMessage()); } finally { // if (isset($this->docker_compose)) { // Storage::disk('deployments')->put(Str::kebab($this->application->name) . '/docker-compose.yml', $this->docker_compose); // } - // execute_remote_command( - // commands: [ - // "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1" - // ], - // server: $this->server, - // queue: $this->application_deployment_queue, - // hide_from_output: true, - // ); + $this->execute_remote_command( + [ + "command" => "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1", + "hidden" => true, + ] + ); } } public function failed(Throwable $exception): void @@ -129,43 +143,32 @@ private function execute_in_builder(string $command) } private function deploy() { - execute_remote_command( - commands: [ - "echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-builder).'", - ], - server: $this->server, - queue: $this->application_deployment_queue, - ); - execute_remote_command( - commands: [ - "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-builder", - ], - server: $this->server, - queue: $this->application_deployment_queue, - show_in_output: false, - ); - execute_remote_command( - commands: [ - "echo 'Done.'", - ], - server: $this->server, - queue: $this->application_deployment_queue, - ); - execute_remote_command( - commands: [ - $this->execute_in_builder("mkdir -p {$this->workdir}") - ], - server: $this->server, - queue: $this->application_deployment_queue, - ); - execute_remote_command( - commands: [ - "echos hello" - ], - server: $this->server, - queue: $this->application_deployment_queue, - ); - $this->next(ApplicationDeploymentStatus::FINISHED->value); + ray()->clearScreen(); + $this->prepare_builder_image(); + $this->clone_repository(); + if (!$this->force_rebuild) { + $this->execute_remote_command([ + "docker images -q {$this->application->uuid}:{$this->commit} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + ]); + if (Str::of($this->saved_outputs->get('local_image_found'))) { + $this->execute_remote_command([ + "echo 'Docker Image found locally with the same Git Commit SHA. Build step skipped...'" + ]); + $this->generate_compose_file(); + $this->stop_running_container(); + $this->start_by_compose_file(); + return $this->next(ApplicationDeploymentStatus::FINISHED->value); + } + } + $this->cleanup_git(); + $this->generate_buildpack(); + $this->generate_compose_file(); + $this->generate_build_env_variables(); + $this->add_build_env_variables_to_dockerfile(); + $this->build_image(); + $this->stop_running_container(); + $this->start_by_compose_file(); + return $this->next(ApplicationDeploymentStatus::FINISHED->value); } private function next(string $status) @@ -178,4 +181,433 @@ private function next(string $status) } queue_next_deployment($this->application); } + private function start_by_compose_file() + { + $this->execute_remote_command( + ["echo -n 'Starting new application... '"], + [$this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true], + ["echo 'Done. 🎉'"], + ); + } + private function stop_running_container() + { + $this->execute_remote_command( + ["echo -n 'Removing old running application.'"], + [$this->execute_in_builder("docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true], + ); + } + private function build_image() + { + $this->execute_remote_command([ + "echo -n 'Building docker image.'", + ]); + + if ($this->application->settings->is_static) { + $this->execute_remote_command([ + $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true + ]); + + $dockerfile = "FROM {$this->application->static_image} +WORKDIR /usr/share/nginx/html/ +LABEL coolify.deploymentId={$this->deployment_uuid} +COPY --from=$this->build_image_name /app/{$this->application->publish_directory} ."; + $docker_file = base64_encode($dockerfile); + + $this->execute_remote_command( + [ + $this->execute_in_builder("echo '{$docker_file}' | base64 -d > {$this->workdir}/Dockerfile-prod") + ], + [ + $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true + ] + ); + } else { + $this->execute_remote_command([ + $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true + ]); + } + } + private function add_build_env_variables_to_dockerfile() + { + $this->execute_remote_command([ + $this->execute_in_builder("cat {$this->workdir}/Dockerfile"), "hidden" => true, "save" => 'dockerfile' + ]); + $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + + foreach ($this->application->build_environment_variables as $env) { + $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}"); + } + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); + $this->execute_remote_command([ + $this->execute_in_builder("echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/Dockerfile"), + "hidden" => true + ]); + } + private function generate_build_env_variables() + { + $this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->commit}"]); + if ($this->pull_request_id === 0) { + foreach ($this->application->build_environment_variables as $env) { + $this->build_args->push("--build-arg {$env->key}={$env->value}"); + } + } else { + foreach ($this->application->build_environment_variables_preview as $env) { + $this->build_args->push("--build-arg {$env->key}={$env->value}"); + } + } + + $this->build_args = $this->build_args->implode(' '); + } + + private function generate_compose_file() + { + $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; + + $persistent_storages = $this->generate_local_persistent_volumes(); + $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); + $environment_variables = $this->generate_environment_variables($ports); + + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $this->container_name => [ + 'image' => $this->production_image_name, + 'container_name' => $this->container_name, + 'restart' => 'always', + 'environment' => $environment_variables, + 'labels' => $this->set_labels_for_applications(), + 'expose' => $ports, + 'networks' => [ + $this->destination->network, + ], + 'healthcheck' => [ + 'test' => [ + 'CMD-SHELL', + $this->generate_healthcheck_commands() + ], + 'interval' => $this->application->health_check_interval . 's', + 'timeout' => $this->application->health_check_timeout . 's', + 'retries' => $this->application->health_check_retries, + 'start_period' => $this->application->health_check_start_period . 's' + ], + 'mem_limit' => $this->application->limits_memory, + 'memswap_limit' => $this->application->limits_memory_swap, + 'mem_swappiness' => $this->application->limits_memory_swappiness, + 'mem_reservation' => $this->application->limits_memory_reservation, + 'cpus' => $this->application->limits_cpus, + 'cpuset' => $this->application->limits_cpuset, + 'cpu_shares' => $this->application->limits_cpu_shares, + ] + ], + 'networks' => [ + $this->destination->network => [ + 'external' => false, + 'name' => $this->destination->network, + 'attachable' => true, + ] + ] + ]; + if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) { + $docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array; + } + if (count($persistent_storages) > 0) { + $docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages; + } + if (count($volume_names) > 0) { + $docker_compose['volumes'] = $volume_names; + } + $this->docker_compose = Yaml::dump($docker_compose, 10); + $docker_compose_base64 = base64_encode($this->docker_compose); + $this->execute_remote_command([$this->execute_in_builder("echo '{$docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); + } + private function generate_local_persistent_volumes() + { + $local_persistent_volumes = []; + foreach ($this->application->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + if ($this->pull_request_id !== 0) { + $volume_name = $volume_name . '-pr-' . $this->pull_request_id; + } + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + ray('local_persistent_volumes', $local_persistent_volumes)->green(); + return $local_persistent_volumes; + } + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->application->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + + if ($this->pull_request_id !== 0) { + $name = $name . '-pr-' . $this->pull_request_id; + } + + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + private function generate_environment_variables($ports) + { + $environment_variables = collect(); + ray('Generate Environment Variables')->green(); + if ($this->pull_request_id === 0) { + ray($this->application->runtime_environment_variables)->green(); + foreach ($this->application->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->value"); + } + } else { + ray($this->application->runtime_environment_variables_preview)->green(); + foreach ($this->application->runtime_environment_variables_preview as $env) { + $environment_variables->push("$env->key=$env->value"); + } + } + // Add PORT if not exists, use the first port as default + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) { + $environment_variables->push("PORT={$ports[0]}"); + } + return $environment_variables->all(); + } + private function generate_healthcheck_commands() + { + if (!$this->application->health_check_port) { + $this->application->health_check_port = $this->application->ports_exposes_array[0]; + } + if ($this->application->health_check_path) { + $generated_healthchecks_commands = [ + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}{$this->application->health_check_path} > /dev/null" + ]; + } else { + $generated_healthchecks_commands = [ + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}/" + ]; + } + return implode(' ', $generated_healthchecks_commands); + } + private function set_labels_for_applications() + { + $labels = []; + $labels[] = 'coolify.managed=true'; + $labels[] = 'coolify.version=' . config('version'); + $labels[] = 'coolify.applicationId=' . $this->application->id; + $labels[] = 'coolify.type=application'; + $labels[] = 'coolify.name=' . $this->application->name; + if ($this->pull_request_id !== 0) { + $labels[] = 'coolify.pullRequestId=' . $this->pull_request_id; + } + if ($this->application->fqdn) { + if ($this->pull_request_id !== 0) { + $preview_fqdn = data_get($this->preview, 'fqdn'); + $template = $this->application->preview_url_template; + $url = Url::fromString($this->application->fqdn); + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2(7); + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $this->preview->fqdn = $preview_fqdn; + $this->preview->save(); + $domains = Str::of($preview_fqdn)->explode(','); + } else { + $domains = Str::of($this->application->fqdn)->explode(','); + } + if ($this->application->destination->server->proxy->type === ProxyTypes::TRAEFIK_V2->value) { + $labels[] = 'traefik.enable=true'; + foreach ($domains as $domain) { + $url = Url::fromString($domain); + $host = $url->getHost(); + $path = $url->getPath(); + $schema = $url->getScheme(); + $slug = Str::slug($host . $path); + + $http_label = "{$this->application->uuid}-{$slug}-http"; + $https_label = "{$this->application->uuid}-{$slug}-https"; + + if ($schema === 'https') { + // Set labels for https + $labels[] = "traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; + $labels[] = "traefik.http.routers.{$https_label}.entryPoints=https"; + $labels[] = "traefik.http.routers.{$https_label}.middlewares=gzip"; + if ($path !== '/') { + $labels[] = "traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix"; + $labels[] = "traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"; + } + + $labels[] = "traefik.http.routers.{$https_label}.tls=true"; + $labels[] = "traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt"; + + // Set labels for http (redirect to https) + $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; + $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http"; + if ($this->application->settings->is_force_https_enabled) { + $labels[] = "traefik.http.routers.{$http_label}.middlewares=redirect-to-https"; + } + } else { + // Set labels for http + $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; + $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http"; + $labels[] = "traefik.http.routers.{$http_label}.middlewares=gzip"; + if ($path !== '/') { + $labels[] = "traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix"; + $labels[] = "traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"; + } + } + } + } + } + return $labels; + } + private function generate_buildpack() + { + $this->execute_remote_command( + [ + "echo -n 'Generating nixpacks configuration.'", + ], + [$this->nixpacks_build_cmd()], + [$this->execute_in_builder("cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")], + [$this->execute_in_builder("rm -f {$this->workdir}/.nixpacks/Dockerfile")] + ); + } + private function nixpacks_build_cmd() + { + $this->generate_env_variables(); + $nixpacks_command = "nixpacks build -o {$this->workdir} {$this->env_args} --no-error-without-start"; + if ($this->application->build_command) { + $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\""; + } + if ($this->application->start_command) { + $nixpacks_command .= " --start-cmd \"{$this->application->start_command}\""; + } + if ($this->application->install_command) { + $nixpacks_command .= " --install-cmd \"{$this->application->install_command}\""; + } + $nixpacks_command .= " {$this->workdir}"; + return $this->execute_in_builder($nixpacks_command); + } + private function generate_env_variables() + { + $this->env_args = collect([]); + if ($this->pull_request_id === 0) { + foreach ($this->application->nixpacks_environment_variables as $env) { + $this->env_args->push("--env {$env->key}={$env->value}"); + } + } else { + foreach ($this->application->nixpacks_environment_variables_preview as $env) { + $this->env_args->push("--env {$env->key}={$env->value}"); + } + } + + $this->env_args = $this->env_args->implode(' '); + } + private function cleanup_git() + { + $this->execute_remote_command( + [$this->execute_in_builder("rm -fr {$this->workdir}/.git")], + ); + } + private function prepare_builder_image() + { + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'" + ], + [ + "echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-builder).'", + ], + [ + "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-builder", + "hidden" => true, + ], + [ + "command" => $this->execute_in_builder("mkdir -p {$this->workdir}") + ], + ); + } + private function set_git_import_settings($git_clone_command) + { + if ($this->application->git_commit_sha !== 'HEAD') { + $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1"; + } + if ($this->application->settings->is_git_submodules_enabled) { + $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git submodule update --init --recursive"; + } + if ($this->application->settings->is_git_lfs_enabled) { + $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git lfs pull"; + } + return $git_clone_command; + } + private function importing_git_repository() + { + $git_clone_command = "git clone -q -b {$this->application->git_branch}"; + if ($this->pull_request_id !== 0) { + $pr_branch_name = "pr-{$this->pull_request_id}-coolify"; + } + + if ($this->application->deploymentType() === 'source') { + $source_html_url = data_get($this->application, 'source.html_url'); + $url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL)); + $source_html_url_host = $url['host']; + $source_html_url_scheme = $url['scheme']; + + if ($this->source->getMorphClass() == 'App\Models\GithubApp') { + if ($this->source->is_public) { + $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->workdir}"; + $git_clone_command = $this->set_git_import_settings($git_clone_command); + + $commands = $this->execute_in_builder($git_clone_command); + + if ($this->pull_request_id !== 0) { + $commands = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name >/dev/null 2>&1 && git checkout $pr_branch_name >/dev/null 2>&1"); + } + return $commands; + } else { + $github_access_token = generate_github_installation_token($this->source); + $commands = $this->execute_in_builder("git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->workdir}"); + if ($this->pull_request_id !== 0) { + $commands[] = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"); + } + return $commands; + } + } + } + if ($this->application->deploymentType() === 'deploy_key') { + $private_key = base64_encode($this->application->private_key->private_key); + $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->workdir}"; + $git_clone_command = $this->set_git_import_settings($git_clone_command); + return [ + $this->execute_in_builder("mkdir -p /root/.ssh"), + $this->execute_in_builder("echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"), + $this->execute_in_builder("chmod 600 /root/.ssh/id_rsa"), + $this->execute_in_builder($git_clone_command) + ]; + } + } + private function clone_repository() + { + $this->execute_remote_command( + [ + "echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} to {$this->workdir}. '" + ], + [ + $this->importing_git_repository() + ], + [ + "command" => $this->execute_in_builder("cd {$this->workdir} && git rev-parse HEAD"), + "hidden" => true, + "save" => "git_commit_sha" + ], + ); + $this->commit = $this->saved_outputs->get('git_commit_sha'); + $this->build_image_name = "{$this->application->git_repository}:{$this->commit}-build"; + $this->production_image_name = "{$this->application->uuid}:{$this->commit}"; + ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); + } } diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php new file mode 100644 index 000000000..6e8c5b761 --- /dev/null +++ b/app/Traits/ExecuteRemoteCommand.php @@ -0,0 +1,83 @@ +server instanceof Server === false) { + throw new \RuntimeException('Server is not set or is not an instance of Server model'); + } + + $ip = data_get($this->server, 'ip'); + $user = data_get($this->server, 'user'); + $port = data_get($this->server, 'port'); + $private_key_location = get_private_key_for_server($this->server); + + $commandsText->each(function ($single_command) use ($private_key_location, $ip, $user, $port) { + $command = data_get($single_command, 'command') ?? $single_command[0] ?? null; + if ($command === null) { + throw new \RuntimeException('Command is not set'); + } + $hidden = data_get($single_command, 'hidden', false); + $ignore_errors = data_get($single_command, 'ignore_errors', false); + $this->save = data_get($single_command, 'save'); + + $remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command); + $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { + $new_log_entry = [ + 'command' => $command, + 'output' => $output, + 'type' => $type === 'err' ? 'stderr' : 'stdout', + 'timestamp' => Carbon::now('UTC'), + 'hidden' => $hidden, + 'batch' => static::$batch_counter, + ]; + + if (!$this->log_model->logs) { + $new_log_entry['order'] = 1; + } else { + $previous_logs = json_decode($this->log_model->logs, associative: true, flags: JSON_THROW_ON_ERROR); + $new_log_entry['order'] = count($previous_logs) + 1; + } + + $previous_logs[] = $new_log_entry; + $this->log_model->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR); + $this->log_model->save(); + + if ($this->save) { + $this->saved_outputs[$this->save] = Str::of($output)->trim(); + } + }); + $this->log_model->update([ + 'current_process_id' => $process->id(), + ]); + + $process_result = $process->wait(); + if ($process_result->exitCode() !== 0) { + if (!$ignore_errors) { + $status = ApplicationDeploymentStatus::FAILED->value; + $this->log_model->status = $status; + $this->log_model->save(); + throw new \RuntimeException($process_result->errorOutput()); + } + } + }); + } +} diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index b0e283d70..c4bbd8d68 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -128,7 +128,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d } try { $decoded = json_decode( - data_get($application_deployment_queue, 'log'), + data_get($application_deployment_queue, 'logs'), associative: true, flags: JSON_THROW_ON_ERROR ); @@ -137,10 +137,10 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d } $formatted = collect($decoded); if (!$is_debug_enabled) { - - $formatted = $formatted->filter(fn ($i) => $i['show_in_output'] ?? true); + $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); } - $formatted = $formatted->sortBy(fn ($i) => $i['order']) + $formatted = $formatted + ->sortBy(fn ($i) => $i['order']) ->map(function ($i) { $i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u'); return $i; @@ -148,51 +148,3 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d return $formatted; } -function execute_remote_command(array|Collection $commands, Server $server, ApplicationDeploymentQueue $queue, bool $show_in_output = true, bool $ignore_errors = false) -{ - if ($commands instanceof Collection) { - $commandsText = $commands; - } else { - $commandsText = collect($commands); - } - $ip = data_get($server, 'ip'); - $user = data_get($server, 'user'); - $port = data_get($server, 'port'); - $private_key_location = get_private_key_for_server($server); - $commandsText->each(function ($command) use ($queue, $private_key_location, $ip, $user, $port, $show_in_output, $ignore_errors) { - $remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command); - $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($queue, $command, $show_in_output) { - $new_log_entry = [ - 'command' => $command, - 'output' => $output, - 'type' => $type === 'err' ? 'stderr' : 'stdout', - 'timestamp' => Carbon::now('UTC'), - 'show_in_output' => $show_in_output, - ]; - - if (!$queue->log) { - $new_log_entry['order'] = 1; - } else { - $previous_logs = json_decode($queue->log, associative: true, flags: JSON_THROW_ON_ERROR); - $new_log_entry['order'] = count($previous_logs) + 1; - } - - $previous_logs[] = $new_log_entry; - $queue->log = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);; - $queue->save(); - }); - $queue->update([ - 'current_process_id' => $process->id(), - ]); - - $process_result = $process->wait(); - if ($process_result->exitCode() !== 0) { - if (!$ignore_errors) { - $status = ApplicationDeploymentStatus::FAILED->value; - $queue->status = $status; - $queue->save(); - throw new \RuntimeException($process_result->errorOutput()); - } - } - }); -} diff --git a/database/migrations/2023_06_23_114133_use_application_deployment_queues_as_activity.php b/database/migrations/2023_06_23_114133_use_application_deployment_queues_as_activity.php index 2e142f788..36623ee7b 100644 --- a/database/migrations/2023_06_23_114133_use_application_deployment_queues_as_activity.php +++ b/database/migrations/2023_06_23_114133_use_application_deployment_queues_as_activity.php @@ -12,7 +12,7 @@ public function up(): void { Schema::table('application_deployment_queues', function (Blueprint $table) { - $table->text('log')->default(null)->nullable(); + $table->text('logs')->default(null)->nullable(); $table->string('current_process_id')->default(null)->nullable(); }); } @@ -23,7 +23,7 @@ public function up(): void public function down(): void { Schema::table('application_deployment_queues', function (Blueprint $table) { - $table->dropColumn('log'); + $table->dropColumn('logs'); $table->dropColumn('current_process_id'); }); } diff --git a/resources/views/livewire/project/application/deployment-logs.blade.php b/resources/views/livewire/project/application/deployment-logs.blade.php index 1da9c1bbb..b3ed8faef 100644 --- a/resources/views/livewire/project/application/deployment-logs.blade.php +++ b/resources/views/livewire/project/application/deployment-logs.blade.php @@ -17,12 +17,12 @@ class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-dott @if (decode_remote_command_output($application_deployment_queue)->count() > 0) @foreach (decode_remote_command_output($application_deployment_queue) as $line) - $line['type'] == 'stdout', 'text-error' => $line['type'] == 'stderr', - ])> - [{{ $line['timestamp'] }}] {{ $line['output'] }} + 'text-warning' => $line['hidden'], + ])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
Command: {{ $line['command'] }}
Output: @endif{{ $line['output'] }}@if ($line['hidden']) @endif @endforeach @else No logs yet.