feat: healthcheck for apps

This commit is contained in:
Andras Bacsai 2023-09-22 15:29:19 +02:00
parent 67078fdc71
commit 3fc544e0b9
13 changed files with 126 additions and 35 deletions

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use Livewire\Component;
class HealthChecks extends Component
{
public $resource;
protected $rules = [
'resource.health_check_path' => 'string',
'resource.health_check_port' => 'nullable|string',
'resource.health_check_host' => 'string',
'resource.health_check_method' => 'string',
'resource.health_check_return_code' => 'integer',
'resource.health_check_scheme' => 'string',
'resource.health_check_response_text' => 'nullable|string',
'resource.health_check_interval' => 'integer',
'resource.health_check_timeout' => 'integer',
'resource.health_check_retries' => 'integer',
'resource.health_check_start_period' => 'integer',
];
public function submit()
{
try {
$this->validate();
$this->resource->save();
$this->emit('saved');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.health-checks');
}
}

View File

@ -37,6 +37,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private int $application_deployment_queue_id; private int $application_deployment_queue_id;
private bool $newVersionIsHealthy = false;
private ApplicationDeploymentQueue $application_deployment_queue; private ApplicationDeploymentQueue $application_deployment_queue;
private Application $application; private Application $application;
private string $deployment_uuid; private string $deployment_uuid;
@ -315,7 +316,11 @@ private function health_check()
], ],
); );
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->execute_remote_command( $this->execute_remote_command(
[
"echo 'New version of your application is healthy.'"
],
[ [
"echo 'Rolling update completed.'" "echo 'Rolling update completed.'"
], ],
@ -524,7 +529,7 @@ private function generate_compose_file()
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'environment' => $environment_variables, 'environment' => $environment_variables,
'labels' => generateLabelsApplication($this->application, $this->preview), 'labels' => generateLabelsApplication($this->application, $this->preview),
'expose' => $ports, // 'expose' => $ports,
'networks' => [ 'networks' => [
$this->destination->network, $this->destination->network,
], ],
@ -632,15 +637,17 @@ private function generate_healthcheck_commands()
return 'exit 0'; return 'exit 0';
} }
if (!$this->application->health_check_port) { if (!$this->application->health_check_port) {
$this->application->health_check_port = $this->application->ports_exposes_array[0]; $health_check_port = $this->application->ports_exposes_array[0];
} else {
$health_check_port = $this->application->health_check_port;
} }
if ($this->application->health_check_path) { if ($this->application->health_check_path) {
$generated_healthchecks_commands = [ $generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}{$this->application->health_check_path} > /dev/null" "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null"
]; ];
} else { } else {
$generated_healthchecks_commands = [ $generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}/" "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"
]; ];
} }
return implode(' ', $generated_healthchecks_commands); return implode(' ', $generated_healthchecks_commands);
@ -700,10 +707,17 @@ private function build_image()
private function stop_running_container() private function stop_running_container()
{ {
if ($this->currently_running_container_name) { if ($this->currently_running_container_name) {
$this->execute_remote_command( if ($this->newVersionIsHealthy) {
["echo -n 'Removing old version of your application.'"], $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true], ["echo -n 'Removing old version of your application.'"],
); [executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true],
);
} else {
$this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true],
);
}
} }
} }

View File

@ -99,6 +99,8 @@ public function handle(): void
foreach ($containers as $container) { foreach ($containers as $container) {
$containerStatus = data_get($container, 'State.Status'); $containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status','unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$labels = data_get($container, 'Config.Labels'); $labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels)); $labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId'); $labelId = data_get($labels, 'coolify.applicationId');
@ -145,6 +147,7 @@ public function handle(): void
} }
$serviceLabelId = data_get($labels, 'coolify.serviceId'); $serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) { if ($serviceLabelId) {
ray('Service label id: ' . $serviceLabelId);
$coolifyName = data_get($labels, 'coolify.name'); $coolifyName = data_get($labels, 'coolify.name');
$serviceName = Str::of($coolifyName)->before('-'); $serviceName = Str::of($coolifyName)->before('-');
$serviceUuid = Str::of($coolifyName)->after('-'); $serviceUuid = Str::of($coolifyName)->after('-');

View File

@ -1,6 +1,7 @@
<?php <?php
use App\Models\Service; use App\Models\Service;
use Illuminate\Support\Str;
function replaceRegex(?string $name = null) function replaceRegex(?string $name = null)
{ {
@ -22,14 +23,14 @@ function serviceStatus(Service $service)
$applications = $service->applications; $applications = $service->applications;
$databases = $service->databases; $databases = $service->databases;
foreach ($applications as $application) { foreach ($applications as $application) {
if ($application->status === 'running') { if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true; $foundRunning = true;
} else { } else {
$isDegraded = true; $isDegraded = true;
} }
} }
foreach ($databases as $database) { foreach ($databases as $database) {
if ($database->status === 'running') { if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true; $foundRunning = true;
} else { } else {
$isDegraded = true; $isDegraded = true;

View File

@ -1,8 +1,8 @@
@props([ @props([
'text' => 'Degraded', 'status' => 'Degraded',
]) ])
<x-loading wire:loading.delay /> <x-loading wire:loading.delay />
<div class="flex items-center gap-2" wire:loading.remove.delay.longer> <div class="flex items-center gap-2" wire:loading.remove.delay.longer>
<div class="badge badge-warning badge-xs"></div> <div class="badge badge-warning badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-warning">{{ $text }}</div> <div class="text-xs font-medium tracking-wide text-warning">{{ Str::headline($status) }}</div>
</div> </div>

View File

@ -1,7 +1,7 @@
@if ($status === 'running') @if (Str::of($status)->startsWith('running'))
<x-status.running /> <x-status.running :status="$status" />
@elseif($status === 'restarting') @elseif(Str::of($status)->startsWith('restarting'))
<x-status.restarting /> <x-status.restarting :status="$status" />
@else @else
<x-status.stopped /> <x-status.stopped :status="$status" />
@endif @endif

View File

@ -1,8 +1,8 @@
@props([ @props([
'text' => 'Restarting', 'status' => 'Restarting',
]) ])
<x-loading wire:loading.delay /> <x-loading wire:loading.delay />
<div class="flex items-center gap-2" wire:loading.remove.delay.longer> <div class="flex items-center gap-2" wire:loading.remove.delay.longer>
<div class="badge badge-warning badge-xs"></div> <div class="badge badge-warning badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-warning">{{ $text }}</div> <div class="text-xs font-medium tracking-wide text-warning">{{ Str::headline($status) }}</div>
</div> </div>

View File

@ -1,8 +1,8 @@
@props([ @props([
'text' => 'Running', 'status' => 'Running',
]) ])
<x-loading wire:loading.delay.longer /> <x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 " wire:loading.remove.delay.longer> <div class="flex items-center gap-2 " wire:loading.remove.delay.longer>
<div class="badge badge-success badge-xs"></div> <div class="badge badge-success badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-success">{{ $text }}</div> <div class="text-xs font-medium tracking-wide text-success">{{ Str::headline($status) }}</div>
</div> </div>

View File

@ -1,9 +1,9 @@
@if ($complexStatus === 'running') @if (Str::of($complexStatus)->startsWith('running'))
<x-status.running /> <x-status.running :status="$complexStatus" />
@elseif($complexStatus === 'restarting') @elseif(Str::of($complexStatus)->startsWith('restarting'))
<x-status.restarting /> <x-status.restarting :status="$complexStatus" />
@elseif($complexStatus === 'degraded') @elseif(Str::of($complexStatus)->startsWith('degraded'))
<x-status.degraded /> <x-status.degraded :status="$complexStatus" />
@else @else
<x-status.stopped /> <x-status.stopped :status="$complexStatus" />
@endif @endif

View File

@ -1,8 +1,8 @@
@props([ @props([
'text' => 'Stopped', 'status' => 'Stopped',
]) ])
<x-loading wire:loading.delay.longer /> <x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 " wire:loading.remove.delay.longer> <div class="flex items-center gap-2 " wire:loading.remove.delay.longer>
<div class="badge badge-error badge-xs"></div> <div class="badge badge-error badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-error">{{ $text }}</div> <div class="text-xs font-medium tracking-wide text-error">{{ Str::headline($status) }}</div>
</div> </div>

View File

@ -53,12 +53,12 @@
@foreach ($application->previews as $preview) @foreach ($application->previews as $preview)
<div class="flex flex-col p-4 bg-coolgray-200"> <div class="flex flex-col p-4 bg-coolgray-200">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} | <div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (data_get($preview, 'status') === 'running') @if (Str::of(data_get($preview, 'status'))->startsWith('running'))
<x-status.running /> <x-status.running :status="$status" />
@elseif (data_get($preview, 'status') === 'restarting') @elseif(Str::of(data_get($preview, 'status'))->startsWith('restarting'))
<x-status.restarting /> <x-status.restarting :status="$status" />
@else @else
<x-status.stopped /> <x-status.stopped :status="$status" />
@endif @endif
@if (data_get($preview, 'status') !== 'exited') @if (data_get($preview, 'status') !== 'exited')
| <a target="_blank" href="{{ data_get($preview, 'fqdn') }}">Open Preview | <a target="_blank" href="{{ data_get($preview, 'fqdn') }}">Open Preview

View File

@ -0,0 +1,28 @@
<form wire:submit.prevent='submit' class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Health Checks</h2>
<x-forms.button type="submit">Save</x-forms.button>
</div>
<div class="pb-4">Define how your resource's health should be checked.</div>
<div class="flex flex-col gap-4">
<div class="flex gap-2">
<x-forms.input id="resource.health_check_method" placeholder="GET" label="Method" required />
<x-forms.input id="resource.health_check_scheme" placeholder="http" label="Scheme" required />
<x-forms.input id="resource.health_check_host" placeholder="localhost" label="Host" required />
<x-forms.input id="resource.health_check_port"
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
<x-forms.input id="resource.health_check_path" placeholder="/health" label="Path" required />
</div>
<div class="flex gap-2">
<x-forms.input id="resource.health_check_return_code" placeholder="200" label="Return Code" required />
<x-forms.input id="resource.health_check_response_text" placeholder="OK" label="Response Text" />
</div>
<div class="flex gap-2">
<x-forms.input id="resource.health_check_interval" placeholder="30" label="Interval" required />
<x-forms.input id="resource.health_check_timeout" placeholder="30" label="Timeout" required />
<x-forms.input id="resource.health_check_retries" placeholder="3" label="Retries" required />
<x-forms.input id="resource.health_check_start_period" placeholder="30" label="Start Period" required />
</div>
</div>
</form>

View File

@ -26,6 +26,9 @@
Deployments Deployments
</a> </a>
@endif @endif
<a :class="activeTab === 'health' && 'text-white'"
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Health Checks
</a>
<a :class="activeTab === 'rollback' && 'text-white'" <a :class="activeTab === 'rollback' && 'text-white'"
@click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback @click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback
</a> </a>
@ -58,6 +61,9 @@
<div x-cloak x-show="activeTab === 'previews'"> <div x-cloak x-show="activeTab === 'previews'">
<livewire:project.application.previews :application="$application" /> <livewire:project.application.previews :application="$application" />
</div> </div>
<div x-cloak x-show="activeTab === 'health'">
<livewire:project.shared.health-checks :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'rollback'"> <div x-cloak x-show="activeTab === 'rollback'">
<livewire:project.application.rollback :application="$application" /> <livewire:project.application.rollback :application="$application" />
</div> </div>