diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php index 530dd259a..802b3180f 100644 --- a/app/Console/Commands/ServicesGenerate.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -26,7 +26,7 @@ class ServicesGenerate extends Command */ public function handle() { - ray()->clearAll(); + // ray()->clearAll(); $files = array_diff(scandir(base_path('templates/compose')), ['.', '..']); $files = array_filter($files, function ($file) { return strpos($file, '.yaml') !== false; diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index a90fadb53..d108bca14 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -26,6 +26,8 @@ class General extends Component public bool $labelsChanged = false; public bool $isConfigurationChanged = false; + public ?string $initialDockerComposeLocation = null; + public bool $is_static; public $parsedServices = []; @@ -109,6 +111,7 @@ class General extends Component } else { $this->customLabels = str($this->application->custom_labels)->replace(',', "\n"); } + $this->initialDockerComposeLocation = $this->application->docker_compose_location; $this->checkLabelUpdates(); } public function instantSave() @@ -118,35 +121,30 @@ class General extends Component } public function loadComposeFile($isInit = false) { - if ($isInit && $this->application->docker_compose_raw) { - return; - } - $uuid = new Cuid2(); - ['commands' => $cloneCommand] = $this->application->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.'); - $workdir = rtrim($this->application->base_directory, '/'); - $composeFile = $this->application->docker_compose_location; - $commands = collect([ - "mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}", - $cloneCommand, - "git sparse-checkout init --cone", - "git sparse-checkout set .$workdir$composeFile", - "git read-tree -mu HEAD", - "cat .$workdir$composeFile", - ]); - $composeFileContent = instant_remote_process($commands, $this->application->destination->server, false); - if (!$composeFileContent) { - $this->emit('error', "Could not load compose file from $workdir$composeFile"); - return; - } else { - $this->application->docker_compose_raw = $composeFileContent; + try { + if ($isInit && $this->application->docker_compose_raw) { + return; + } + ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit); + $this->emit('success', 'Docker compose file loaded.'); + } catch (\Throwable $e) { + $this->application->docker_compose_location = $this->initialDockerComposeLocation; $this->application->save(); + return handleError($e, $this); } - $commands = collect([ - "rm -rf /tmp/{$uuid}", - ]); - instant_remote_process($commands, $this->application->destination->server, false); - $this->parsedServices = $this->application->parseCompose(); - $this->emit('success', 'Compose file loaded.'); + } + public function generateDomain(string $serviceName) + { + $domain = $this->parsedServiceDomains[$serviceName]['domain'] ?? null; + if (!$domain) { + $uuid = new Cuid2(7); + $domain = generateFqdn($this->application->destination->server, $uuid); + $this->parsedServiceDomains[$serviceName]['domain'] = $domain; + $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); + $this->application->save(); + $this->emit('success', 'Domain generated.'); + } + return $domain; } public function updatedApplicationBuildPack() { @@ -190,6 +188,9 @@ class General extends Component public function submit($showToaster = true) { try { + if ($this->initialDockerComposeLocation !== $this->application->docker_compose_location) { + $this->loadComposeFile(); + } $this->validate(); if ($this->ports_exposes !== $this->application->ports_exposes) { $this->resetDefaultLabels(false); diff --git a/app/Http/Livewire/Project/Application/Heading.php b/app/Http/Livewire/Project/Application/Heading.php index e09e469a9..dae105cd6 100644 --- a/app/Http/Livewire/Project/Application/Heading.php +++ b/app/Http/Livewire/Project/Application/Heading.php @@ -41,6 +41,10 @@ class Heading extends Component public function deploy(bool $force_rebuild = false) { + if (!$this->application->deployableComposeBuildPack()) { + $this->emit('error', 'Please load a Compose file first.'); + return; + } $this->setDeploymentUuid(); queue_application_deployment( application_id: $this->application->id, @@ -68,7 +72,8 @@ class Heading extends Component $this->application->save(); $this->application->refresh(); } - public function restart() { + public function restart() + { $this->setDeploymentUuid(); queue_application_deployment( application_id: $this->application->id, diff --git a/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php index eed0f7052..dc2808d08 100644 --- a/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -59,6 +59,7 @@ class Show extends Component { $this->validate(); $this->env->save(); + ray($this->env); $this->emit('success', 'Environment variable updated successfully.'); $this->emit('refreshEnvs'); } diff --git a/app/Http/Livewire/Project/Shared/Logs.php b/app/Http/Livewire/Project/Shared/Logs.php index f58ec672a..982f729b4 100644 --- a/app/Http/Livewire/Project/Shared/Logs.php +++ b/app/Http/Livewire/Project/Shared/Logs.php @@ -17,13 +17,15 @@ class Logs extends Component public ?string $type = null; public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource; public Server $server; - public ?string $container = null; + public $container = []; + public $containers; public $parameters; public $query; public $status; public function mount() { + $this->containers = collect(); $this->parameters = get_route_parameters(); $this->query = request()->query(); if (data_get($this->parameters, 'application_uuid')) { @@ -33,7 +35,9 @@ class Logs extends Component $this->server = $this->resource->destination->server; $containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0); if ($containers->count() > 0) { - $this->container = data_get($containers[0], 'Names'); + $containers->each(function ($container) { + $this->containers->push(str_replace('/', '', $container['Names'])); + }); } } else if (data_get($this->parameters, 'database_uuid')) { $this->type = 'database'; diff --git a/app/Jobs/ApplicationDeployDockerImageJob.php b/app/Jobs/ApplicationDeployDockerImageJob.php index c21ad5680..9dfdad1f1 100644 --- a/app/Jobs/ApplicationDeployDockerImageJob.php +++ b/app/Jobs/ApplicationDeployDockerImageJob.php @@ -30,7 +30,7 @@ class ApplicationDeployDockerImageJob implements ShouldQueue, ShouldBeEncrypted } public function handle() { - ray()->clearAll(); + // ray()->clearAll(); ray('Deploying Docker Image'); static::$batch_counter = 0; try { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index ac7d4dde7..f4982ad7d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -76,7 +76,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private string $docker_compose_location = '/docker-compose.yml'; private ?string $addHosts = null; private ?string $buildTarget = null; - private $log_model; private Collection $saved_outputs; private ?string $full_healthcheck_url = null; @@ -93,9 +92,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted public $tries = 1; public function __construct(int $application_deployment_queue_id) { - // ray()->clearScreen(); $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->build_pack = data_get($this->application, 'build_pack'); @@ -187,7 +184,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository(); try { - ray($this->application->build_pack); if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { $this->just_restart(); if ($this->server->isProxyShouldRun()) { @@ -451,7 +447,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->docker_compose_location = $this->application->docker_compose_location; } $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); - $this->server->executeRemoteCommand( commands: $this->application->prepareHelperImage($this->deployment_uuid), loggingModel: $this->application_deployment_queue @@ -474,6 +469,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->save_environment_variables(); $this->stop_running_container(force: true); $this->start_by_compose_file(); + $this->application->loadComposeFile(isInit: false); } private function deploy_dockerfile_buildpack() { @@ -752,14 +748,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private function clone_repository() { $importCommands = $this->generate_git_import_commands(); - ray($importCommands); + $this->application_deployment_queue->addLogEntry("\n----------------------------------------"); + $this->application_deployment_queue->addLogEntry("Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}."); $this->execute_remote_command( - [ - "echo '\n----------------------------------------'", - ], - [ - "echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '" - ], [ $importCommands, "hidden" => true ] @@ -768,7 +759,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private function generate_git_import_commands() { - ['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands($this->deployment_uuid, $this->pull_request_id, $this->git_type); return $commands; } @@ -1178,10 +1168,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function stop_running_container(bool $force = false) { - $this->execute_remote_command(["echo -n 'Removing old container.'"]); + $this->application_deployment_queue->addLogEntry("Removing old containers."); if ($this->newVersionIsHealthy || $force) { $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); - ray($containers); if ($this->pull_request_id !== 0) { $containers = $containers->filter(function ($container) { return data_get($container, 'Names') === $this->container_name; @@ -1197,14 +1186,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ); }); - $this->execute_remote_command( - [ - "echo 'Rolling update completed.'" - ], - ); + $this->application_deployment_queue->addLogEntry("Rolling update completed."); } else { + $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container."); $this->execute_remote_command( - ["echo -n 'New container is not healthy, rolling back to the old container.'"], [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ); } @@ -1213,8 +1198,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function start_by_compose_file() { if ($this->application->build_pack === 'dockerimage') { + $this->application_deployment_queue->addLogEntry("Pulling latest images from the registry."); $this->execute_remote_command( - ["echo -n 'Pulling latest images from the registry.'"], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], ); diff --git a/app/Jobs/MultipleApplicationDeploymentJob.php b/app/Jobs/MultipleApplicationDeploymentJob.php index 32c98d3b0..4c3cc29c9 100644 --- a/app/Jobs/MultipleApplicationDeploymentJob.php +++ b/app/Jobs/MultipleApplicationDeploymentJob.php @@ -91,7 +91,6 @@ class MultipleApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted public $tries = 1; public function __construct(int $application_deployment_queue_id) { - // ray()->clearScreen(); $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); diff --git a/app/Models/Application.php b/app/Models/Application.php index 20f61ed39..3b0926659 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -8,6 +8,7 @@ use Spatie\Activitylog\Models\Activity; use Illuminate\Support\Str; use RuntimeException; use Symfony\Component\Yaml\Yaml; +use Visus\Cuid2\Cuid2; class Application extends BaseModel { @@ -47,6 +48,10 @@ class Application extends BaseModel $application->environment_variables_preview()->delete(); }); } + public function deployableComposeBuildPack() + { + return $this->build_pack === 'dockercompose' && $this->docker_compose_raw; + } public function link() { return route('project.application.configuration', [ @@ -592,4 +597,42 @@ class Application extends BaseModel return collect([]); } } + function loadComposeFile($isInit = false) + { + $initialDockerComposeLocation = $this->docker_compose_location; + if ($this->build_pack === 'dockercompose') { + if ($isInit && $this->docker_compose_raw) { + return; + } + $uuid = new Cuid2(); + ['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.'); + $workdir = rtrim($this->base_directory, '/'); + $composeFile = $this->docker_compose_location; + $commands = collect([ + "mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}", + $cloneCommand, + "git sparse-checkout init --cone", + "git sparse-checkout set .$workdir$composeFile", + "git read-tree -mu HEAD", + "cat .$workdir$composeFile", + ]); + $composeFileContent = instant_remote_process($commands, $this->destination->server, false); + if (!$composeFileContent) { + $this->docker_compose_location = $initialDockerComposeLocation; + $this->save(); + throw new \Exception("Could not load compose file from $workdir$composeFile"); + } else { + $this->docker_compose_raw = $composeFileContent; + $this->save(); + } + $commands = collect([ + "rm -rf /tmp/{$uuid}", + ]); + instant_remote_process($commands, $this->destination->server, false); + return [ + 'parsedServices' => $this->parseCompose(), + 'initialDockerComposeLocation' => $this->docker_compose_location + ]; + } + } } diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index bc9b9471a..99aed3750 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -9,7 +9,8 @@ class ApplicationDeploymentQueue extends Model { protected $guarded = []; - public function getOutput($name) { + public function getOutput($name) + { if (!$this->logs) { return null; } @@ -21,9 +22,13 @@ class ApplicationDeploymentQueue extends Model if ($type === 'error') { $type = 'stderr'; } + $message = str($message)->trim(); + if ($message->startsWith('╔')) { + $message = "\n" . $message; + } $newLogEntry = [ 'command' => null, - 'output' => $message, + 'output' => remove_iip($message), 'type' => $type, 'timestamp' => Carbon::now('UTC'), 'hidden' => $hidden, diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 42d0d9e0f..6c0dc5d03 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -24,7 +24,6 @@ trait ExecuteRemoteCommand if ($this->server instanceof Server === false) { throw new \RuntimeException('Server is not set or is not an instance of Server model'); } - $commandsText->each(function ($single_command) { $command = data_get($single_command, 'command') ?? $single_command[0] ?? null; if ($command === null) { @@ -49,32 +48,29 @@ trait ExecuteRemoteCommand 'hidden' => $hidden, 'batch' => static::$batch_counter, ]; - - if (!$this->log_model->logs) { + if (!$this->application_deployment_queue->logs) { $new_log_entry['order'] = 1; } else { - $previous_logs = json_decode($this->log_model->logs, associative: true, flags: JSON_THROW_ON_ERROR); + $previous_logs = json_decode($this->application_deployment_queue->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(); + $this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR); + $this->application_deployment_queue->save(); if ($this->save) { $this->saved_outputs[$this->save] = Str::of($output)->trim(); } }); - $this->log_model->update([ + $this->application_deployment_queue->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(); + $this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value; + $this->application_deployment_queue->save(); throw new \RuntimeException($process_result->errorOutput()); } } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index d6ef6f1ce..616e0e864 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -274,3 +274,18 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview } return $labels->all(); } + +function isDatabaseImage(string $image) +{ + $image = str($image); + if ($image->contains(':')) { + $image = str($image); + } else { + $image = str($image)->append(':latest'); + } + $imageName = $image->before(':'); + if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) { + return true; + } + return false; +} diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 4f755c89e..45c8ae0e5 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -151,6 +151,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d if (is_null($application_deployment_queue)) { return collect([]); } + // ray(data_get($application_deployment_queue, 'logs')); try { $decoded = json_decode( data_get($application_deployment_queue, 'logs'), @@ -160,6 +161,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d } catch (\JsonException $exception) { return collect([]); } + // ray($decoded ); $formatted = collect($decoded); if (!$is_debug_enabled) { $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 4f47d2332..a01017c13 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -580,7 +580,7 @@ function getTopLevelNetworks(Service|Application $resource) } function parseDockerComposeFile(Service|Application $resource, bool $isNew = false) { - ray()->clearAll(); + // ray()->clearAll(); if ($resource->getMorphClass() === 'App\Models\Service') { if ($resource->docker_compose_raw) { try { @@ -627,18 +627,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $containerName = "$serviceName-{$resource->uuid}"; // Decide if the service is a database - $isDatabase = false; + $isDatabase = isDatabaseImage(data_get_str($service, 'image')); $image = data_get_str($service, 'image'); - if ($image->contains(':')) { - $image = Str::of($image); - } else { - $image = Str::of($image)->append(':latest'); - } - $imageName = $image->before(':'); - - if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) { - $isDatabase = true; - } data_set($service, 'is_database', $isDatabase); // Create new serviceApplication or serviceDatabase @@ -1093,7 +1083,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $resource->docker_compose = Yaml::dump($finalServices, 10, 2); $resource->save(); $resource->saveComposeConfigs(); - return $finalServices; + return collect($finalServices); } else { return collect([]); } @@ -1141,18 +1131,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $containerName = "$serviceName-{$resource->uuid}"; // Decide if the service is a database - $isDatabase = false; + $isDatabase = isDatabaseImage(data_get_str($service, 'image')); $image = data_get_str($service, 'image'); - if ($image->contains(':')) { - $image = Str::of($image); - } else { - $image = Str::of($image)->append(':latest'); - } - $imageName = $image->before(':'); - - if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) { - $isDatabase = true; - } data_set($service, 'is_database', $isDatabase); // Collect/create/update networks @@ -1367,14 +1347,14 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($foundEnv) { $defaultValue = data_get($foundEnv, 'value'); } + $isBuildTime = data_get($foundEnv, 'is_build_time', false); EnvironmentVariable::updateOrCreate([ - 'key' => $key, + 'key' => $key->value(), 'application_id' => $resource->id, ], [ 'value' => $defaultValue, - 'is_build_time' => false, - 'service_id' => $resource->id, - 'is_preview' => false, + 'is_build_time' => $isBuildTime, + 'application_id' => $resource->id, ]); } } diff --git a/resources/views/components/applications/links.blade.php b/resources/views/components/applications/links.blade.php index 1c716a3af..44d69df62 100644 --- a/resources/views/components/applications/links.blade.php +++ b/resources/views/components/applications/links.blade.php @@ -16,22 +16,43 @@ @endif @if (data_get($application, 'fqdn')) - @foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn) -