Merge pull request #1946 from coollabsio/next

v4.0.0-beta.252
This commit is contained in:
Andras Bacsai
2024-04-09 09:28:29 +02:00
committed by GitHub
36 changed files with 453 additions and 246 deletions

View File

@@ -13,6 +13,9 @@ class CheckProxy
if ($server->proxyType() === 'NONE') { if ($server->proxyType() === 'NONE') {
return false; return false;
} }
if (!$server->validateConnection()) {
throw new \Exception("Server Connection Error");
}
if (!$server->isProxyShouldRun()) { if (!$server->isProxyShouldRun()) {
if ($fromUI) { if ($fromUI) {
throw new \Exception("Proxy should not run. You selected the Custom Proxy."); throw new \Exception("Proxy should not run. You selected the Custom Proxy.");

View File

@@ -45,7 +45,6 @@ class UpdateCoolify
} }
$this->update(); $this->update();
} }
send_internal_notification("Instance updated from {$this->currentVersion} -> {$this->latestVersion}");
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray('InstanceAutoUpdateJob failed'); ray('InstanceAutoUpdateJob failed');
ray($e->getMessage()); ray($e->getMessage());
@@ -83,6 +82,7 @@ class UpdateCoolify
"bash /data/coolify/source/upgrade.sh $this->latestVersion" "bash /data/coolify/source/upgrade.sh $this->latestVersion"
], $this->server); ], $this->server);
} }
send_internal_notification("Instance updated from {$this->currentVersion} -> {$this->latestVersion}");
return; return;
} }
} }

View File

@@ -179,6 +179,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
if (!$this->server->isFunctional()) {
$this->application_deployment_queue->addLogEntry("Server is not functional.");
$this->fail("Server is not functional.");
return;
}
try { try {
// Generate custom host<->ip mapping // Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
@@ -646,21 +651,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
if ($this->application->docker_registry_image_name) { if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build"); $this->build_image_name = "{$this->application->docker_registry_image_name}:build";
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest"); $this->production_image_name = "{$this->application->docker_registry_image_name}:latest";
} else { } else {
$this->build_image_name = Str::lower("{$this->application->uuid}:build"); $this->build_image_name = "{$this->application->uuid}:build";
$this->production_image_name = Str::lower("{$this->application->uuid}:latest"); $this->production_image_name = "{$this->application->uuid}:latest";
} }
} else if ($this->application->build_pack === 'dockerimage') { } else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); $this->production_image_name = "{$this->dockerImage}:{$this->dockerImageTag}";
} else if ($this->pull_request_id !== 0) { } else if ($this->pull_request_id !== 0) {
if ($this->application->docker_registry_image_name) { if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build"); $this->build_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build";
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}"); $this->production_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}";
} else { } else {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); $this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build";
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); $this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}";
} }
} else { } else {
$this->dockerImageTag = str($this->commit)->substr(0, 128); $this->dockerImageTag = str($this->commit)->substr(0, 128);
@@ -668,11 +673,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->dockerImageTag = $this->application->docker_registry_image_tag; $this->dockerImageTag = $this->application->docker_registry_image_tag;
} }
if ($this->application->docker_registry_image_name) { if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build"); $this->build_image_name = "{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build";
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}"); $this->production_image_name = "{$this->application->docker_registry_image_name}:{$this->dockerImageTag}";
} else { } else {
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build"); $this->build_image_name = "{$this->application->uuid}:{$this->dockerImageTag}-build";
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}"); $this->production_image_name = "{$this->application->uuid}:{$this->dockerImageTag}";
} }
} }
} }
@@ -1001,11 +1006,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return $commands; return $commands;
} }
private function set_git_import_settings($git_clone_command)
{
return $this->application->setGitImportSettings($this->deployment_uuid, $git_clone_command);
}
private function cleanup_git() private function cleanup_git()
{ {
$this->execute_remote_command( $this->execute_remote_command(
@@ -1814,7 +1814,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void public function failed(Throwable $exception): void
{ {
$this->next(ApplicationDeploymentStatus::FAILED->value); $this->next(ApplicationDeploymentStatus::FAILED->value);
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr'); $this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
if (str($exception->getMessage())->isNotEmpty()) { if (str($exception->getMessage())->isNotEmpty()) {

View File

@@ -163,18 +163,16 @@ class General extends Component
} }
public function generateDomain(string $serviceName) public function generateDomain(string $serviceName)
{ {
$domain = $this->parsedServiceDomains[$serviceName]['domain'] ?? null;
if (!$domain) {
$uuid = new Cuid2(7); $uuid = new Cuid2(7);
$domain = generateFqdn($this->application->destination->server, $uuid); $domain = generateFqdn($this->application->destination->server, $uuid);
$this->parsedServiceDomains[$serviceName]['domain'] = $domain; $this->parsedServiceDomains[$serviceName]['domain'] = $domain;
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save(); $this->application->save();
} $this->dispatch('success', 'Domain generated.');
return $domain; return $domain;
} }
public function updatedApplicationBaseDirectory() { public function updatedApplicationBaseDirectory()
raY('asdf'); {
if ($this->application->build_pack === 'dockercompose') { if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile(); $this->loadComposeFile();
} }
@@ -206,30 +204,47 @@ class General extends Component
$fqdn = generateFqdn($server, $this->application->uuid); $fqdn = generateFqdn($server, $this->application->uuid);
$this->application->fqdn = $fqdn; $this->application->fqdn = $fqdn;
$this->application->save(); $this->application->save();
$this->updatedApplicationFqdn(); $this->dispatch('success', 'Wildcard domain generated.');
} }
} }
public function resetDefaultLabels($showToaster = true) public function resetDefaultLabels()
{ {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->ports_exposes = $this->application->ports_exposes; $this->ports_exposes = $this->application->ports_exposes;
$this->submit($showToaster);
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
} }
public function updatedApplicationFqdn() public function checkFqdns($showToaster = true)
{ {
if (data_get($this->application, 'fqdn')) {
$domains = str($this->application->fqdn)->trim()->explode(',');
if ($this->application->additional_servers->count() === 0) {
foreach ($domains as $domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
}
}
check_domain_usage(resource: $this->application);
$this->application->fqdn = $domains->implode(',');
}
}
public function submit($showToaster = true)
{
try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower(); return str($domain)->trim()->lower();
}); });
$this->application->fqdn = $this->application->fqdn->unique()->implode(','); $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->checkFqdns();
$this->application->save(); $this->application->save();
$this->resetDefaultLabels(false);
}
public function submit($showToaster = true)
{
try {
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
@@ -241,25 +256,14 @@ class General extends Component
} }
$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();
} }
if (data_get($this->application, 'build_pack') === 'dockerimage') { if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([ $this->validate([
'application.docker_registry_image_name' => 'required', 'application.docker_registry_image_name' => 'required',
]); ]);
} }
if (data_get($this->application, 'fqdn')) {
$domains = str($this->application->fqdn)->trim()->explode(',');
if ($this->application->additional_servers->count() === 0) {
foreach ($domains as $domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
}
}
check_fqdn_usage($this->application);
$this->application->fqdn = $domains->implode(',');
}
if (data_get($this->application, 'custom_docker_run_options')) { if (data_get($this->application, 'custom_docker_run_options')) {
$this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim(); $this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim();
} }
@@ -277,6 +281,15 @@ class General extends Component
} }
if ($this->application->build_pack === 'dockercompose') { if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
foreach ($this->parsedServiceDomains as $serviceName => $service) {
$domain = data_get($service, 'domain');
if ($domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
check_domain_usage(resource: $this->application);
}
}
if ($this->application->settings->is_raw_compose_deployment_enabled) { if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose(); $this->application->parseRawCompose();
} else { } else {

View File

@@ -10,7 +10,7 @@ use Livewire\Component;
class Index extends Component class Index extends Component
{ {
public Service $service; public ?Service $service = null;
public ?ServiceApplication $serviceApplication = null; public ?ServiceApplication $serviceApplication = null;
public ?ServiceDatabase $serviceDatabase = null; public ?ServiceDatabase $serviceDatabase = null;
public array $parameters; public array $parameters;
@@ -26,7 +26,10 @@ class Index extends Component
$this->services = collect([]); $this->services = collect([]);
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->first();
if (!$this->service) {
return redirect()->route('dashboard');
}
$service = $this->service->applications()->whereUuid($this->parameters['stack_service_uuid'])->first(); $service = $this->service->applications()->whereUuid($this->parameters['stack_service_uuid'])->first();
if ($service) { if ($service) {
$this->serviceApplication = $service; $this->serviceApplication = $service;

View File

@@ -65,12 +65,12 @@ class ServiceApplicationView extends Component
public function submit() public function submit()
{ {
try { try {
check_fqdn_usage($this->application); check_domain_usage(resource: $this->application);
$this->validate(); $this->validate();
$this->application->save(); $this->application->save();
updateCompose($this->application); updateCompose($this->application);
if (str($this->application->fqdn)->contains(',')) { if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.'); $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
} else { } else {
$this->dispatch('success', 'Service saved.'); $this->dispatch('success', 'Service saved.');
} }

View File

@@ -59,6 +59,9 @@ class Configuration extends Component
public function submit() public function submit()
{ {
try {
$error_show = false;
$this->server = Server::findOrFail(0);
$this->resetErrorBag(); $this->resetErrorBag();
if ($this->settings->public_port_min > $this->settings->public_port_max) { if ($this->settings->public_port_min > $this->settings->public_port_max) {
$this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.'); $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.');
@@ -66,6 +69,13 @@ class Configuration extends Component
} }
$this->validate(); $this->validate();
if ($this->settings->is_dns_validation_enabled) {
if (!validate_dns_entry($this->settings->fqdn, $this->server)) {
$this->dispatch('error', "Validating DNS ({$this->settings->fqdn}) failed.<br><br>Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$error_show = true;
}
}
check_domain_usage(domain: $this->settings->fqdn);
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim(); $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim();
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) { $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) {
return str($dns)->trim()->lower(); return str($dns)->trim()->lower();
@@ -74,8 +84,12 @@ class Configuration extends Component
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(','); $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(',');
$this->settings->save(); $this->settings->save();
$this->server = Server::findOrFail(0);
$this->server->setupDynamicProxyConfiguration(); $this->server->setupDynamicProxyConfiguration();
if (!$error_show) {
$this->dispatch('success', 'Instance settings updated successfully!'); $this->dispatch('success', 'Instance settings updated successfully!');
} }
} catch (\Exception $e) {
return handleError($e, $this);
}
}
} }

View File

@@ -32,10 +32,10 @@ class Upgrade extends Component
public function upgrade() public function upgrade()
{ {
try { try {
$this->rateLimit(1, 30);
if ($this->showProgress) { if ($this->showProgress) {
return; return;
} }
$this->rateLimit(1, 30);
$this->showProgress = true; $this->showProgress = true;
UpdateCoolify::run(force: true, async: true); UpdateCoolify::run(force: true, async: true);
$this->dispatch('success', "Updating Coolify to {$this->latestVersion} version..."); $this->dispatch('success', "Updating Coolify to {$this->latestVersion} version...");

View File

@@ -598,21 +598,30 @@ class Application extends BaseModel
} else { } else {
$github_access_token = generate_github_installation_token($this->source); $github_access_token = generate_github_installation_token($this->source);
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}")); $git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git"; $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
} else { } else {
$commands->push("{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}"); $git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
} }
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name"; $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
$git_checkout_command = $this->buildGitCheckoutCommand($pr_branch_name);
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name")); $commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command"));
} else { } else {
$commands->push("cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name"); $commands->push("cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command");
} }
} }
ray($commands);
return [ return [
'commands' => $commands->implode(' && '), 'commands' => $commands->implode(' && '),
'branch' => $branch, 'branch' => $branch,
@@ -654,7 +663,7 @@ class Application extends BaseModel
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'github') { } else if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name"; $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) { if ($exec_in_docker) {
@@ -662,14 +671,14 @@ class Application extends BaseModel
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'bitbucket') { } else if ($git_type === 'bitbucket') {
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
} }
} }
@@ -697,7 +706,7 @@ class Application extends BaseModel
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'github') { } else if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name"; $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) { if ($exec_in_docker) {
@@ -705,14 +714,14 @@ class Application extends BaseModel
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'bitbucket') { } else if ($git_type === 'bitbucket') {
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
} }
} }
@@ -904,6 +913,16 @@ class Application extends BaseModel
: explode(',', $this->fqdn), : explode(',', $this->fqdn),
); );
} }
protected function buildGitCheckoutCommand($target): string
{
$command = "git checkout $target";
if ($this->settings->is_git_submodules_enabled) {
$command .= " && git submodule update --init --recursive";
}
return $command;
}
public function watchPaths(): Attribute public function watchPaths(): Attribute
{ {
return Attribute::make( return Attribute::make(

View File

@@ -690,7 +690,13 @@ $schema://$host {
} }
public function isFunctional() public function isFunctional()
{ {
return $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled; $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled;
['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this);
if (!$isFunctional) {
Storage::disk('ssh-keys')->delete($private_key_filename);
Storage::disk('ssh-mux')->delete($mux_filename);
}
return $isFunctional;
} }
public function isLogDrainEnabled() public function isLogDrainEnabled()
{ {

View File

@@ -2,6 +2,7 @@
namespace App\Notifications\Server; namespace App\Notifications\Server;
use App\Jobs\ContainerStatusJob;
use App\Models\Server; use App\Models\Server;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
@@ -21,6 +22,7 @@ class Revived extends Notification implements ShouldQueue
if ($this->server->unreachable_notification_sent === false) { if ($this->server->unreachable_notification_sent === false) {
return; return;
} }
dispatch(new ContainerStatusJob($server));
} }
public function via(object $notifiable): array public function via(object $notifiable): array

View File

@@ -37,7 +37,7 @@ const SPECIFIC_SERVICES = [
// Based on /etc/os-release // Based on /etc/os-release
const SUPPORTED_OS = [ const SUPPORTED_OS = [
'ubuntu debian raspbian', 'ubuntu debian raspbian',
'centos fedora rhel ol rocky', 'centos fedora rhel ol rocky amzn',
'sles opensuse-leap opensuse-tumbleweed' 'sles opensuse-leap opensuse-tumbleweed'
]; ];

View File

@@ -55,17 +55,30 @@ function remote_process(
), ),
])(); ])();
} }
function server_ssh_configuration(Server $server)
{
$uuid = data_get($server, 'uuid');
if (is_null($uuid)) {
throw new \Exception("Server does not have a uuid");
}
$private_key_filename = "id.root@{$server->uuid}";
$location = '/var/www/html/storage/app/ssh/keys/' . $private_key_filename;
$mux_filename = '/var/www/html/storage/app/ssh/mux/' . $server->muxFilename();
return [
'location' => $location,
'mux_filename' => $mux_filename,
'private_key_filename' => $private_key_filename
];
}
function savePrivateKeyToFs(Server $server) function savePrivateKeyToFs(Server $server)
{ {
if (data_get($server, 'privateKey.private_key') === null) { if (data_get($server, 'privateKey.private_key') === null) {
throw new \Exception("Server {$server->name} does not have a private key"); throw new \Exception("Server {$server->name} does not have a private key");
} }
$sshKeyFileLocation = "id.root@{$server->uuid}"; ['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server);
Storage::disk('ssh-keys')->makeDirectory('.'); Storage::disk('ssh-keys')->makeDirectory('.');
Storage::disk('ssh-mux')->makeDirectory('.'); Storage::disk('ssh-mux')->makeDirectory('.');
Storage::disk('ssh-keys')->put($sshKeyFileLocation, $server->privateKey->private_key); Storage::disk('ssh-keys')->put($private_key_filename, $server->privateKey->private_key);
$location = '/var/www/html/storage/app/ssh/keys/' . $sshKeyFileLocation;
return $location; return $location;
} }
@@ -223,6 +236,13 @@ function remove_iip($text)
$text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text); $text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text); return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
} }
function remove_mux_and_private_key(Server $server)
{
$muxFilename = $server->muxFilename();
$privateKeyLocation = savePrivateKeyToFs($server);
Storage::disk('ssh-mux')->delete($muxFilename);
Storage::disk('ssh-keys')->delete($privateKeyLocation);
}
function refresh_server_connection(?PrivateKey $private_key = null) function refresh_server_connection(?PrivateKey $private_key = null)
{ {
if (is_null($private_key)) { if (is_null($private_key)) {

View File

@@ -88,33 +88,106 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
$dockerCompose = Yaml::parse($dockerComposeRaw); $dockerCompose = Yaml::parse($dockerComposeRaw);
// Switch Image // Switch Image
$image = data_get($resource, 'image'); $updatedImage = data_get_str($resource, 'image');
data_set($dockerCompose, "services.{$name}.image", $image); $currentImage = data_get_str($dockerCompose, "services.{$name}.image");
if ($currentImage !== $updatedImage) {
data_set($dockerCompose, "services.{$name}.image", $updatedImage->value());
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2); $dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw; $resource->service->docker_compose_raw = $dockerComposeRaw;
$resource->service->save(); $resource->service->save();
$resource->image = $updatedImage;
if ($resource->fqdn && !str($resource->fqdn)->contains(',')) { $resource->save();
// Update FQDN }
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper(); if ($resource->fqdn) {
$resourceFqdns = str($resource->fqdn)->explode(',');
if ($resourceFqdns->count() === 1) {
$resourceFqdns = $resourceFqdns->first();
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$fqdn = Url::fromString($resource->fqdn); $fqdn = Url::fromString($resourceFqdns);
$port = $fqdn->getPort();
$fqdn = $fqdn->getScheme() . '://' . $fqdn->getHost(); $fqdn = $fqdn->getScheme() . '://' . $fqdn->getHost();
if ($generatedEnv) { if ($generatedEnv) {
$generatedEnv->value = $fqdn; $generatedEnv->value = $fqdn;
$generatedEnv->save(); $generatedEnv->save();
} }
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper(); if ($port) {
$variableName = $variableName . "_$port";
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($resource->fqdn); if ($generatedEnv) {
$generatedEnv->value = $fqdn . ':' . $port;
$generatedEnv->save();
}
}
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($fqdn);
$port = $url->getPort();
$url = $url->getHost(); $url = $url->getHost();
if ($generatedEnv) { if ($generatedEnv) {
$url = Str::of($resource->fqdn)->after('://'); $url = Str::of($fqdn)->after('://');
$generatedEnv->value = $url;
$generatedEnv->save();
}
if ($port) {
$variableName = $variableName . "_$port";
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv) {
$generatedEnv->value = $url . ':' . $port;
$generatedEnv->save();
}
}
} else if ($resourceFqdns->count() > 1) {
foreach ($resourceFqdns as $fqdn) {
$host = Url::fromString($fqdn);
$port = $host->getPort();
$url = $host->getHost();
$host = $host->getScheme() . '://' . $host->getHost();
if ($port) {
$port_envs = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'like', "SERVICE_FQDN_%_$port")->get();
foreach ($port_envs as $port_env) {
$service_fqdn = str($port_env->key)->beforeLast('_')->after('SERVICE_FQDN_');
$env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_FQDN_' . $service_fqdn)->first();
if ($env) {
$env->value = $host;
$env->save();
}
$port_env->value = $host . ':' . $port;
$port_env->save();
}
$port_envs_url = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'like', "SERVICE_URL_%_$port")->get();
foreach ($port_envs_url as $port_env_url) {
$service_url = str($port_env_url->key)->beforeLast('_')->after('SERVICE_URL_');
$env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_URL_' . $service_url)->first();
if ($env) {
$env->value = $url;
$env->save();
}
$port_env_url->value = $url . ':' . $port;
$port_env_url->save();
}
} else {
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$fqdn = Url::fromString($fqdn);
$fqdn = $fqdn->getScheme() . '://' . $fqdn->getHost();
if ($generatedEnv) {
$generatedEnv->value = $fqdn;
$generatedEnv->save();
}
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($fqdn);
$url = $url->getHost();
if ($generatedEnv) {
$url = Str::of($fqdn)->after('://');
$generatedEnv->value = $url; $generatedEnv->value = $url;
$generatedEnv->save(); $generatedEnv->save();
} }
} }
}
}
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e);
} }

View File

@@ -443,7 +443,7 @@ function sslip(Server $server)
function getServiceTemplates() function getServiceTemplates()
{ {
if (!isDev()) { if (isDev()) {
$services = File::get(base_path('templates/service-templates.json')); $services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys(); $services = collect(json_decode($services))->sortKeys();
} else { } else {
@@ -1168,6 +1168,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// }); // });
// ray($withoutServiceEnvs); // ray($withoutServiceEnvs);
// data_set($service, 'environment', $withoutServiceEnvs->toArray()); // data_set($service, 'environment', $withoutServiceEnvs->toArray());
updateCompose($savedService);
return $service; return $service;
}); });
$finalServices = [ $finalServices = [
@@ -1856,13 +1857,26 @@ function ip_match($ip, $cidrs, &$match = null)
} }
return false; return false;
} }
function check_fqdn_usage(ServiceApplication|Application $own_resource) function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
{ {
$domains = collect($own_resource->fqdns)->map(function ($domain) { if ($resource) {
if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') {
$domains = data_get(json_decode($resource->docker_compose_domains, true), "*.domain");
ray($domains);
$domains = collect($domains);
} else {
$domains = collect($resource->fqdns);
}
} else if ($domain) {
$domains = collect($domain);
} else {
throw new \RuntimeException("No resource or FQDN provided.");
}
$domains = $domains->map(function ($domain) {
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
} }
return str($domain)->replace('http://', '')->replace('https://', ''); return str($domain);
}); });
$apps = Application::all(); $apps = Application::all();
foreach ($apps as $app) { foreach ($apps as $app) {
@@ -1871,10 +1885,15 @@ function check_fqdn_usage(ServiceApplication|Application $own_resource)
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
} }
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value(); $naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) { if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) { if (data_get($resource, 'uuid')) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource:<br> {$app->name}."); ray($resource->uuid, $app->uuid);
if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
}
} else if ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
} }
} }
} }
@@ -1886,12 +1905,29 @@ function check_fqdn_usage(ServiceApplication|Application $own_resource)
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
} }
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value(); $naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) { if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) { if (data_get($resource, 'uuid')) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource."); if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
}
} else if ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
} }
} }
} }
} }
if ($resource) {
$settings = InstanceSettings::get();
if (data_get($settings, 'fqdn')) {
$domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance.");
}
}
}
} }

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.251', 'release' => '4.0.0-beta.252',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.251'; return '4.0.0-beta.252';

34
package-lock.json generated
View File

@@ -6,8 +6,8 @@
"": { "": {
"dependencies": { "dependencies": {
"@tailwindcss/forms": "0.5.7", "@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.12",
"alpinejs": "3.13.7", "alpinejs": "3.13.8",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
}, },
@@ -19,8 +19,8 @@
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.38", "postcss": "8.4.38",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.3",
"vite": "4.5.2", "vite": "4.5.3",
"vue": "3.4.21" "vue": "3.4.21"
} }
}, },
@@ -496,9 +496,9 @@
} }
}, },
"node_modules/@tailwindcss/typography": { "node_modules/@tailwindcss/typography": {
"version": "0.5.10", "version": "0.5.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.12.tgz",
"integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", "integrity": "sha512-CNwpBpconcP7ppxmuq3qvaCxiRWnbhANpY/ruH4L5qs2GCiVDJXde/pjj2HWPV1+Q4G9+V/etrwUYopdcjAlyg==",
"dependencies": { "dependencies": {
"lodash.castarray": "^4.4.0", "lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6", "lodash.isplainobject": "^4.0.6",
@@ -683,9 +683,9 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
}, },
"node_modules/alpinejs": { "node_modules/alpinejs": {
"version": "3.13.7", "version": "3.13.8",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.7.tgz", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.8.tgz",
"integrity": "sha512-rcTyjTANbsePq1hb7eSekt3qjI94HLGeO6JaRjCssCVbIIc+qBrc7pO5S/+2JB6oojIibjM6FA+xRI3zhGPZIg==", "integrity": "sha512-XolbBJryCndomtaHd/KHQjQeD/L72FJxy/YhLLFD4Lr7zzGcpcbg+UgXteMR2pYg1KhRUr6V4O3GfN1zJAmRWw==",
"dependencies": { "dependencies": {
"@vue/reactivity": "~3.1.1" "@vue/reactivity": "~3.1.1"
} }
@@ -1900,9 +1900,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.1", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
"integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@@ -1912,7 +1912,7 @@
"fast-glob": "^3.3.0", "fast-glob": "^3.3.0",
"glob-parent": "^6.0.2", "glob-parent": "^6.0.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"jiti": "^1.19.1", "jiti": "^1.21.0",
"lilconfig": "^2.1.0", "lilconfig": "^2.1.0",
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
@@ -2020,9 +2020,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.2", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",

View File

@@ -13,14 +13,14 @@
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.38", "postcss": "8.4.38",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.3",
"vite": "4.5.2", "vite": "4.5.3",
"vue": "3.4.21" "vue": "3.4.21"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/forms": "0.5.7", "@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.12",
"alpinejs": "3.13.7", "alpinejs": "3.13.8",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
} }

View File

@@ -66,7 +66,7 @@
</div> </div>
@endif @endif
@foreach ($enabled_oauth_providers as $provider_setting) @foreach ($enabled_oauth_providers as $provider_setting)
<x-forms.button type="button" <x-forms.button class="w-full" type="button"
onclick="document.location.href='/auth/{{ $provider_setting->provider }}/redirect'"> onclick="document.location.href='/auth/{{ $provider_setting->provider }}/redirect'">
{{ __("auth.login.$provider_setting->provider") }} {{ __("auth.login.$provider_setting->provider") }}
</x-forms.button> </x-forms.button>

View File

@@ -1,4 +1,4 @@
<nav class="flex flex-col flex-1 pl-2 bg-white border-r dark:border-coolgray-200 dark:bg-base" x-data="{ <nav class="flex flex-col flex-1 bg-white border-r dark:border-coolgray-200 dark:bg-base" x-data="{
switchWidth() { switchWidth() {
if (this.full === 'full') { if (this.full === 'full') {
localStorage.removeItem('pageWidth'); localStorage.removeItem('pageWidth');

View File

@@ -19,22 +19,17 @@
@if ($projects->count() > 0) @if ($projects->count() > 0)
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2"> <div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
@foreach ($projects as $project) @foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group"> <div class="gap-2 border border-transparent cursor-pointer box group"
@if (data_get($project, 'environments')->count() === 1) @if (data_get($project, 'environments')->count() === 1) onclick="gotoProject('{{ data_get($project, 'uuid') }}', '{{ data_get($project, 'environments.0.name', 'production') }}')"
<a class="flex flex-col justify-center flex-1 mx-6"
href="{{ route('project.resource.index', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<div class="box-title">{{ $project->name }}</div>
<div class="box-description"> {{ $project->description }}</div>
</a>
@else @else
<a class="flex flex-col justify-center flex-1 mx-6" onclick="window.location.href = '{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}'" @endif>
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}"> <div class="flex flex-col justify-center flex-1 mx-6">
<div class="box-title">{{ $project->name }}</div> <div class="box-title">{{ $project->name }}</div>
<div class="box-description"> <div class="box-description">
{{ $project->description }}</div> {{ $project->description }}</div>
</a> </div>
@endif <span
<div class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal"> class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal">
<a class="hover:underline" <a class="hover:underline"
href="{{ route('project.resource.create', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}"> href="{{ route('project.resource.create', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<span class="p-2 font-bold">+ <span class="p-2 font-bold">+
@@ -44,7 +39,7 @@
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
Settings Settings
</a> </a>
</div> </span>
</div> </div>
@endforeach @endforeach
</div> </div>

View File

@@ -81,15 +81,14 @@
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" /> d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
</g> </g>
</svg></button> </svg></button>
<div id="logs" class="flex flex-col"> <div id="logs" class="flex flex-col font-mono">
@if (decode_remote_command_output($application_deployment_queue)->count() > 0) @if (decode_remote_command_output($application_deployment_queue)->count() > 0)
@foreach (decode_remote_command_output($application_deployment_queue) as $line) @foreach (decode_remote_command_output($application_deployment_queue) as $line)
<div @class([ <span @class([
'font-mono', 'dark:text-warning' => $line['hidden'],
'dark:text-warning whitespace-pre-line' => $line['hidden'], 'text-red-500 font-bold' => $line['type'] == 'stderr',
'text-red-500 font-bold whitespace-pre-line' => $line['type'] == 'stderr',
])>[{{ $line['timestamp'] }}] @if ($line['hidden']) ])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT: <br>COMMAND: {{ $line['command'] }}<br>OUTPUT :
@endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://')) @endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://'))
@php @php
$line['output'] = preg_replace( $line['output'] = preg_replace(
@@ -101,7 +100,7 @@
@else @else
{{ $line['output'] }} {{ $line['output'] }}
@endif @endif
</div> </span>
@endforeach @endforeach
@else @else
<span class="font-mono text-neutral-400">No logs yet.</span> <span class="font-mono text-neutral-400">No logs yet.</span>

View File

@@ -59,10 +59,8 @@
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. " 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() }}" label="Domains for {{ str($serviceName)->headline() }}"
id="parsedServiceDomains.{{ $serviceName }}.domain"></x-forms.input> id="parsedServiceDomains.{{ $serviceName }}.domain"></x-forms.input>
@if (!data_get($parsedServiceDomains, "$serviceName.domain"))
<x-forms.button wire:click="generateDomain('{{ $serviceName }}')">Generate <x-forms.button wire:click="generateDomain('{{ $serviceName }}')">Generate
Domain</x-forms.button> Domain</x-forms.button>
@endif
</div> </div>
@endif @endif
@endforeach @endforeach

View File

@@ -25,14 +25,16 @@
</div> </div>
<div class="pb-4">Code source of your application.</div> <div class="pb-4">Code source of your application.</div>
<div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input placeholder="coollabsio/coolify-example" id="application.git_repository" <x-forms.input placeholder="coollabsio/coolify-example" id="application.git_repository"
label="Repository" /> label="Repository" />
<x-forms.input placeholder="main" id="application.git_branch" label="Branch" /> <x-forms.input placeholder="main" id="application.git_branch" label="Branch" />
</div> </div>
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<x-forms.input placeholder="HEAD" id="application.git_commit_sha" placeholder="HEAD" label="Commit SHA" /> <x-forms.input placeholder="HEAD" id="application.git_commit_sha" placeholder="HEAD"
label="Commit SHA" />
</div>
</div> </div>
@isset($application->private_key_id) @isset($application->private_key_id)
<h3 class="pt-4">Deploy Key</h3> <h3 class="pt-4">Deploy Key</h3>

View File

@@ -27,7 +27,7 @@
helper="More info <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper> helper="More info <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse ($project->environment_variables->sort()->sortBy('real_value') as $env) @forelse ($project->environment_variables->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" type="project" /> :env="$env" type="project" />
@empty @empty

View File

@@ -52,7 +52,7 @@
helper="More info <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper> helper="More info <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse ($environment->environment_variables->sort()->sortBy('real_value') as $env) @forelse ($environment->environment_variables->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" type="environment" /> :env="$env" type="environment" />
@empty @empty

View File

@@ -1,27 +1,17 @@
<div tabindex="0" x-data="{ open: false }" <div class="p-4 transition border rounded cursor-pointer border-coolgray-200">
class="transition border rounded cursor-pointer collapse collapse-arrow border-coolgray-200" <div class="flex flex-col justify-center pb-4 text-sm select-text">
:class="open ? 'collapse-open' : 'collapse-close'"> <h2>{{ $service->name }}</h2>
<div class="flex flex-col justify-center text-sm select-text collapse-title" x-on:click="open = !open">
<div>{{ $workdir }}{{ $fs_path }} -> {{ $fileStorage->mount_path }}</div> <div>{{ $workdir }}{{ $fs_path }} -> {{ $fileStorage->mount_path }}</div>
</div> </div>
<div class="collapse-content"> <div>
<form wire:submit='submit' class="flex flex-col gap-2"> <form wire:submit='submit' class="flex flex-col gap-2">
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox> <x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox>
</div> </div>
{{-- @if ($fileStorage->is_directory)
<x-forms.input readonly label="Directory on Filesystem (save files here)" id="fs_path"></x-forms.input>
@else --}}
{{-- <div class="flex gap-2">
<x-forms.input readonly label="File in Docker Compose file" id="fileStorage.fs_path"></x-forms.input>
<x-forms.input readonly label="File on Filesystem (save files here)" id="fs_path"></x-forms.input>
</div>
<x-forms.input readonly label="Mount (in container)" id="fileStorage.mount_path"></x-forms.input> --}}
@if (!$fileStorage->is_directory) @if (!$fileStorage->is_directory)
<x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea> <x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
@endif @endif
{{-- @endif --}}
</form> </form>
</div> </div>
</div> </div>

View File

@@ -32,7 +32,7 @@
@endif @endif
@endif @endif
@else @else
@if ($resource->persistentStorages()->get()->count() > 0 || $resource->fileStorages()->get()->count() > 0) @if ($resource->persistentStorages()->get()->count() > 0)
<h3 class="pt-4">{{ Str::headline($resource->name) }} </h3> <h3 class="pt-4">{{ Str::headline($resource->name) }} </h3>
@endif @endif
@if ($resource->persistentStorages()->get()->count() > 0) @if ($resource->persistentStorages()->get()->count() > 0)

View File

@@ -16,7 +16,7 @@
@endif @endif
</div> </div>
@if ($view === 'normal') @if ($view === 'normal')
@forelse ($resource->environment_variables->sort()->sortBy('real_value') as $env) @forelse ($resource->environment_variables->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" :type="$resource->type()" /> :env="$env" :type="$resource->type()" />
@empty @empty
@@ -27,7 +27,7 @@
<h3>Preview Deployments</h3> <h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div> <div>Environment (secrets) variables for Preview Deployments.</div>
</div> </div>
@foreach ($resource->environment_variables_preview->sort()->sortBy('real_value') as $env) @foreach ($resource->environment_variables_preview->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" :type="$resource->type()" /> :env="$env" :type="$resource->type()" />
@endforeach @endforeach

View File

@@ -12,7 +12,7 @@
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse ($team->environment_variables->sort()->sortBy('real_value') as $env) @forelse ($team->environment_variables->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" type="team" /> :env="$env" type="team" />
@empty @empty

View File

@@ -6,7 +6,7 @@ set -e # Exit immediately if a command exits with a non-zero status
#set -u # Treat unset variables as an error and exit #set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
VERSION="1.2.3" VERSION="1.3.0"
DOCKER_VERSION="24.0" DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify" CDN="https://cdn.coollabs.io/coolify"
@@ -18,6 +18,11 @@ else
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
fi fi
# Install xargs on Amazon Linux 2023 - lol
if [ "$OS_TYPE" = 'amzn' ]; then
dnf install -y findutils >/dev/null 2>&1
fi
LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | sed -n '2p' | xargs | awk '{print $2}' | tr -d ',') LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | sed -n '2p' | xargs | awk '{print $2}' | tr -d ',')
DATE=$(date +"%Y%m%d-%H%M%S") DATE=$(date +"%Y%m%d-%H%M%S")
@@ -27,7 +32,7 @@ if [ $EUID != 0 ]; then
fi fi
case "$OS_TYPE" in case "$OS_TYPE" in
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux) ;; arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn) ;;
*) *)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
exit exit
@@ -64,8 +69,12 @@ ubuntu | debian | raspbian)
apt update -y >/dev/null 2>&1 apt update -y >/dev/null 2>&1
apt install -y curl wget git jq >/dev/null 2>&1 apt install -y curl wget git jq >/dev/null 2>&1
;; ;;
centos | fedora | rhel | ol | rocky | almalinux) centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq >/dev/null 2>&1
else
dnf install -y curl wget git jq >/dev/null 2>&1 dnf install -y curl wget git jq >/dev/null 2>&1
fi
;; ;;
sles | opensuse-leap | opensuse-tumbleweed) sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null 2>&1 zypper refresh >/dev/null 2>&1
@@ -133,6 +142,7 @@ if [ -x "$(command -v snap)" ]; then
fi fi
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
# Almalinux
if [ "$OS_TYPE" == 'almalinux' ]; then if [ "$OS_TYPE" == 'almalinux' ]; then
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
@@ -146,6 +156,7 @@ if ! [ -x "$(command -v docker)" ]; then
set +e set +e
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
echo "Docker is not installed. Installing Docker." echo "Docker is not installed. Installing Docker."
# Arch Linux
if [ "$OS_TYPE" = "arch" ]; then if [ "$OS_TYPE" = "arch" ]; then
pacman -Sy docker docker-compose --noconfirm pacman -Sy docker docker-compose --noconfirm
systemctl enable docker.service systemctl enable docker.service
@@ -157,6 +168,24 @@ if ! [ -x "$(command -v docker)" ]; then
exit exit
fi fi
else else
# Amazon Linux 2023
if [ "$OS_TYPE" = "amzn" ]; then
dnf install docker -y
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
systemctl start docker
systemctl enable docker
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Failed to install Docker with pacman. Try to install it manually."
echo "Please visit https://wiki.archlinux.org/title/docker for more information."
exit
fi
else
# Automated Docker installation
curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
if [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully." echo "Docker installed successfully."
@@ -174,6 +203,7 @@ if ! [ -x "$(command -v docker)" ]; then
fi fi
fi fi
fi fi
fi
set -e set -e
fi fi
fi fi

View File

@@ -15,3 +15,8 @@ services:
environment: environment:
- SERVICE_FQDN_SPDF_8080 - SERVICE_FQDN_SPDF_8080
- DOCKER_ENABLE_SECURITY=false - DOCKER_ENABLE_SECURITY=false
healthcheck:
test: 'curl --fail -I http://localhost:8080 || exit 1'
interval: 5s
timeout: 20s
retries: 10

View File

@@ -15,7 +15,7 @@ services:
condition: service_healthy condition: service_healthy
environment: environment:
- SERVICE_FQDN_SUPABASE_8000 - SERVICE_FQDN_SUPABASE_8000
- JWT_SERCET=${SERVICE_PASSWORD_JWT} - JWT_SECRET=${SERVICE_PASSWORD_JWT}
- KONG_DATABASE=off - KONG_DATABASE=off
- KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml - KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml
# https://github.com/supabase/cli/issues/14 # https://github.com/supabase/cli/issues/14

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.251" "version": "4.0.0-beta.252"
} }
} }
} }