fix: new logging for deployment jobs

fix: git based docker compose files
This commit is contained in:
Andras Bacsai 2023-11-27 11:54:55 +01:00
parent fae97e4dee
commit 8d86d53292
21 changed files with 247 additions and 168 deletions

@ -26,7 +26,7 @@ class ServicesGenerate extends Command
*/ */
public function handle() public function handle()
{ {
ray()->clearAll(); // ray()->clearAll();
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']); $files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
$files = array_filter($files, function ($file) { $files = array_filter($files, function ($file) {
return strpos($file, '.yaml') !== false; return strpos($file, '.yaml') !== false;

@ -26,6 +26,8 @@ class General extends Component
public bool $labelsChanged = false; public bool $labelsChanged = false;
public bool $isConfigurationChanged = false; public bool $isConfigurationChanged = false;
public ?string $initialDockerComposeLocation = null;
public bool $is_static; public bool $is_static;
public $parsedServices = []; public $parsedServices = [];
@ -109,6 +111,7 @@ class General extends Component
} else { } else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n"); $this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
} }
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates(); $this->checkLabelUpdates();
} }
public function instantSave() public function instantSave()
@ -118,35 +121,30 @@ class General extends Component
} }
public function loadComposeFile($isInit = false) public function loadComposeFile($isInit = false)
{ {
if ($isInit && $this->application->docker_compose_raw) { try {
return; 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: '.'); ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
$workdir = rtrim($this->application->base_directory, '/'); $this->emit('success', 'Docker compose file loaded.');
$composeFile = $this->application->docker_compose_location; } catch (\Throwable $e) {
$commands = collect([ $this->application->docker_compose_location = $this->initialDockerComposeLocation;
"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;
$this->application->save(); $this->application->save();
return handleError($e, $this);
} }
$commands = collect([ }
"rm -rf /tmp/{$uuid}", public function generateDomain(string $serviceName)
]); {
instant_remote_process($commands, $this->application->destination->server, false); $domain = $this->parsedServiceDomains[$serviceName]['domain'] ?? null;
$this->parsedServices = $this->application->parseCompose(); if (!$domain) {
$this->emit('success', 'Compose file loaded.'); $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() public function updatedApplicationBuildPack()
{ {
@ -190,6 +188,9 @@ class General extends Component
public function submit($showToaster = true) public function submit($showToaster = true)
{ {
try { try {
if ($this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
$this->loadComposeFile();
}
$this->validate(); $this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) { if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false); $this->resetDefaultLabels(false);

@ -41,6 +41,10 @@ class Heading extends Component
public function deploy(bool $force_rebuild = false) public function deploy(bool $force_rebuild = false)
{ {
if (!$this->application->deployableComposeBuildPack()) {
$this->emit('error', 'Please load a Compose file first.');
return;
}
$this->setDeploymentUuid(); $this->setDeploymentUuid();
queue_application_deployment( queue_application_deployment(
application_id: $this->application->id, application_id: $this->application->id,
@ -68,7 +72,8 @@ class Heading extends Component
$this->application->save(); $this->application->save();
$this->application->refresh(); $this->application->refresh();
} }
public function restart() { public function restart()
{
$this->setDeploymentUuid(); $this->setDeploymentUuid();
queue_application_deployment( queue_application_deployment(
application_id: $this->application->id, application_id: $this->application->id,

@ -59,6 +59,7 @@ class Show extends Component
{ {
$this->validate(); $this->validate();
$this->env->save(); $this->env->save();
ray($this->env);
$this->emit('success', 'Environment variable updated successfully.'); $this->emit('success', 'Environment variable updated successfully.');
$this->emit('refreshEnvs'); $this->emit('refreshEnvs');
} }

@ -17,13 +17,15 @@ class Logs extends Component
public ?string $type = null; public ?string $type = null;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource; public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public Server $server; public Server $server;
public ?string $container = null; public $container = [];
public $containers;
public $parameters; public $parameters;
public $query; public $query;
public $status; public $status;
public function mount() public function mount()
{ {
$this->containers = collect();
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
if (data_get($this->parameters, 'application_uuid')) { if (data_get($this->parameters, 'application_uuid')) {
@ -33,7 +35,9 @@ class Logs extends Component
$this->server = $this->resource->destination->server; $this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0); $containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($containers->count() > 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')) { } else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database'; $this->type = 'database';

@ -30,7 +30,7 @@ class ApplicationDeployDockerImageJob implements ShouldQueue, ShouldBeEncrypted
} }
public function handle() public function handle()
{ {
ray()->clearAll(); // ray()->clearAll();
ray('Deploying Docker Image'); ray('Deploying Docker Image');
static::$batch_counter = 0; static::$batch_counter = 0;
try { try {

@ -76,7 +76,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private string $docker_compose_location = '/docker-compose.yml'; private string $docker_compose_location = '/docker-compose.yml';
private ?string $addHosts = null; private ?string $addHosts = null;
private ?string $buildTarget = null; private ?string $buildTarget = null;
private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
private ?string $full_healthcheck_url = null; private ?string $full_healthcheck_url = null;
@ -93,9 +92,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public $tries = 1; public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
// ray()->clearScreen();
$this->application_deployment_queue = ApplicationDeploymentQueue::find($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 = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack'); $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(); ['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
try { try {
ray($this->application->build_pack);
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart(); $this->just_restart();
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
@ -451,7 +447,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->docker_compose_location = $this->application->docker_compose_location; $this->docker_compose_location = $this->application->docker_compose_location;
} }
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
$this->server->executeRemoteCommand( $this->server->executeRemoteCommand(
commands: $this->application->prepareHelperImage($this->deployment_uuid), commands: $this->application->prepareHelperImage($this->deployment_uuid),
loggingModel: $this->application_deployment_queue loggingModel: $this->application_deployment_queue
@ -474,6 +469,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->save_environment_variables(); $this->save_environment_variables();
$this->stop_running_container(force: true); $this->stop_running_container(force: true);
$this->start_by_compose_file(); $this->start_by_compose_file();
$this->application->loadComposeFile(isInit: false);
} }
private function deploy_dockerfile_buildpack() private function deploy_dockerfile_buildpack()
{ {
@ -752,14 +748,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function clone_repository() private function clone_repository()
{ {
$importCommands = $this->generate_git_import_commands(); $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( $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 $importCommands, "hidden" => true
] ]
@ -768,7 +759,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_git_import_commands() 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); ['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands($this->deployment_uuid, $this->pull_request_id, $this->git_type);
return $commands; return $commands;
} }
@ -1178,10 +1168,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function stop_running_container(bool $force = false) 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) { if ($this->newVersionIsHealthy || $force) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
ray($containers);
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$containers = $containers->filter(function ($container) { $containers = $containers->filter(function ($container) {
return data_get($container, 'Names') === $this->container_name; 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], [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
); );
}); });
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Rolling update completed.");
[
"echo 'Rolling update completed.'"
],
);
} else { } else {
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
$this->execute_remote_command( $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], [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() private function start_by_compose_file()
{ {
if ($this->application->build_pack === 'dockerimage') { if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command( $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} pull"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
); );

@ -91,7 +91,6 @@ class MultipleApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public $tries = 1; public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
// ray()->clearScreen();
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue; $this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id); $this->application = Application::find($this->application_deployment_queue->application_id);

@ -8,6 +8,7 @@ use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
class Application extends BaseModel class Application extends BaseModel
{ {
@ -47,6 +48,10 @@ class Application extends BaseModel
$application->environment_variables_preview()->delete(); $application->environment_variables_preview()->delete();
}); });
} }
public function deployableComposeBuildPack()
{
return $this->build_pack === 'dockercompose' && $this->docker_compose_raw;
}
public function link() public function link()
{ {
return route('project.application.configuration', [ return route('project.application.configuration', [
@ -592,4 +597,42 @@ class Application extends BaseModel
return collect([]); 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
];
}
}
} }

@ -9,7 +9,8 @@ class ApplicationDeploymentQueue extends Model
{ {
protected $guarded = []; protected $guarded = [];
public function getOutput($name) { public function getOutput($name)
{
if (!$this->logs) { if (!$this->logs) {
return null; return null;
} }
@ -21,9 +22,13 @@ class ApplicationDeploymentQueue extends Model
if ($type === 'error') { if ($type === 'error') {
$type = 'stderr'; $type = 'stderr';
} }
$message = str($message)->trim();
if ($message->startsWith('╔')) {
$message = "\n" . $message;
}
$newLogEntry = [ $newLogEntry = [
'command' => null, 'command' => null,
'output' => $message, 'output' => remove_iip($message),
'type' => $type, 'type' => $type,
'timestamp' => Carbon::now('UTC'), 'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden, 'hidden' => $hidden,

@ -24,7 +24,6 @@ trait ExecuteRemoteCommand
if ($this->server instanceof Server === false) { if ($this->server instanceof Server === false) {
throw new \RuntimeException('Server is not set or is not an instance of Server model'); throw new \RuntimeException('Server is not set or is not an instance of Server model');
} }
$commandsText->each(function ($single_command) { $commandsText->each(function ($single_command) {
$command = data_get($single_command, 'command') ?? $single_command[0] ?? null; $command = data_get($single_command, 'command') ?? $single_command[0] ?? null;
if ($command === null) { if ($command === null) {
@ -49,32 +48,29 @@ trait ExecuteRemoteCommand
'hidden' => $hidden, 'hidden' => $hidden,
'batch' => static::$batch_counter, 'batch' => static::$batch_counter,
]; ];
if (!$this->application_deployment_queue->logs) {
if (!$this->log_model->logs) {
$new_log_entry['order'] = 1; $new_log_entry['order'] = 1;
} else { } 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; $new_log_entry['order'] = count($previous_logs) + 1;
} }
$previous_logs[] = $new_log_entry; $previous_logs[] = $new_log_entry;
$this->log_model->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR); $this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
$this->log_model->save(); $this->application_deployment_queue->save();
if ($this->save) { if ($this->save) {
$this->saved_outputs[$this->save] = Str::of($output)->trim(); $this->saved_outputs[$this->save] = Str::of($output)->trim();
} }
}); });
$this->log_model->update([ $this->application_deployment_queue->update([
'current_process_id' => $process->id(), 'current_process_id' => $process->id(),
]); ]);
$process_result = $process->wait(); $process_result = $process->wait();
if ($process_result->exitCode() !== 0) { if ($process_result->exitCode() !== 0) {
if (!$ignore_errors) { if (!$ignore_errors) {
$status = ApplicationDeploymentStatus::FAILED->value; $this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value;
$this->log_model->status = $status; $this->application_deployment_queue->save();
$this->log_model->save();
throw new \RuntimeException($process_result->errorOutput()); throw new \RuntimeException($process_result->errorOutput());
} }
} }

@ -274,3 +274,18 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
} }
return $labels->all(); 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;
}

@ -151,6 +151,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
if (is_null($application_deployment_queue)) { if (is_null($application_deployment_queue)) {
return collect([]); return collect([]);
} }
// ray(data_get($application_deployment_queue, 'logs'));
try { try {
$decoded = json_decode( $decoded = json_decode(
data_get($application_deployment_queue, 'logs'), data_get($application_deployment_queue, 'logs'),
@ -160,6 +161,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
} catch (\JsonException $exception) { } catch (\JsonException $exception) {
return collect([]); return collect([]);
} }
// ray($decoded );
$formatted = collect($decoded); $formatted = collect($decoded);
if (!$is_debug_enabled) { if (!$is_debug_enabled) {
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);

@ -580,7 +580,7 @@ function getTopLevelNetworks(Service|Application $resource)
} }
function parseDockerComposeFile(Service|Application $resource, bool $isNew = false) function parseDockerComposeFile(Service|Application $resource, bool $isNew = false)
{ {
ray()->clearAll(); // ray()->clearAll();
if ($resource->getMorphClass() === 'App\Models\Service') { if ($resource->getMorphClass() === 'App\Models\Service') {
if ($resource->docker_compose_raw) { if ($resource->docker_compose_raw) {
try { try {
@ -627,18 +627,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$containerName = "$serviceName-{$resource->uuid}"; $containerName = "$serviceName-{$resource->uuid}";
// Decide if the service is a database // Decide if the service is a database
$isDatabase = false; $isDatabase = isDatabaseImage(data_get_str($service, 'image'));
$image = 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); data_set($service, 'is_database', $isDatabase);
// Create new serviceApplication or serviceDatabase // 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->docker_compose = Yaml::dump($finalServices, 10, 2);
$resource->save(); $resource->save();
$resource->saveComposeConfigs(); $resource->saveComposeConfigs();
return $finalServices; return collect($finalServices);
} else { } else {
return collect([]); return collect([]);
} }
@ -1141,18 +1131,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$containerName = "$serviceName-{$resource->uuid}"; $containerName = "$serviceName-{$resource->uuid}";
// Decide if the service is a database // Decide if the service is a database
$isDatabase = false; $isDatabase = isDatabaseImage(data_get_str($service, 'image'));
$image = 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); data_set($service, 'is_database', $isDatabase);
// Collect/create/update networks // Collect/create/update networks
@ -1367,14 +1347,14 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($foundEnv) { if ($foundEnv) {
$defaultValue = data_get($foundEnv, 'value'); $defaultValue = data_get($foundEnv, 'value');
} }
$isBuildTime = data_get($foundEnv, 'is_build_time', false);
EnvironmentVariable::updateOrCreate([ EnvironmentVariable::updateOrCreate([
'key' => $key, 'key' => $key->value(),
'application_id' => $resource->id, 'application_id' => $resource->id,
], [ ], [
'value' => $defaultValue, 'value' => $defaultValue,
'is_build_time' => false, 'is_build_time' => $isBuildTime,
'service_id' => $resource->id, 'application_id' => $resource->id,
'is_preview' => false,
]); ]);
} }
} }

@ -16,22 +16,43 @@
</li> </li>
@endif @endif
@if (data_get($application, 'fqdn')) @if (data_get($application, 'fqdn'))
@foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn) @if (data_get($application, 'build_pack') === 'dockercompose')
<li> @foreach (collect(json_decode($this->application->docker_compose_domains)) as $fqdn)
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" @if (data_get($fqdn, 'domain'))
target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}"> <li>
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" target="_blank" href="{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}">
stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
<path d="M9 15l6 -6" /> stroke-linejoin="round">
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path <path d="M9 15l6 -6" />
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /> <path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
</svg>{{ getFqdnWithoutPort($fqdn) }} <path
</a> d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</li> </svg>{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}
@endforeach </a>
</li>
@endif
@endforeach
@else
@foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getFqdnWithoutPort($fqdn) }}
</a>
</li>
@endforeach
@endif
@endif @endif
@if (data_get($application, 'previews', collect([]))->count() > 0) @if (data_get($application, 'previews', collect([]))->count() > 0)
@foreach (data_get($application, 'previews') as $preview) @foreach (data_get($application, 'previews') as $preview)

@ -13,51 +13,57 @@
</a> </a>
<x-applications.links :application="$application" /> <x-applications.links :application="$application" />
<div class="flex-1"></div> <div class="flex-1"></div>
<x-applications.advanced :application="$application" /> @if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@if ($application->status !== 'exited') @else
<button title="With rolling update if possible" wire:click='deploy' <x-applications.advanced :application="$application" />
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> @if ($application->status !== 'exited')
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24" stroke-width="2" <button title="With rolling update if possible" wire:click='deploy'
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@if ($application->build_pack !== 'dockercompose')
<button title="Restart without rebuilding" wire:click='restart'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-width="2"> stroke-linejoin="round">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" /> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 4v5h-5" /> <path
</g> d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg> </svg>
Restart Redeploy
</button>
@if ($application->build_pack !== 'dockercompose')
<button title="Restart without rebuilding" wire:click='restart'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
@endif
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@else
<button wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button> </button>
@endif @endif
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@else
<button wire:click='deploy' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
@endif @endif
</div> </div>

@ -20,7 +20,7 @@
<a :class="activeTab === 'server' && 'text-white'" <a :class="activeTab === 'server' && 'text-white'"
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server @click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server
</a> </a>
@if ($application->build_pack !== 'static') @if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose')
<a :class="activeTab === 'storages' && 'text-white'" <a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages @click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a> </a>
@ -34,7 +34,7 @@
Deployments Deployments
</a> </a>
@endif @endif
@if ($application->build_pack !== 'static') @if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose')
<a :class="activeTab === 'health' && 'text-white'" <a :class="activeTab === 'health' && 'text-white'"
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Healthchecks @click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Healthchecks
</a> </a>
@ -42,10 +42,12 @@
<a :class="activeTab === 'rollback' && 'text-white'" <a :class="activeTab === 'rollback' && 'text-white'"
@click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback @click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback
</a> </a>
<a :class="activeTab === 'resource-limits' && 'text-white'" @if ($application->build_pack !== 'dockercompose')
@click.prevent="activeTab = 'resource-limits'; window.location.hash = 'resource-limits'" <a :class="activeTab === 'resource-limits' && 'text-white'"
href="#">Resource Limits @click.prevent="activeTab = 'resource-limits'; window.location.hash = 'resource-limits'"
</a> href="#">Resource Limits
</a>
@endif
<a :class="activeTab === 'danger' && 'text-white'" <a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone @click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
</a> </a>

@ -1,5 +1,5 @@
<div class="flex items-center gap-2 pb-4"> <div class="flex items-center gap-2 pb-4">
<h2>Logs</h2> <h2>Deployment Log</h2>
@if ($is_debug_enabled) @if ($is_debug_enabled)
<x-forms.button wire:click.prevent="show_debug">Hide Debug Logs</x-forms.button> <x-forms.button wire:click.prevent="show_debug">Hide Debug Logs</x-forms.button>
@else @else

@ -32,7 +32,6 @@
<option value="static">Static</option> <option value="static">Static</option>
<option value="dockerfile">Dockerfile</option> <option value="dockerfile">Dockerfile</option>
<option value="dockercompose">Docker Compose</option> <option value="dockercompose">Docker Compose</option>
{{-- <option value="dockerimage">Docker Image</option> --}}
</x-forms.select> </x-forms.select>
@if ($application->settings->is_static || $application->build_pack === 'static') @if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.select id="application.static_image" label="Static Image" required> <x-forms.select id="application.static_image" label="Static Image" required>
@ -122,9 +121,17 @@
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button> <x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
@if (count($parsedServices) > 0) @if (count($parsedServices) > 0)
@foreach (data_get($parsedServices, 'services') as $serviceName => $service) @foreach (data_get($parsedServices, 'services') as $serviceName => $service)
@if (!collect(DATABASE_DOCKER_IMAGES)->contains(data_get($service, 'image'))) @if (!isDatabaseImage(data_get($service, 'image')))
<x-forms.input helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " label="Domains for {{ str($serviceName)->headline() }}" <div class="flex items-end gap-2">
id="parsedServiceDomains.{{ $serviceName }}.domain"></x-forms.input> <x-forms.input
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
label="Domains for {{ str($serviceName)->headline() }}"
id="parsedServiceDomains.{{ $serviceName }}.domain"></x-forms.input>
@if (!data_get($parsedServiceDomains, "$serviceName.domain"))
<x-forms.button wire:click="generateDomain('{{ $serviceName }}')">Generate
Domain</x-forms.button>
@endif
</div>
@endif @endif
@endforeach @endforeach
@endif @endif

@ -1,6 +1,6 @@
<div x-init="$wire.getLogs"> <div x-init="$wire.getLogs">
<div class="flex gap-2"> <div class="flex gap-2">
<h2>Logs</h2> <h4>Container: {{$container}}</h4>
@if ($streamLogs) @if ($streamLogs)
<span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span> <span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span>
@endif @endif
@ -13,7 +13,7 @@
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required id="numberOfLines"></x-forms.input> <x-forms.input label="Only Show Number of Lines" placeholder="1000" required id="numberOfLines"></x-forms.input>
<x-forms.button type="submit">Refresh</x-forms.button> <x-forms.button type="submit">Refresh</x-forms.button>
</form> </form>
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full pt-4 mx-auto'"> <div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full py-4 mx-auto'">
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300" <div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'"> :class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg <button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg

@ -3,7 +3,14 @@
<h1>Logs</h1> <h1>Logs</h1>
<livewire:project.application.heading :application="$resource" /> <livewire:project.application.heading :application="$resource" />
<div class="pt-4"> <div class="pt-4">
<livewire:project.shared.get-logs :server="$server" :container="$container" /> @forelse ($containers as $container)
@if ($loop->first)
<h2 class="pb-4">Logs</h2>
@endif
<livewire:project.shared.get-logs :server="$server" :container="$container" />
@empty
<div>No containers are not running.</div>
@endforelse
</div> </div>
@elseif ($type === 'database') @elseif ($type === 'database')
<h1>Logs</h1> <h1>Logs</h1>