refactor applicationdeploymentjob

This commit is contained in:
Andras Bacsai
2023-12-04 15:08:24 +01:00
parent a696b5271a
commit 8b6323b906
11 changed files with 769 additions and 17 deletions

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
use RuntimeException;
@@ -48,6 +49,65 @@ class Application extends BaseModel
$application->environment_variables_preview()->delete();
});
}
// Build packs / deployment types
public function servers(): Collection
{
$mainServer = data_get($this, 'destination.server');
$additionalDestinations = data_get($this, 'additional_destinations', null);
$additionalServers = collect([]);
if ($this->isMultipleServerDeployment()) {
ray('asd');
if (str($additionalDestinations)->isNotEmpty()) {
$additionalDestinations = str($additionalDestinations)->explode(',');
foreach ($additionalDestinations as $destinationId) {
$destination = StandaloneDocker::find($destinationId)->whereNot('id', $mainServer->id)->first();
$server = data_get($destination, 'server');
$additionalServers->push($server);
}
}
}
return collect([$mainServer])->merge($additionalServers);
}
public function generateImageNames(string $commit, int $pullRequestId)
{
if ($this->dockerfile) {
if ($this->docker_registry_image_name) {
$buildImageName = Str::lower("{$this->docker_registry_image_name}:build");
$productionImageName = Str::lower("{$this->docker_registry_image_name}:latest");
} else {
$buildImageName = Str::lower("{$this->uuid}:build");
$productionImageName = Str::lower("{$this->uuid}:latest");
}
} else if ($this->build_pack === 'dockerimage') {
$productionImageName = Str::lower("{$this->docker_registry_image_name}:{$this->docker_registry_image_tag}");
} else if ($pullRequestId === 0) {
$dockerImageTag = str($commit)->substr(0, 128);
if ($this->docker_registry_image_name) {
$buildImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}-build");
$productionImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}");
} else {
$buildImageName = Str::lower("{$this->uuid}:{$dockerImageTag}-build");
$productionImageName = Str::lower("{$this->uuid}:{$dockerImageTag}");
}
} else if ($pullRequestId !== 0) {
if ($this->docker_registry_image_name) {
$buildImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}-build");
$productionImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}");
} else {
$buildImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}-build");
$productionImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}");
}
}
return [
'buildImageName' => $buildImageName,
'productionImageName' => $productionImageName,
];
}
// End of build packs / deployment types
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -385,9 +445,7 @@ class Application extends BaseModel
}
public function isMultipleServerDeployment()
{
if (isDev()) {
return true;
}
return false;
if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) {
return true;
}
@@ -431,6 +489,294 @@ class Application extends BaseModel
{
return "/artifacts/{$uuid}";
}
function generateHealthCheckCommands()
{
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0';
}
if (!$this->health_check_port) {
$health_check_port = $this->ports_exposes_array[0];
} else {
$health_check_port = $this->health_check_port;
}
if ($this->health_check_path) {
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
$generated_healthchecks_commands = [
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path} > /dev/null"
];
} else {
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
$generated_healthchecks_commands = [
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/"
];
}
return implode(' ', $generated_healthchecks_commands);
}
function generateLocalPersistentVolumes(int $pullRequestId)
{
$persistentStorages = [];
$volumeNames = [];
foreach ($this->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($pullRequestId !== 0) {
$volume_name = $volume_name . '-pr-' . $pullRequestId;
}
$persistentStorages[] = $volume_name . ':' . $persistentStorage->mount_path;
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
if ($pullRequestId !== 0) {
$name = $name . '-pr-' . $pullRequestId;
}
$volumeNames[$name] = [
'name' => $name,
'external' => false,
];
}
return [
'persistentStorages' => $persistentStorages,
'volumeNames' => $volumeNames,
];
}
public function generateEnvironmentVariables($ports)
{
$environmentVariables = collect();
// ray('Generate Environment Variables')->green();
if ($this->pull_request_id === 0) {
// ray($this->runtime_environment_variables)->green();
foreach ($this->runtime_environment_variables as $env) {
$environmentVariables->push("$env->key=$env->value");
}
foreach ($this->nixpacks_environment_variables as $env) {
$environmentVariables->push("$env->key=$env->value");
}
} else {
// ray($this->runtime_environment_variables_preview)->green();
foreach ($this->runtime_environment_variables_preview as $env) {
$environmentVariables->push("$env->key=$env->value");
}
foreach ($this->nixpacks_environment_variables_preview as $env) {
$environmentVariables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default
if ($environmentVariables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
$environmentVariables->push("PORT={$ports[0]}");
}
return $environmentVariables->all();
}
function generateDockerComposeFile(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
{
$pullRequestId = $deployment->pull_request_id;
$ports = $this->settings->is_static ? [80] : $this->ports_exposes_array;
$container_name = generateApplicationContainerName($this, $this->pull_request_id);
$commit = str($deployment->getOutput('git_commit_sha'))->before("\t");
[
'productionImageName' => $productionImageName
] = $this->generateImageNames($commit, $pullRequestId);
[
'persistentStorages' => $persistentStorages,
'volumeNames' => $volumeNames
] = $this->generateLocalPersistentVolumes($pullRequestId);
$environmentVariables = $this->generateEnvironmentVariables($ports);
if (data_get($this, 'custom_labels')) {
$labels = collect(str($this->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !Str::startsWith($value, 'coolify.');
});
$this->custom_labels = $labels->implode(',');
$this->save();
} else {
$labels = collect(generateLabelsApplication($this, $this->preview));
}
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this, $this->preview));
}
$labels = $labels->merge(defaultLabels($this->id, $this->uuid, $this->pull_request_id))->toArray();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $productionImageName,
'container_name' => $container_name,
'restart' => RESTART_MODE,
'environment' => $environmentVariables,
'expose' => $ports,
'networks' => [
$this->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
$this->generateHealthCheckCommands()
],
'interval' => $this->health_check_interval . 's',
'timeout' => $this->health_check_timeout . 's',
'retries' => $this->health_check_retries,
'start_period' => $this->health_check_start_period . 's'
],
'mem_limit' => $this->limits_memory,
'memswap_limit' => $this->limits_memory_swap,
'mem_swappiness' => $this->limits_memory_swappiness,
'mem_reservation' => $this->limits_memory_reservation,
'cpus' => (int) $this->limits_cpus,
'cpuset' => $this->limits_cpuset,
'cpu_shares' => $this->limits_cpu_shares,
]
],
'networks' => [
$this->destination->network => [
'external' => true,
'name' => $this->destination->network,
'attachable' => true
]
]
];
if ($server->isSwarm()) {
data_forget($docker_compose, 'services.' . $container_name . '.container_name');
data_forget($docker_compose, 'services.' . $container_name . '.expose');
data_forget($docker_compose, 'services.' . $container_name . '.restart');
data_forget($docker_compose, 'services.' . $container_name . '.mem_limit');
data_forget($docker_compose, 'services.' . $container_name . '.memswap_limit');
data_forget($docker_compose, 'services.' . $container_name . '.mem_swappiness');
data_forget($docker_compose, 'services.' . $container_name . '.mem_reservation');
data_forget($docker_compose, 'services.' . $container_name . '.cpus');
data_forget($docker_compose, 'services.' . $container_name . '.cpuset');
data_forget($docker_compose, 'services.' . $container_name . '.cpu_shares');
$docker_compose['services'][$container_name]['deploy'] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated',
'replicas' => 1,
'update_config' => [
'order' => 'start-first'
],
'rollback_config' => [
'order' => 'start-first'
],
'labels' => $labels,
'resources' => [
'limits' => [
'cpus' => $this->limits_cpus,
'memory' => $this->limits_memory,
],
'reservations' => [
'cpus' => $this->limits_cpus,
'memory' => $this->limits_memory,
]
]
];
} else {
$docker_compose['services'][$container_name]['labels'] = $labels;
}
if ($server->isLogDrainEnabled() && $this->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($this->settings->is_gpu_enabled) {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($this, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($this, 'settings.gpu_options', [])
]
];
if (data_get($this, 'settings.gpu_count')) {
$count = data_get($this, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($this, 'settings.gpu_device_ids')) {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this, 'settings.gpu_device_ids');
}
}
if ($this->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $container_name . '.healthcheck');
}
if (count($this->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$container_name]['ports'] = $this->ports_mappings_array;
}
if (count($persistentStorages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistentStorages;
}
if (count($volumeNames) > 0) {
$docker_compose['volumes'] = $volumeNames;
}
$docker_compose['services'][$this->uuid] = $docker_compose['services'][$container_name];
data_forget($docker_compose, 'services.' . $container_name);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$server->executeRemoteCommand(
commands: collect([])->push([
'command' => executeInDocker($deployment->deployment_uuid, "echo '{$docker_compose_base64}' | base64 -d > {$workdir}/docker-compose.yml"),
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $deployment
);
}
function rollingUpdateApplication(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
{
$pullRequestId = $deployment->pull_request_id;
$containerName = generateApplicationContainerName($this, $pullRequestId);
// if (count($this->ports_mappings_array) > 0) {
// $deployment->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
$containers = getCurrentApplicationContainerStatus($server, $this->id, $pullRequestId);
ray($containers);
// if ($pullRequestId === 0) {
// $containers = $containers->filter(function ($container) use ($containerName) {
// return data_get($container, 'Names') !== $containerName;
// });
// }
$containers->each(function ($container) use ($server, $deployment) {
$removingContainerName = data_get($container, 'Names');
$server->executeRemoteCommand(
commands: collect([])->push([
'command' => "docker rm -f $removingContainerName",
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $deployment
);
});
// }
$server->executeRemoteCommand(
commands: collect([])->push([
'command' => executeInDocker($deployment->deployment_uuid, "docker compose --project-directory {$workdir} up --build -d"),
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $deployment
);
$deployment->addLogEntry("New container started.");
}
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
{
$baseDir = $this->generateBaseDir($deployment_uuid);