Merge pull request #1474 from coollabsio/next

v4.0.0-beta.145
This commit is contained in:
Andras Bacsai 2023-11-22 08:45:00 +01:00 committed by GitHub
commit ec98afe707
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2449 additions and 377 deletions

View File

@ -23,7 +23,7 @@ public function handle(StandaloneMariadb $database)
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@ -104,7 +104,7 @@ public function handle(StandaloneMariadb $database)
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}

View File

@ -25,7 +25,7 @@ public function handle(StandaloneMongodb $database)
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@ -120,7 +120,7 @@ public function handle(StandaloneMongodb $database)
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}

View File

@ -23,7 +23,7 @@ public function handle(StandaloneMysql $database)
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@ -104,7 +104,7 @@ public function handle(StandaloneMysql $database)
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}

View File

@ -23,7 +23,7 @@ public function handle(StandalonePostgresql $database)
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/"
];
@ -130,7 +130,7 @@ public function handle(StandalonePostgresql $database)
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}

View File

@ -26,7 +26,7 @@ public function handle(StandaloneRedis $database)
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@ -114,7 +114,7 @@ public function handle(StandaloneRedis $database)
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}

View File

@ -11,6 +11,11 @@ class InstallDocker
use AsAction;
public function handle(Server $server)
{
$supported_os_type = $server->validateOS();
if (!$supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
}
ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS: ' . $supported_os_type);
$dockerVersion = '24.0';
$config = base64_encode('{
"log-driver": "json-file",
@ -27,36 +32,49 @@ public function handle(Server $server)
'server_id' => $server->id,
]);
}
$command = collect([]);
if (isDev() && $server->id === 0) {
$command = [
"echo '####### Installing Prerequisites...'",
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"sleep 1",
"echo '####### Installing/updating Docker Engine...'",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
"echo 'Installing Docker Engine...'",
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"sleep 4",
"echo '####### Restarting Docker Engine...'",
"echo 'Restarting Docker Engine...'",
"ls -l /tmp"
];
]);
} else {
$command = [
"echo '####### Installing Prerequisites...'",
"command -v jq >/dev/null || apt-get update",
"command -v jq >/dev/null || apt install -y jq",
"echo '####### Installing/updating Docker Engine...'",
if ($supported_os_type === 'debian') {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || apt-get update",
"command -v jq >/dev/null || apt install -y jq",
]);
} else if ($supported_os_type === 'rhel') {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || dnf install -y jq",
]);
} else {
throw new \Exception('Unsupported OS');
}
$command = $command->merge([
"echo 'Installing Docker Engine...'",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
"cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify",
"cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json",
"echo '####### Restarting Docker Engine...'",
"echo 'Restarting Docker Engine...'",
"systemctl enable docker >/dev/null 2>&1 || true",
"systemctl restart docker",
"echo '####### Creating default Docker network (coolify)...'",
"echo 'Creating default Docker network (coolify)...'",
"docker network create --attachable coolify >/dev/null 2>&1 || true",
"echo '####### Done!'"
];
"echo 'Done!'"
]);
return remote_process($command, $server);
}
return remote_process($command, $server);
}
}

View File

@ -14,13 +14,13 @@ public function handle(Service $service)
$network = $service->destination->network;
$service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo '####### Creating Docker network.'";
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'";
$commands[] = "echo 'Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo 'Pulling images.'";
$commands[] = "docker compose pull";
$commands[] = "echo '####### Starting containers.'";
$commands[] = "echo 'Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy || true";
$compose = data_get($service,'docker_compose',[]);

View File

@ -6,6 +6,7 @@
use App\Models\User;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use RuntimeException;
use Sentry\Laravel\Integration;
use Sentry\State\Scope;
use Throwable;
@ -55,7 +56,9 @@ public function register(): void
{
$this->reportable(function (Throwable $e) {
if (isDev()) {
ray($e);
// return;
}
if ($e instanceof RuntimeException) {
return;
}
$this->settings = InstanceSettings::get();
@ -74,6 +77,7 @@ function (Scope $scope) {
);
}
);
ray('reporting to sentry');
Integration::captureUnhandledException($e);
});
}

View File

@ -10,23 +10,6 @@ class ApplicationController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function configuration()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
return view('project.application.configuration', ['application' => $application]);
}
public function deployments()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();

View File

@ -188,7 +188,6 @@ public function saveServer()
public function validateServer()
{
try {
$customErrorMessage = "Server is not reachable:";
config()->set('coolify.mux_enabled', false);
instant_remote_process(['uptime'], $this->createdServer, true);
@ -198,7 +197,7 @@ public function validateServer()
]);
} catch (\Throwable $e) {
$this->serverReachable = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
return handleError(error: $e, livewire: $this);
}
try {
@ -206,7 +205,7 @@ public function validateServer()
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) {
$this->currentState = 'install-docker';
throw new \Exception('Docker version is not supported or not installed.');
throw new \Exception('Docker not found or old version is installed.');
}
$this->createdServer->settings()->update([
'is_usable' => true,
@ -214,14 +213,20 @@ public function validateServer()
$this->getProxyType();
} catch (\Throwable $e) {
// $this->dockerInstallationStarted = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
return handleError(error: $e, livewire: $this);
}
}
public function installDocker()
{
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->createdServer);
$this->emit('newMonitorActivity', $activity->id);
try {
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->createdServer);
$this->emit('installDocker');
$this->emit('newMonitorActivity', $activity->id);
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this);
}
}
public function dockerInstalledOrSkipped()
{

View File

@ -3,7 +3,6 @@
namespace App\Http\Livewire;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server;
use Livewire\Component;

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use Livewire\Component;
class Advanced extends Component
{
public Application $application;
protected $rules = [
'application.settings.is_git_submodules_enabled' => 'boolean|required',
'application.settings.is_git_lfs_enabled' => 'boolean|required',
'application.settings.is_preview_deployments_enabled' => 'boolean|required',
'application.settings.is_auto_deploy_enabled' => 'boolean|required',
'application.settings.is_force_https_enabled' => 'boolean|required',
'application.settings.is_log_drain_enabled' => 'boolean|required',
'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required',
'application.settings.gpu_count' => 'string|required',
'application.settings.gpu_device_ids' => 'string|required',
'application.settings.gpu_options' => 'string|required',
];
public function instantSave()
{
if ($this->application->settings->is_log_drain_enabled) {
if (!$this->application->destination->server->isLogDrainEnabled()) {
$this->application->settings->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on this server.');
return;
}
}
if ($this->application->settings->is_force_https_enabled) {
$this->emit('resetDefaultLabels', false);
}
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function submit() {
if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) {
$this->emit('error', 'You cannot set both GPU count and GPU device IDs.');
$this->application->settings->gpu_count = null;
$this->application->settings->gpu_device_ids = null;
$this->application->settings->save();
return;
}
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function render()
{
return view('livewire.project.application.advanced');
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Livewire\Component;
class Configuration extends Component
{
public Application $application;
public $servers;
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
$this->application = $application;
$mainServer = $application->destination->server;
$servers = Server::ownedByCurrentTeam()->get();
$this->servers = $servers->filter(function ($server) use ($mainServer) {
return $server->id != $mainServer->id;
});
}
public function render()
{
return view('livewire.project.application.configuration');
}
}

View File

@ -26,14 +26,10 @@ class General extends Component
public bool $isConfigurationChanged = false;
public bool $is_static;
public bool $is_git_submodules_enabled;
public bool $is_git_lfs_enabled;
public bool $is_debug_enabled;
public bool $is_preview_deployments_enabled;
public bool $is_auto_deploy_enabled;
public bool $is_force_https_enabled;
public bool $is_log_drain_enabled;
protected $listeners = [
'resetDefaultLabels'
];
protected $rules = [
'application.name' => 'required',
'application.description' => 'nullable',
@ -56,6 +52,7 @@ class General extends Component
'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required',
];
protected $validationAttributes = [
'application.name' => 'name',
@ -79,6 +76,7 @@ class General extends Component
'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.settings.is_static' => 'Is static',
];
public function mount()
@ -93,18 +91,13 @@ public function mount()
} else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
}
if (data_get($this->application, 'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
$this->is_log_drain_enabled = $this->application->settings->is_log_drain_enabled;
}
$this->checkLabelUpdates();
}
public function instantSave()
{
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
@ -121,40 +114,6 @@ public function checkLabelUpdates()
$this->labelsChanged = false;
}
}
public function instantSave()
{
// @TODO: find another way - if possible
$force_https = $this->application->settings->is_force_https_enabled;
$this->application->settings->is_static = $this->is_static;
if ($this->is_static) {
$this->application->ports_exposes = 80;
} else {
$this->application->ports_exposes = 3000;
}
$this->application->settings->is_git_submodules_enabled = $this->is_git_submodules_enabled;
$this->application->settings->is_git_lfs_enabled = $this->is_git_lfs_enabled;
$this->application->settings->is_debug_enabled = $this->is_debug_enabled;
$this->application->settings->is_preview_deployments_enabled = $this->is_preview_deployments_enabled;
$this->application->settings->is_auto_deploy_enabled = $this->is_auto_deploy_enabled;
$this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
$this->application->settings->is_log_drain_enabled = $this->is_log_drain_enabled;
if ($this->is_log_drain_enabled) {
if (!$this->application->destination->server->isLogDrainEnabled()) {
$this->application->settings->is_log_drain_enabled = $this->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
}
$this->application->settings->save();
$this->application->save();
$this->application->refresh();
$this->emit('success', 'Application settings updated!');
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if ($force_https !== $this->is_force_https_enabled) {
$this->resetDefaultLabels(false);
}
}
public function getWildcardDomain()
{

View File

@ -38,10 +38,10 @@ public function rollbackImage($commit)
]);
}
public function loadImages()
public function loadImages($showToast = false)
{
try {
$image = $this->application->uuid;
$image = $this->application->docker_registry_image_name ?? $this->application->uuid;
if ($this->application->destination->server->isFunctional()) {
$output = instant_remote_process([
"docker inspect --format='{{.Config.Image}}' {$this->application->uuid}",
@ -66,6 +66,7 @@ public function loadImages()
];
})->toArray();
}
$showToast && $this->emit('success', 'Images loaded.');
return [];
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@ -6,5 +6,7 @@
class Destination extends Component
{
public $destination;
public $resource;
public $servers = [];
public $additionalServers = [];
}

View File

@ -43,9 +43,9 @@ public function mount()
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
}
public function serverRefresh()
public function serverRefresh($install = true)
{
$this->validateServer();
$this->validateServer($install);
}
public function instantSave()
{
@ -77,12 +77,15 @@ public function validateServer($install = true)
{
try {
$uptime = $this->server->validateConnection();
if ($uptime) {
$install && $this->emit('success', 'Server is reachable.');
} else {
if (!$uptime) {
$install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return;
}
$supported_os_type = $this->server->validateOS();
if (!$supported_os_type) {
$install && $this->emit('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) {
$install && $this->emit('success', 'Docker Engine is installed.<br> Checking version.');
@ -92,7 +95,7 @@ public function validateServer($install = true)
}
$dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) {
$install && $this->emit('success', 'Docker Engine version is 23+.');
$install && $this->emit('success', 'Docker Engine version is 22+.');
} else {
$install && $this->installDocker();
return;

View File

@ -25,7 +25,7 @@ public function mount()
}
public function submit()
{
$this->emit('serverRefresh');
$this->emit('serverRefresh',false);
}
public function render()
{

View File

@ -0,0 +1,111 @@
<?php
namespace App\Jobs;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Traits\ExecuteRemoteCommandNew;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
class ApplicationDeployDockerImageJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommandNew;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle()
{
ray()->clearAll();
ray('Deploying Docker Image');
try {
$applicationDeploymentQueue = ApplicationDeploymentQueue::find($this->applicationDeploymentQueueId);
$application = Application::find($applicationDeploymentQueue->application_id);
$deploymentUuid = data_get($applicationDeploymentQueue, 'deployment_uuid');
$dockerImage = data_get($application, 'docker_registry_image_name');
$dockerImageTag = data_get($application, 'docker_registry_image_tag');
$productionImageName = str("{$dockerImage}:{$dockerImageTag}");
$destination = $application->destination->getMorphClass()::where('id', $application->destination->id)->first();
$pullRequestId = data_get($applicationDeploymentQueue, 'pull_request_id');
$server = data_get($destination, 'server');
$network = data_get($destination, 'network');
$containerName = generateApplicationContainerName($application, $pullRequestId);
savePrivateKeyToFs($server);
ray("echo 'Starting deployment of {$productionImageName}.'");
$applicationDeploymentQueue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: prepareHelperContainer($server, $network, $deploymentUuid)
);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: generateComposeFile(
deploymentUuid: $deploymentUuid,
server: $server,
network: $network,
application: $application,
containerName: $containerName,
imageName: $productionImageName,
pullRequestId: $pullRequestId
)
);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: rollingUpdate(application: $application, deploymentUuid: $deploymentUuid)
);
} catch (Throwable $e) {
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: [
"echo 'Oops something is not okay, are you okay? 😢'",
"echo '{$e->getMessage()}'",
"echo -n 'Deployment failed. Removing the new version of your application.'",
executeInDocker($deploymentUuid, "docker rm -f $containerName >/dev/null 2>&1"),
]
);
// $this->next(ApplicationDeploymentStatus::FAILED->value);
throw $e;
}
}
// private function next(string $status)
// {
// // If the deployment is cancelled by the user, don't update the status
// if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
// $this->application_deployment_queue->update([
// 'status' => $status,
// ]);
// }
// queue_next_deployment($this->application);
// if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
// }
// if ($status === ApplicationDeploymentStatus::FAILED->value) {
// $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
// }
// }
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Jobs;
use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationDeploySimpleDockerfileJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle() {
ray('Deploying Simple Dockerfile');
}
}

View File

@ -24,6 +24,7 @@
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use RuntimeException;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Throwable;
@ -54,6 +55,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination;
private Server $server;
private Server $mainServer;
private ?ApplicationPreview $preview = null;
private ?string $git_type = null;
@ -110,7 +112,7 @@ public function __construct(int $application_deployment_queue_id)
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
}
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->destination->server;
$this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
@ -180,10 +182,6 @@ public function handle(): void
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
}
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
// Check custom port
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) {
@ -197,6 +195,12 @@ public function handle(): void
try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart();
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
return;
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') {
@ -215,10 +219,12 @@ public function handle(): void
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
if ($this->application->docker_registry_image_name) {
$this->push_to_docker_registry();
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
} catch (Exception $e) {
ray($e);
$this->fail($e);
throw $e;
} finally {
@ -256,7 +262,41 @@ public function handle(): void
);
}
}
private function push_to_docker_registry()
{
try {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"],
[
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
],
);
if ($this->application->docker_registry_image_tag) {
// Tag image with latest
$this->execute_remote_command(
['echo -n "Tagging and pushing image with latest tag."'],
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
);
}
$this->execute_remote_command([
"echo -n 'Image pushed to docker registry.'"
]);
} catch (Exception $e) {
$this->execute_remote_command(
["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
);
ray($e);
}
}
// private function deploy_docker_compose()
// {
// $dockercompose_base64 = base64_encode($this->application->dockercompose);
@ -296,20 +336,32 @@ public function handle(): void
private function generate_image_names()
{
if ($this->application->dockerfile) {
$this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
}
} else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
} else if ($this->pull_request_id !== 0) {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
} else {
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
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->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
}
} else {
$this->dockerImageTag = str($this->commit)->substr(0, 128);
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
}
$this->build_image_name = Str::lower("{$this->application->uuid}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
}
}
private function just_restart()
@ -323,31 +375,41 @@ private function just_restart()
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->generate_compose_file();
$this->rolling_update();
return;
}
$this->execute_remote_command([
"echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'",
]);
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
}
private function save_environment_variables()
private function check_image_locally_or_remotely()
{
$envs = collect([]);
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->value);
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
$this->execute_remote_command([
"docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
]);
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
}
$envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
],
);
}
// private function save_environment_variables()
// {
// $envs = collect([]);
// foreach ($this->application->environment_variables as $env) {
// $envs->push($env->key . '=' . $env->value);
// }
// $envs_base64 = base64_encode($envs->implode("\n"));
// $this->execute_remote_command(
// [
// executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
// ],
// );
// }
private function deploy_simple_dockerfile()
{
$dockerfile_base64 = base64_encode($this->application->dockerfile);
@ -406,7 +468,12 @@ private function deploy_dockerfile_buildpack()
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->rolling_update();
// if ($this->application->additional_destinations) {
// $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations();
// } else {
$this->rolling_update();
// }
}
private function deploy_nixpacks_buildpack()
{
@ -420,12 +487,10 @@ private function deploy_nixpacks_buildpack()
$this->set_base_dir();
$this->generate_image_names();
if (!$this->force_rebuild) {
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->execute_remote_command([
"echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'",
"echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
]);
$this->generate_compose_file();
$this->rolling_update();
@ -468,12 +533,18 @@ private function rolling_update()
{
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
);
$this->stop_running_container(force: true);
$this->start_by_compose_file();
} else {
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Rolling update started.'"],
);
$this->start_by_compose_file();
@ -489,10 +560,10 @@ private function health_check()
}
// ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 0;
$counter = 1;
$this->execute_remote_command(
[
"echo 'Waiting for healthcheck to pass on the new version of your application.'"
"echo 'Waiting for healthcheck to pass on the new container.'"
]
);
if ($this->full_healthcheck_url) {
@ -504,9 +575,6 @@ private function health_check()
}
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries}'"
],
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
@ -516,17 +584,17 @@ private function health_check()
);
$this->execute_remote_command(
[
"echo 'New version healthcheck status: {$this->saved_outputs->get('health_check')}'"
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->execute_remote_command(
[
"echo 'Rolling update completed.'"
"echo 'New container is healthy.'"
],
);
$this->application->update(['status' => 'running']);
break;
}
$counter++;
@ -563,12 +631,15 @@ private function deploy_pull_request()
private function prepare_builder_image()
{
$helperImage = config('coolify.helper_image');
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
$this->execute_remote_command(
[
"echo -n 'Preparing container with helper image: $helperImage.'",
@ -582,7 +653,31 @@ private function prepare_builder_image()
],
);
}
private function deploy_to_additional_destinations()
{
$destination_ids = collect(str($this->application->additional_destinations)->explode(','));
foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id);
$server = $destination->server;
if ($server->team_id !== $this->mainServer->team_id) {
$this->execute_remote_command(
[
"echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'",
],
);
continue;
}
$this->server = $server;
$this->execute_remote_command(
[
"echo -n 'Deploying to {$this->server->name}.'",
],
);
$this->prepare_builder_image();
$this->generate_image_names();
$this->rolling_update();
}
}
private function set_base_dir()
{
$this->execute_remote_command(
@ -631,6 +726,9 @@ private function clone_repository()
{
$importCommands = $this->generate_git_import_commands();
$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}. '"
],
@ -678,7 +776,7 @@ private function generate_git_import_commands()
$this->fullRepoUrl = $this->customRepository;
$private_key = data_get($this->application, 'private_key.private_key');
if (is_null($private_key)) {
throw new Exception('Private key not found. Please add a private key to the application and try again.');
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
@ -737,16 +835,10 @@ private function cleanup_git()
private function generate_nixpacks_confs()
{
$this->execute_remote_command(
[
"echo -n 'Generating nixpacks configuration.'",
]
);
$nixpacks_command = $this->nixpacks_build_cmd();
$this->execute_remote_command(
[
"echo -n Running: $nixpacks_command",
"echo -n 'Generating nixpacks configuration with: $nixpacks_command'",
],
[executeInDocker($this->deployment_uuid, $nixpacks_command)],
[executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
@ -874,6 +966,26 @@ private function generate_compose_file()
]
];
}
if ($this->application->settings->is_gpu_enabled) {
ray('asd');
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($this->application, 'settings.gpu_options', [])
]
];
if (data_get($this->application, 'settings.gpu_count')) {
$count = data_get($this->application, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($this->application, 'settings.gpu_device_ids')) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids');
}
}
if ($this->application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
}
@ -1000,9 +1112,12 @@ private function build_image()
"echo -n 'Static deployment. Copying static assets to the image.'",
]);
} else {
$this->execute_remote_command([
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
$this->execute_remote_command(
[
"echo -n 'Building docker image started.'",
],
["echo -n 'To check the current progress, click on Show Debug Logs.'"]
);
}
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
@ -1084,12 +1199,14 @@ private function build_image()
]);
}
}
$this->execute_remote_command([
"echo -n 'Building docker image completed.'",
]);
}
private function stop_running_container(bool $force = false)
{
$this->execute_remote_command(["echo -n 'Removing old version of your application.'"]);
$this->execute_remote_command(["echo -n 'Removing old container.'"]);
if ($this->newVersionIsHealthy || $force) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id !== 0) {
@ -1107,9 +1224,14 @@ private function stop_running_container(bool $force = false)
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
});
$this->execute_remote_command(
[
"echo 'Rolling update completed.'"
],
);
} else {
$this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"],
["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],
);
}
@ -1121,12 +1243,10 @@ private function start_by_compose_file()
$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],
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
} else {
$this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
}
@ -1185,9 +1305,9 @@ private function next(string $status)
public function failed(Throwable $exception): void
{
$this->execute_remote_command(
["echo 'Oops something is not okay, are you okay? 😢'"],
["echo '{$exception->getMessage()}'"],
["echo -n 'Deployment failed. Removing the new version of your application.'"],
["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
["echo '{$exception->getMessage()}'", 'type' => 'err'],
["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
);

View File

@ -0,0 +1,28 @@
<?php
namespace App\Jobs;
use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationRestartJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle() {
ray('Restarting application');
}
}

View File

@ -37,7 +37,7 @@ public function uniqueId(): int
public function handle(): void
{
ray("checking container statuses for {$this->server->id}");
// ray("checking container statuses for {$this->server->id}");
try {
if (!$this->server->isServerReady()) {
return;

File diff suppressed because it is too large Load Diff

View File

@ -225,7 +225,8 @@ public function source()
return $this->morphTo();
}
public function isDeploymentInprogress() {
public function isDeploymentInprogress()
{
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
if ($deployments > 0) {
return true;
@ -300,8 +301,9 @@ public function isHealthcheckDisabled(): bool
}
return false;
}
public function isLogDrainEnabled() {
return data_get($this, 'settings.is_log_drain_enabled', false);
public function isLogDrainEnabled()
{
return data_get($this, 'settings.is_log_drain_enabled', false);
}
public function isConfigurationChanged($save = false)
{
@ -330,4 +332,14 @@ public function isConfigurationChanged($save = false)
return true;
}
}
public function isMultipleServerDeployment()
{
if (isDev()) {
return true;
}
if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) {
return true;
}
return false;
}
}

View File

@ -171,7 +171,7 @@ public function isServerReady()
break;
}
$result = $this->validateConnection();
ray('validateConnection: ' . $result);
// ray('validateConnection: ' . $result);
if (!$result) {
$serverUptimeCheckNumber++;
$this->update([
@ -304,6 +304,27 @@ public function isLogDrainEnabled()
{
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
}
public function validateOS()
{
$os_release = instant_remote_process(['cat /etc/os-release'], $this);
$datas = collect(explode("\n", $os_release));
$collectedData = collect([]);
foreach ($datas as $data) {
$item = Str::of($data)->trim();
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
}
$ID = data_get($collectedData, 'ID');
$ID_LIKE = data_get($collectedData, 'ID_LIKE');
$VERSION_ID = data_get($collectedData, 'VERSION_ID');
// ray($ID, $ID_LIKE, $VERSION_ID);
if (collect(SUPPORTED_OS)->contains($ID_LIKE)) {
ray('supported');
return str($ID_LIKE)->explode(' ')->first();
} else {
ray('not supported');
return false;
}
}
public function validateConnection()
{
if ($this->skipServer()) {
@ -314,27 +335,22 @@ public function validateConnection()
if (!$uptime) {
$this->settings()->update([
'is_reachable' => false,
'is_usable' => false
]);
return false;
} else {
$this->settings()->update([
'is_reachable' => true,
]);
$this->update([
'unreachable_count' => 0,
]);
}
if (data_get($this, 'unreachable_notification_sent') === true) {
$this->team->notify(new Revived($this));
$this->update(['unreachable_notification_sent' => false]);
}
if (
data_get($this, 'settings.is_reachable') === false ||
data_get($this, 'settings.is_usable') === false
) {
$this->settings()->update([
'is_reachable' => true,
'is_usable' => true
]);
}
$this->update([
'unreachable_count' => 0,
]);
return true;
}
public function validateDockerEngine($throwError = false)
@ -344,7 +360,7 @@ public function validateDockerEngine($throwError = false)
$this->settings->is_usable = false;
$this->settings->save();
if ($throwError) {
throw new \Exception('Server is not usable.');
throw new \Exception('Server is not usable. Docker Engine is not installed.');
}
return false;
}
@ -362,6 +378,7 @@ public function validateDockerEngineVersion()
$this->settings->save();
return false;
}
$this->settings->is_reachable = true;
$this->settings->is_usable = true;
$this->settings->save();
return true;

View File

@ -53,50 +53,83 @@ public function extraFields()
$image = str($application->image)->before(':')->value();
switch ($image) {
case str($image)->contains('minio'):
$data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
if (is_null($admin_user)) {
$admin_user = $this->environment_variables()->where('key', 'MINIO_ROOT_USER')->first();
}
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
$fields->put('MinIO', [
'Console URL' => [
'key' => data_get($console_url, 'key'),
'value' => data_get($console_url, 'value'),
'rules' => 'required|url',
],
'S3 API URL' => [
'key' => data_get($s3_api_url, 'key'),
'value' => data_get($s3_api_url, 'value'),
'rules' => 'required|url',
],
'Admin User' => [
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value'),
'rules' => 'required',
],
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
if (is_null($admin_password)) {
$admin_password = $this->environment_variables()->where('key', 'MINIO_ROOT_PASSWORD')->first();
}
if ($console_url) {
$data = $data->merge([
'Console URL' => [
'key' => data_get($console_url, 'key'),
'value' => data_get($console_url, 'value'),
'rules' => 'required|url',
],
]);
}
if ($s3_api_url) {
$data = $data->merge([
'S3 API URL' => [
'key' => data_get($s3_api_url, 'key'),
'value' => data_get($s3_api_url, 'value'),
'rules' => 'required|url',
],
]);
}
if ($admin_user) {
$data = $data->merge([
'Admin User' => [
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value'),
'rules' => 'required',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('MinIO', $data->toArray());
break;
case str($image)->contains('weblate'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
$fields->put('Weblate', [
'Admin Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
if ($admin_email) {
$data = $data->merge([
'Admin Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Weblate', $data);
}
}
$databases = $this->databases()->get();
@ -367,6 +400,19 @@ public function parse(bool $isNew = false): Collection
$serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', []));
$serviceLabels = collect(data_get($service, 'labels', []));
if ($serviceLabels->count() > 0) {
$removedLabels = collect([]);
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
if (!str($serviceLabel)->contains('=')) {
$removedLabels->put($serviceLabelName, $serviceLabel);
return false;
}
return $serviceLabel;
});
foreach($removedLabels as $removedLabelName =>$removedLabel) {
$serviceLabels->push("$removedLabelName=$removedLabel");
}
}
$containerName = "$serviceName-{$this->uuid}";

View File

@ -12,7 +12,6 @@
trait ExecuteRemoteCommand
{
public ?string $save = null;
public function execute_remote_command(...$commands)
{
static::$batch_counter++;
@ -32,16 +31,20 @@ public function execute_remote_command(...$commands)
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($single_command, 'hidden', false);
$customType = data_get($single_command, 'type');
$ignore_errors = data_get($single_command, 'ignore_errors', false);
$this->save = data_get($single_command, 'save');
$remote_command = generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType) {
$output = Str::of($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$new_log_entry = [
'command' => $command,
'output' => $output,
'type' => $type === 'err' ? 'stderr' : 'stdout',
'command' => remove_iip($command),
'output' => remove_iip($output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,

View File

@ -0,0 +1,77 @@
<?php
namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
trait ExecuteRemoteCommandNew
{
public static $batch_counter = 0;
public function executeRemoteCommand(Server $server, $logModel, $commands)
{
static::$batch_counter++;
if ($commands instanceof Collection) {
$commandsText = $commands;
} else {
$commandsText = collect($commands);
}
$commandsText->each(function ($singleCommand) use ($server, $logModel) {
$command = data_get($singleCommand, 'command') ?? $singleCommand[0] ?? null;
if ($command === null) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($singleCommand, 'hidden', false);
$customType = data_get($singleCommand, 'type');
$ignoreErrors = data_get($singleCommand, 'ignore_errors', false);
$save = data_get($singleCommand, 'save');
$remote_command = generateSshCommand($server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $logModel, $save) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$newLogEntry = [
'command' => remove_iip($command),
'output' => remove_iip($output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if (!$logModel->logs) {
$newLogEntry['order'] = 1;
} else {
$previousLogs = json_decode($logModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
}
$previousLogs[] = $newLogEntry;
$logModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
$logModel->save();
if ($save) {
$this->remoteCommandOutputs[$save] = str($output)->trim();
}
});
$logModel->update([
'current_process_id' => $process->id(),
]);
$processResult = $process->wait();
if ($processResult->exitCode() !== 0) {
if (!$ignoreErrors) {
$status = ApplicationDeploymentStatus::FAILED->value;
$logModel->status = $status;
$logModel->save();
throw new \RuntimeException($processResult->errorOutput());
}
}
});
}
}

View File

@ -38,7 +38,7 @@ public function render(): View|Closure|string
if (is_null($this->id)) $this->id = new Cuid2(7);
if (is_null($this->name)) $this->name = $this->id;
$this->label = Str::title($this->label);
// $this->label = Str::title($this->label);
return view('components.forms.textarea');
}
}

View File

@ -1,8 +1,15 @@
<?php
use App\Jobs\ApplicationDeployDockerImageJob;
use App\Jobs\ApplicationDeploymentJob;
use App\Jobs\ApplicationDeploySimpleDockerfileJob;
use App\Jobs\ApplicationRestartJob;
use App\Jobs\MultipleApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\Server;
use Symfony\Component\Yaml\Yaml;
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null)
{
@ -29,17 +36,305 @@ function queue_application_deployment(int $application_id, string $deployment_uu
if ($running_deployments->count() > 0) {
return;
}
// New deployment
// dispatchDeploymentJob($deployment->id);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
))->onConnection('long-running')->onQueue('long-running');
}
function queue_next_deployment(Application $application)
{
$next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first();
if ($next_found) {
// New deployment
// dispatchDeploymentJob($next_found->id);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
))->onConnection('long-running')->onQueue('long-running');
}
}
function dispatchDeploymentJob($id)
{
$applicationQueue = ApplicationDeploymentQueue::find($id);
$application = Application::find($applicationQueue->application_id);
$isRestartOnly = data_get($applicationQueue, 'restart_only');
$isSimpleDockerFile = data_get($application, 'dockerfile');
$isDockerImage = data_get($application, 'build_pack') === 'dockerimage';
if ($isRestartOnly) {
ApplicationRestartJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else if ($isSimpleDockerFile) {
ApplicationDeploySimpleDockerfileJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else if ($isDockerImage) {
ApplicationDeployDockerImageJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else {
throw new Exception('Unknown build pack');
}
}
// Deployment things
function generateHostIpMapping(Server $server, string $network)
{
// Generate custom host<->ip hostnames
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
return $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
function generateBaseDir(string $deplyomentUuid)
{
return "/artifacts/$deplyomentUuid";
}
function generateWorkdir(string $deplyomentUuid, Application $application)
{
return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/');
}
function prepareHelperContainer(Server $server, string $network, string $deploymentUuid)
{
$basedir = generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network -v /:/host --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
}
$commands->push([
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
"hidden" => true,
]);
return $commands;
}
function generateComposeFile(string $deploymentUuid, Server $server, string $network, Application $application, string $containerName, string $imageName, ?ApplicationPreview $preview = null, int $pullRequestId = 0)
{
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
$workDir = generateWorkdir($deploymentUuid, $application);
$persistent_storages = generateLocalPersistentVolumes($application, $pullRequestId);
$volume_names = generateLocalPersistentVolumesOnlyVolumeNames($application, $pullRequestId);
$environment_variables = generateEnvironmentVariables($application, $ports, $pullRequestId);
if (data_get($application, 'custom_labels')) {
$labels = collect(str($application->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !str($value)->startsWith('coolify.');
});
$application->custom_labels = $labels->implode(',');
$application->save();
} else {
$labels = collect(generateLabelsApplication($application, $preview));
}
if ($pullRequestId !== 0) {
$labels = collect(generateLabelsApplication($application, $preview));
}
$labels = $labels->merge(defaultLabels($application->id, $application->uuid, 0))->toArray();
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => $imageName,
'container_name' => $containerName,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => $labels,
'expose' => $ports,
'networks' => [
$network,
],
'mem_limit' => $application->limits_memory,
'memswap_limit' => $application->limits_memory_swap,
'mem_swappiness' => $application->limits_memory_swappiness,
'mem_reservation' => $application->limits_memory_reservation,
'cpus' => (int) $application->limits_cpus,
'cpuset' => $application->limits_cpuset,
'cpu_shares' => $application->limits_cpu_shares,
]
],
'networks' => [
$network => [
'external' => true,
'name' => $network,
'attachable' => true
]
]
];
if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) {
$docker_compose['services'][$containerName]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($application->settings->is_gpu_enabled) {
ray('asd');
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($application, 'settings.gpu_options', [])
]
];
if (data_get($application, 'settings.gpu_count')) {
$count = data_get($application, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($application, 'settings.gpu_device_ids')) {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($application, 'settings.gpu_device_ids');
}
}
if ($application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $containerName . '.healthcheck');
}
if (count($application->ports_mappings_array) > 0 && $pullRequestId === 0) {
$docker_compose['services'][$containerName]['ports'] = $application->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$containerName]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$commands = collect([]);
$commands->push([
"command" => executeInDocker($deploymentUuid, "echo '{$docker_compose_base64}' | base64 -d > {$workDir}/docker-compose.yml"),
"hidden" => true,
]);
return $commands;
}
function generateLocalPersistentVolumes(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes = [];
foreach ($application->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($pullRequestId !== 0) {
$volume_name = $volume_name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
function generateLocalPersistentVolumesOnlyVolumeNames(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes_names = [];
foreach ($application->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
if ($pullRequestId !== 0) {
$name = $name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
function generateEnvironmentVariables(Application $application, $ports, int $pullRequestId = 0)
{
$environment_variables = collect();
// ray('Generate Environment Variables')->green();
if ($pullRequestId === 0) {
// ray($this->application->runtime_environment_variables)->green();
foreach ($application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
} else {
// ray($this->application->runtime_environment_variables_preview)->green();
foreach ($application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => str($env)->contains('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}");
}
return $environment_variables->all();
}
function rollingUpdate(Application $application, string $deploymentUuid)
{
$commands = collect([]);
$workDir = generateWorkdir($deploymentUuid, $application);
if (count($application->ports_mappings_array) > 0) {
// $this->execute_remote_command(
// [
// "echo '\n----------------------------------------'",
// ],
// ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
// );
// $this->stop_running_container(force: true);
// $this->start_by_compose_file();
} else {
$commands->push(
[
"command" => "echo '\n----------------------------------------'"
],
[
"command" => "echo -n 'Rolling update started.'"
]
);
if ($application->build_pack === 'dockerimage') {
$commands->push(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"), "hidden" => true],
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
);
} else {
$commands->push(
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
);
}
return $commands;
}
}

View File

@ -1,5 +1,6 @@
<?php
const REDACTED = '<REDACTED>';
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb'];
const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *',
@ -26,3 +27,8 @@
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
];
const SUPPORTED_OS = [
'debian',
'rhel centos fedora'
];

View File

@ -170,10 +170,13 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
$i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u');
return $i;
});
return $formatted;
}
function remove_iip($text)
{
$text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
}
function refresh_server_connection(?PrivateKey $private_key = null)
{
if (is_null($private_key)) {

View File

@ -93,8 +93,13 @@ function refreshSession(?Team $team = null): void
}
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{
ray('handleError');
ray($error);
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
return $livewire->emit('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
}
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
}
if ($error instanceof Throwable) {
$message = $error->getMessage();
} else {
@ -103,55 +108,12 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
if ($customErrorMessage) {
$message = $customErrorMessage . ' ' . $message;
}
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
return $livewire->emit('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
}
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
}
if (isset($livewire)) {
return $livewire->emit('error', $message);
}
throw new RuntimeException($message);
throw new Exception($message);
}
function general_error_handler(Throwable $err, Livewire\Component $that = null, $isJson = false, $customErrorMessage = null): mixed
{
try {
ray($err);
ray('ERROR OCCURRED: ' . $err->getMessage());
if ($err instanceof QueryException) {
if ($err->errorInfo[0] === '23505') {
throw new Exception($customErrorMessage ?? 'Duplicate entry found.', '23505');
} else if (count($err->errorInfo) === 4) {
throw new Exception($customErrorMessage ?? $err->errorInfo[3]);
} else {
throw new Exception($customErrorMessage ?? $err->errorInfo[2]);
}
} elseif ($err instanceof TooManyRequestsException) {
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
} else {
if ($err->getMessage() === 'This action is unauthorized.') {
return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage());
}
throw new Exception($customErrorMessage ?? $err->getMessage());
}
} catch (\Throwable $e) {
if ($that) {
return $that->emit('error', $customErrorMessage ?? $e->getMessage());
} elseif ($isJson) {
return response()->json([
'code' => $e->getCode(),
'error' => $e->getMessage(),
]);
} else {
ray($customErrorMessage);
ray($e);
return $customErrorMessage ?? $e->getMessage();
}
}
}
function get_route_parameters(): array
{
return Route::current()->parameters();

View File

@ -3,11 +3,11 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://c35fe90ee56e18b220bb55e8217d4839@o1082494.ingest.sentry.io/4505347448045568',
'dsn' => 'https://396748153b19c469f5ceff50f1664323@o1082494.ingest.sentry.io/4505347448045568',
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.144',
'release' => '4.0.0-beta.145',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
@ -76,6 +76,7 @@
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate
'enable_tracing' => env('SENTRY_ENABLE_TRACING', false),
'traces_sample_rate' => 0.2,
'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_PROFILES_SAMPLE_RATE'),

View File

@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.144';
return '4.0.0-beta.145';

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('is_gpu_enabled')->default(false);
$table->string('gpu_driver')->default('nvidia');
$table->string('gpu_count')->nullable();
$table->string('gpu_device_ids')->nullable();
$table->longText('gpu_options')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_gpu_enabled');
$table->dropColumn('gpu_driver');
$table->dropColumn('gpu_count');
$table->dropColumn('gpu_device_ids');
$table->dropColumn('gpu_options');
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('additional_destinations')->nullable()->after('destination');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('additional_destinations');
});
}
};

View File

@ -1,4 +1,4 @@
<div class="form-control min-w-fit">
<div class="px-2 form-control min-w-fit hover:bg-coolgray-100">
<label class="flex gap-4 px-0 cursor-pointer label">
<span class="flex gap-2 label-text min-w-fit">
@if ($label)
@ -7,20 +7,7 @@
{{ $id }}
@endif
@if ($helper)
<div class="group w-fit">
<div class="cursor-pointer text-warning">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="w-4 h-4 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="absolute hidden text-xs group-hover:block border-coolgray-400 bg-coolgray-500">
<div class="p-4 card-body">
{!! $helper !!}
</div>
</div>
</div>
<x-helper :helper="$helper" />
@endif
</span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html data-theme="coollabs" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -25,6 +26,7 @@
@endif
</head>
@section('body')
<body>
@livewireScripts
<dialog id="help" class="modal">
@ -120,6 +122,9 @@ function copyToClipboard(text) {
Livewire.on('success', (message) => {
if (message) Toaster.success(message)
})
Livewire.on('installDocker', () => {
installDocker.showModal();
})
</script>
</body>
@show

View File

@ -225,8 +225,7 @@
Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="installDocker"
onclick="installDocker.showModal()">
<x-forms.button class="justify-center box" wire:click="installDocker">
Let's do it!</x-forms.button>
@if ($dockerInstallationStarted)
<x-forms.button class="justify-center box" wire:click="dockerInstalledOrSkipped">
@ -235,9 +234,10 @@
</x-slot:actions>
<x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.</p>
to run optimal.<br><br>Minimum Docker Engine version is: 22<br><br>To manually install Docker Engine, check <a target="_blank" class="underline text-warning" href="https://coolify.io/docs/servers#install-docker-engine-manually">this documentation</a>.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>

View File

@ -0,0 +1,55 @@
<div>
<div class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Advanced</h2>
</div>
<div>Advanced configuration for your application.</div>
<div class="flex flex-col w-full pt-4">
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave id="application.settings.is_log_drain_enabled" label="Drain Logs" />
<x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..."
instantSave id="application.settings.is_force_https_enabled" label="Force Https" />
@if ($application->git_based())
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
id="application.settings.is_auto_deploy_enabled" label="Auto Deploy" />
<x-forms.checkbox
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="application.settings.is_preview_deployments_enabled" label="Preview Deployments" />
<x-forms.checkbox instantSave id="application.settings.is_git_submodules_enabled" label="Git Submodules"
helper="Allow Git Submodules during build process." />
<x-forms.checkbox instantSave id="application.settings.is_git_lfs_enabled" label="Git LFS"
helper="Allow Git LFS during build process." />
@endif
<form wire:submit.prevent="submit">
<div class="flex gap-2">
<x-forms.checkbox helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='text-white underline' target='_blank'>here</a>." instantSave
id="application.settings.is_gpu_enabled" label="GPU Enabled Application" />
@if ($application->settings->is_gpu_enabled)
<x-forms.button type="submiot">Save</x-forms.button>
@endif
</div>
@if ($application->settings->is_gpu_enabled)
<div class="flex flex-col w-full gap-2 p-2 xl:flex-row">
<x-forms.input label="GPU Driver" id="application.settings.gpu_driver"> </x-forms.input>
<x-forms.input label="GPU Count" placeholder="empty means use all GPUs"
id="application.settings.gpu_count"> </x-forms.input>
<x-forms.input label="GPU Device Ids" placeholder="0,2"
helper="Comma separated list of device ids. More info <a href='https://docs.docker.com/compose/gpu-support/#access-specific-devices' class='text-white underline' target='_blank'>here</a>."
id="application.settings.gpu_device_ids"> </x-forms.input>
</div>
<div class="px-2">
<x-forms.textarea label="GPU Options" id="application.settings.gpu_options">
</x-forms.textarea>
</div>
@endif
</form>
{{-- <x-forms.checkbox disabled instantSave id="is_dual_cert" label="Dual Certs?" />
<x-forms.checkbox disabled instantSave id="is_custom_ssl" label="Is Custom SSL?" />
<x-forms.checkbox disabled instantSave id="is_http2" label="Is Http2?" /> --}}
</div>
</div>
</div>

View File

@ -1,10 +1,12 @@
<x-layout>
<div>
<h1>Configuration</h1>
<livewire:project.application.heading :application="$application" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
<a :class="activeTab === 'advanced' && 'text-white'"
@click.prevent="activeTab = 'advanced'; window.location.hash = 'advanced'" href="#">Advanced</a>
@if ($application->build_pack !== 'static')
<a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
@ -34,7 +36,7 @@
@endif
@if ($application->build_pack !== 'static')
<a :class="activeTab === 'health' && 'text-white'"
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Health Checks
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Healthchecks
</a>
@endif
<a :class="activeTab === 'rollback' && 'text-white'"
@ -52,6 +54,9 @@
<div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.application.general :application="$application" />
</div>
<div x-cloak x-show="activeTab === 'advanced'" class="h-full">
<livewire:project.application.advanced :application="$application" />
</div>
<div x-cloak x-show="activeTab === 'environment-variables'">
<livewire:project.shared.environment-variable.all :resource="$application" />
</div>
@ -61,7 +66,7 @@
</div>
@endif
<div x-cloak x-show="activeTab === 'server'">
<livewire:project.shared.destination :destination="$application->destination" />
<livewire:project.shared.destination :resource="$application" :servers="$servers" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.service.storage :resource="$application" />
@ -86,4 +91,4 @@
</div>
</div>
</div>
</x-layout>
</div>

View File

@ -48,9 +48,8 @@ class="fixed top-4 right-16" x-on:click="toggleScroll"><svg class="icon" viewBox
@foreach (decode_remote_command_output($application_deployment_queue) as $line)
<div @class([
'font-mono whitespace-pre-line',
'text-white' => $line['type'] == 'stdout',
'text-error' => $line['type'] == 'stderr',
'text-warning' => $line['hidden'],
'text-error' => $line['type'] == 'stderr',
])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT:
@endif{{ $line['output'] }}@if ($line['hidden'])

View File

@ -40,12 +40,33 @@
</div>
@if ($application->could_set_build_commands())
<div class="w-64">
<x-forms.checkbox instantSave id="is_static" label="Is it a static site?"
<x-forms.checkbox instantSave id="application.settings.is_static"
label="Is it a static site?"
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
</div>
@endif
</div>
@endif
<h3>Docker Registry</h3>
@if ($application->build_pack !== 'dockerimage')
<div>Push the built image to a docker registry. More info <a class="underline"
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
@endif
<div class="flex flex-col gap-2 xl:flex-row">
@if ($application->build_pack === 'dockerimage')
<x-forms.input id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@else
<x-forms.input id="application.docker_registry_image_name"
helper="Empty means it won't push the image to a docker registry."
placeholder="Empty means it won't push the image to a docker registry." label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag"
placeholder="Empty means only push commit sha tag."
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" />
@endif
</div>
@if ($application->build_pack !== 'dockerimage')
<h3>Build</h3>
@ -64,8 +85,6 @@
</div>
@endif
@endif
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@ -88,11 +107,6 @@
@endif
@endif
</div>
@else
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
</div>
@endif
@if ($application->dockerfile)
@ -112,29 +126,5 @@
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>
</div>
<h3>Advanced</h3>
<div class="flex flex-col">
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings." instantSave
id="is_log_drain_enabled" label="Drain Logs" />
<x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..."
instantSave id="is_force_https_enabled" label="Force Https" />
@if ($application->git_based())
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
id="is_auto_deploy_enabled" label="Auto Deploy" />
<x-forms.checkbox
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="is_preview_deployments_enabled" label="Preview Deployments" />
<x-forms.checkbox instantSave id="is_git_submodules_enabled" label="Git Submodules"
helper="Allow Git Submodules during build process." />
<x-forms.checkbox instantSave id="is_git_lfs_enabled" label="Git LFS"
helper="Allow Git LFS during build process." />
@endif
{{-- <x-forms.checkbox disabled instantSave id="is_dual_cert" label="Dual Certs?" />
<x-forms.checkbox disabled instantSave id="is_custom_ssl" label="Is Custom SSL?" />
<x-forms.checkbox disabled instantSave id="is_http2" label="Is Http2?" /> --}}
</div>
</form>
</div>

View File

@ -1,12 +1,12 @@
<div x-init="$wire.loadImages">
<div class="flex items-center gap-2">
<h2>Rollback</h2>
<x-forms.button wire:click='loadImages'>Reload Available Images</x-forms.button>
<x-forms.button wire:click='loadImages(true)'>Reload Available Images</x-forms.button>
</div>
<div class="pb-4 ">You can easily rollback to a previously built image quickly.</div>
<div class="pb-4 ">You can easily rollback to a previously built <span class="text-warning">(local)</span> images quickly.</div>
<div wire:target='loadImages'>
<div class="flex flex-wrap">
@foreach ($images as $image)
@forelse ($images as $image)
<div class="w-2/4 p-2">
<div class="rounded shadow-lg bg-coolgray-200">
<div class="p-2">
@ -25,14 +25,16 @@
Rollback
</x-forms.button>
@else
<x-forms.button wire:click="rollbackImage('{{ data_get($image, 'tag') }}')">
<x-forms.button class="bg-coolgray-100" wire:click="rollbackImage('{{ data_get($image, 'tag') }}')">
Rollback
</x-forms.button>
@endif
</div>
</div>
</div>
@endforeach
@empty
<div>No images found locally.</div>
@endforelse
</div>
</div>
</div>

View File

@ -17,10 +17,10 @@
<h3>Service Specific Configuration</h3>
</div>
<div class="grid grid-cols-2 gap-2">
@foreach ($fields as $serviceName => $fields)
<x-forms.input type="{{ data_get($fields, 'isPassword') ? 'password' : 'text' }}" required
@foreach ($fields as $serviceName => $field)
<x-forms.input type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}" required
helper="Variable name: {{ $serviceName }}"
label="{{ data_get($fields, 'serviceName') }} {{ data_get($fields, 'name') }}"
label="{{ data_get($field, 'serviceName') }} {{ data_get($field, 'name') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input>
@endforeach
</div>

View File

@ -3,7 +3,34 @@
<div class="">The destination server where your application will be deployed to.</div>
<div class="py-4 ">
<a class="box"
href="{{ route('server.show', ['server_uuid' => data_get($destination, 'server.uuid')]) }}">On server <span class="px-1 text-warning">{{ data_get($destination, 'server.name') }}</span>
in <span class="px-1 text-warning"> {{ data_get($destination, 'network') }} </span> network.</a>
href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}">On
server <span class="px-1 text-warning">{{ data_get($resource, 'destination.server.name') }}</span>
in <span class="px-1 text-warning"> {{ data_get($resource, 'destination.network') }} </span> network.</a>
</div>
{{-- {{$resource->additional_destinations}} --}}
{{-- @if (count($servers) > 0)
<div>
<h3>Additional Servers</h3>
@foreach ($servers as $server)
<form wire:submit.prevent='submit' class="p-2 border border-coolgray-400">
<h4>{{ $server->name }}</h4>
<div class="text-sm text-coolgray-600">{{ $server->description }}</div>
<x-forms.checkbox id="additionalServers.{{ $loop->index }}.enabled" label="Enabled">
</x-forms.checkbox>
<x-forms.select label="Destination" id="additionalServers.{{ $loop->index }}.destination" required>
@foreach ($server->destinations() as $destination)
@if ($loop->first)
<option selected value="{{ $destination->uuid }}">{{ $destination->name }}</option>
<option value="{{ $destination->uuid }}">{{ $destination->name }}</option>
@else
<option value="{{ $destination->uuid }}">{{ $destination->name }}</option>
<option value="{{ $destination->uuid }}">{{ $destination->name }}</option>
@endif
@endforeach
</x-forms.select>
<x-forms.button type="submit">Save</x-forms.button>
</form>
@endforeach
</div>
@endif --}}
</div>

View File

@ -1,4 +1,4 @@
<div x-init="$wire.validateServer(false)">
<div>
<x-modal yesOrNo modalId="changeLocalhost" modalTitle="Change Localhost" action="submit">
<x-slot:modalBody>
<p>You could lost a lot of functionalities if you change the server details of the server where Coolify is
@ -64,9 +64,4 @@
helper="Disk cleanup job will be executed if disk usage is more than this number." />
@endif
</form>
<script>
Livewire.on('installDocker', () => {
installDocker.showModal();
})
</script>
</div>

View File

@ -63,7 +63,7 @@
<livewire:project.shared.environment-variable.all :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'server'">
<livewire:project.shared.destination :destination="$database->destination" />
<livewire:project.shared.destination :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.service.storage :resource="$database" />

View File

@ -5,6 +5,7 @@
use App\Http\Controllers\DatabaseController;
use App\Http\Controllers\MagicController;
use App\Http\Controllers\ProjectController;
use App\Http\Livewire\Project\Application\Configuration as ApplicationConfiguration;
use App\Http\Livewire\Boarding\Index as BoardingIndex;
use App\Http\Livewire\Project\Service\Index as ServiceIndex;
use App\Http\Livewire\Project\Service\Show as ServiceShow;
@ -101,7 +102,8 @@
Route::get('/project/{project_uuid}/{environment_name}', [ProjectController::class, 'resources'])->name('project.resources');
// Applications
Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}', [ApplicationController::class, 'configuration'])->name('project.application.configuration');
Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}', ApplicationConfiguration::class)->name('project.application.configuration');
Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}/deployment', [ApplicationController::class, 'deployments'])->name('project.application.deployments');
Route::get(
'/project/{project_uuid}/{environment_name}/application/{application_uuid}/deployment/{deployment_uuid}',
@ -167,7 +169,6 @@
'private_key' => PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail()
]))->name('security.private-key.show');
Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens');
});

View File

@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.144"
"version": "4.0.0-beta.145"
}
}
}