wip: swarm

This commit is contained in:
Andras Bacsai 2023-11-28 18:31:04 +01:00
parent c505a6ce9c
commit b4874c7df3
8 changed files with 172 additions and 130 deletions

View File

@ -11,29 +11,34 @@ class StopApplication
public function handle(Application $application) public function handle(Application $application)
{ {
$server = $application->destination->server; $server = $application->destination->server;
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0); if ($server->isSwarm()) {
if ($containers->count() > 0) { instant_remote_process(["docker stack rm {$application->uuid}" ], $server);
foreach ($containers as $container) { } else {
$containerName = data_get($container, 'Names'); $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
if ($containerName) { if ($containers->count() > 0) {
instant_remote_process( foreach ($containers as $container) {
["docker rm -f {$containerName}"], $containerName = data_get($container, 'Names');
$server if ($containerName) {
); instant_remote_process(
["docker rm -f {$containerName}"],
$server
);
}
} }
// TODO: make notification for application
// $application->environment->project->team->notify(new StatusChanged($application));
} }
// TODO: make notification for application // Delete Preview Deployments
// $application->environment->project->team->notify(new StatusChanged($application)); $previewDeployments = $application->previews;
} foreach ($previewDeployments as $previewDeployment) {
// Delete Preview Deployments $containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
$previewDeployments = $application->previews; foreach ($containers as $container) {
foreach ($previewDeployments as $previewDeployment) { $name = str_replace('/', '', $container['Names']);
$containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id); instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
foreach ($containers as $container) { }
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
} }
} }
} }
} }

View File

@ -14,8 +14,6 @@ class Dashboard extends Component
public function mount() public function mount()
{ {
$this->servers = Server::ownedByCurrentTeam()->get(); $this->servers = Server::ownedByCurrentTeam()->get();
ray($this->servers[1]);
ray($this->servers[1]->standaloneDockers);
$this->projects = Project::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get();
} }
// public function getIptables() // public function getIptables()

View File

@ -126,7 +126,6 @@ class Select extends Component
{ {
$this->server_id = $server->id; $this->server_id = $server->id;
$this->standaloneDockers = $server->standaloneDockers; $this->standaloneDockers = $server->standaloneDockers;
ray($server);
$this->swarmDockers = $server->swarmDockers; $this->swarmDockers = $server->swarmDockers;
$this->current_step = 'destinations'; $this->current_step = 'destinations';
} }

View File

@ -156,25 +156,27 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Generate custom host<->ip mapping // Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
$allContainers = format_docker_command_output_to_json($allContainers); if (!is_null($allContainers)) {
$ips = collect([]); $allContainers = format_docker_command_output_to_json($allContainers);
if (count($allContainers) > 0) { $ips = collect([]);
$allContainers = $allContainers[0]; if (count($allContainers) > 0) {
foreach ($allContainers as $container) { $allContainers = $allContainers[0];
$containerName = data_get($container, 'Name'); foreach ($allContainers as $container) {
if ($containerName === 'coolify-proxy') { $containerName = data_get($container, 'Name');
continue; if ($containerName === 'coolify-proxy') {
} continue;
$containerIp = data_get($container, 'IPv4Address'); }
if ($containerName && $containerIp) { $containerIp = data_get($container, 'IPv4Address');
$containerIp = str($containerIp)->before('/'); if ($containerName && $containerIp) {
$ips->put($containerName, $containerIp->value()); $containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
} }
} }
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
} }
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
if ($this->application->dockerfile_target_build) { if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$this->application->dockerfile_target_build} "; $this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
@ -214,6 +216,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') { if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') {
$this->push_to_docker_registry(); $this->push_to_docker_registry();
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Creating / updating stack.");
$this->execute_remote_command(
[
"docker stack deploy --with-registry-auth --prune --compose-file {$this->configuration_dir}/docker-compose.yml {$this->application->uuid}"
],
);
}
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true); $this->application->isConfigurationChanged(true);
@ -534,75 +544,83 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update() private function rolling_update()
{ {
if (count($this->application->ports_mappings_array) > 0) { if ($this->server->isSwarm()) {
$this->execute_remote_command( // Skip this.
[
"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 { } else {
$this->execute_remote_command( if (count($this->application->ports_mappings_array) > 0) {
[ $this->execute_remote_command(
"echo '\n----------------------------------------'", [
], "echo '\n----------------------------------------'",
["echo -n 'Rolling update started.'"], ],
); ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
$this->start_by_compose_file(); );
$this->health_check(); $this->stop_running_container(force: true);
$this->stop_running_container(); $this->start_by_compose_file();
$this->application_deployment_queue->addLogEntry("Rolling update completed."); } else {
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Rolling update started.'"],
);
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
}
} }
} }
private function health_check() private function health_check()
{ {
if ($this->application->isHealthcheckDisabled()) { if ($this->server->isSwarm()) {
$this->newVersionIsHealthy = true; // Implement healthcheck for swarm
return; } else {
} if ($this->application->isHealthcheckDisabled()) {
// ray('New container name: ', $this->container_name); $this->newVersionIsHealthy = true;
if ($this->container_name) { return;
$counter = 1; }
$this->execute_remote_command( // ray('New container name: ', $this->container_name);
[ if ($this->container_name) {
"echo 'Waiting for healthcheck to pass on the new container.'" $counter = 1;
]
);
if ($this->full_healthcheck_url) {
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'" "echo 'Waiting for healthcheck to pass on the new container.'"
] ]
); );
} if ($this->full_healthcheck_url) {
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"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( $this->execute_remote_command(
[ [
"echo 'New container is healthy.'" "echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
]
);
}
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
], ],
); );
break; if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->execute_remote_command(
[
"echo 'New container is healthy.'"
],
);
break;
}
$counter++;
sleep($this->application->health_check_interval);
} }
$counter++;
sleep($this->application->health_check_interval);
} }
} }
} }
@ -849,21 +867,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview)); $labels = collect(generateLabelsApplication($this->application, $this->preview));
// $newHostLabel = $newLabels->filter(function ($label) {
// return str($label)->contains('Host');
// });
// $labels = $labels->reject(function ($label) {
// return str($label)->contains('Host');
// });
// ray($labels,$newLabels);
// $labels = $labels->map(function ($label) {
// $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
// $replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
// $newLabel = preg_replace($pattern, $replacement, $label);
// return $newLabel;
// });
// $labels = $labels->merge($newHostLabel);
} }
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$docker_compose = [ $docker_compose = [
@ -906,6 +909,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
] ]
] ]
]; ];
if ($this->server->isSwarm()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.container_name');
data_forget($docker_compose, 'services.' . $this->container_name . '.expose');
data_forget($docker_compose, 'services.' . $this->container_name . '.restart');
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_limit');
data_forget($docker_compose, 'services.' . $this->container_name . '.memswap_limit');
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_swappiness');
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_reservation');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpus');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpuset');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares');
} else {
}
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) { if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = [ $docker_compose['services'][$this->container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',

View File

@ -67,7 +67,7 @@ class Server extends BaseModel
{ {
$teamId = currentTeam()->id; $teamId = currentTeam()->id;
$selectArray = collect($select)->concat(['id']); $selectArray = collect($select)->concat(['id']);
return Server::whereTeamId($teamId)->with('settings')->select($selectArray->all())->orderBy('name'); return Server::whereTeamId($teamId)->with('settings','swarmDockers','standaloneDockers')->select($selectArray->all())->orderBy('name');
} }
static public function isUsable() static public function isUsable()
@ -87,6 +87,8 @@ class Server extends BaseModel
return $this->hasOne(ServerSetting::class); return $this->hasOne(ServerSetting::class);
} }
public function addInitialNetwork() { public function addInitialNetwork() {
ray($this->id);
if ($this->id === 0) { if ($this->id === 0) {
if ($this->isSwarm()) { if ($this->isSwarm()) {
SwarmDocker::create([ SwarmDocker::create([
@ -106,13 +108,13 @@ class Server extends BaseModel
} else { } else {
if ($this->isSwarm()) { if ($this->isSwarm()) {
SwarmDocker::create([ SwarmDocker::create([
'name' => 'coolify', 'name' => 'coolify-overlay',
'network' => 'coolify-overlay', 'network' => 'coolify-overlay',
'server_id' => $this->id, 'server_id' => $this->id,
]); ]);
} else { } else {
StandaloneDocker::create([ StandaloneDocker::create([
'name' => 'coolify', 'name' => 'coolify-overlay',
'network' => 'coolify', 'network' => 'coolify',
'server_id' => $this->id, 'server_id' => $this->id,
]); ]);
@ -452,7 +454,7 @@ class Server extends BaseModel
public function validateCoolifyNetwork($isSwarm = false) public function validateCoolifyNetwork($isSwarm = false)
{ {
if ($isSwarm) { if ($isSwarm) {
return instant_remote_process(["docker network create --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false); return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false);
} else { } else {
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
} }

View File

@ -123,6 +123,9 @@ function instant_remote_process(Collection|array $command, Server $server, $thro
} }
return excludeCertainErrors($process->errorOutput(), $exitCode); return excludeCertainErrors($process->errorOutput(), $exitCode);
} }
if ($output === 'null') {
$output = null;
}
return $output; return $output;
} }
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)

View File

@ -13,9 +13,9 @@ class SwarmDockerSeeder extends Seeder
*/ */
public function run(): void public function run(): void
{ {
SwarmDocker::create([ // SwarmDocker::create([
'name' => 'Swarm Docker 1', // 'name' => 'Swarm Docker 1',
'server_id' => 1, // 'server_id' => 1,
]); // ]);
} }
} }

View File

@ -69,21 +69,39 @@
@endif @endif
@if ($application->build_pack !== 'dockerimage' && $application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockerimage' && $application->build_pack !== 'dockercompose')
<h3>Docker Registry</h3> <h3>Docker Registry</h3>
<div>Push the built image to a docker registry. More info <a class="underline" @if ($application->destination->server->isSwarm())
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div> <div>Docker Swarm requires the image to be available in a registry. More info <a class="underline"
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
@else
<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"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->build_pack === 'dockerimage') @if ($application->build_pack === 'dockerimage')
<x-forms.input id="application.docker_registry_image_name" label="Docker Image" /> @if ($application->destination->server->isSwarm())
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" /> <x-forms.input required 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" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@endif
@else @else
<x-forms.input id="application.docker_registry_image_name" @if ($application->destination->server->isSwarm())
helper="Empty means it won't push the image to a docker registry." <x-forms.input id="application.docker_registry_image_name" required label="Docker Image" />
placeholder="Empty means it won't push the image to a docker registry." <x-forms.input id="application.docker_registry_image_tag"
label="Docker Image" /> 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."
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
placeholder="Empty means only push commit sha tag." @else
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." <x-forms.input id="application.docker_registry_image_name"
label="Docker Image Tag" /> 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
@endif @endif
</div> </div>
@ -140,8 +158,8 @@
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button> <x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
<x-forms.textarea rows="10" readonly id="application.docker_compose" label="Docker Compose Content" <x-forms.textarea rows="10" readonly id="application.docker_compose"
helper="You need to modify the docker compose file." /> label="Docker Compose Content" helper="You need to modify the docker compose file." />
{{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr" {{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr"
label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}} label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}}
@endif @endif