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 bool $newVersionIsHealthy = false;
private ApplicationDeploymentQueue $application_deployment_queue;
private Application $application;
private string $deployment_uuid;
@ -315,7 +316,11 @@ private function health_check()
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->execute_remote_command(
[
"echo 'New version of your application is healthy.'"
],
[
"echo 'Rolling update completed.'"
],
@ -524,7 +529,7 @@ private function generate_compose_file()
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => generateLabelsApplication($this->application, $this->preview),
'expose' => $ports,
// 'expose' => $ports,
'networks' => [
$this->destination->network,
],
@ -632,15 +637,17 @@ private function generate_healthcheck_commands()
return 'exit 0';
}
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) {
$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 {
$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);
@ -700,10 +707,17 @@ private function build_image()
private function stop_running_container()
{
if ($this->currently_running_container_name) {
$this->execute_remote_command(
["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],
);
if ($this->newVersionIsHealthy) {
$this->execute_remote_command(
["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) {
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status','unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
@ -145,6 +147,7 @@ public function handle(): void
}
$serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) {
ray('Service label id: ' . $serviceLabelId);
$coolifyName = data_get($labels, 'coolify.name');
$serviceName = Str::of($coolifyName)->before('-');
$serviceUuid = Str::of($coolifyName)->after('-');

View File

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

View File

@ -1,8 +1,8 @@
@props([
'text' => 'Degraded',
'status' => 'Degraded',
])
<x-loading wire:loading.delay />
<div class="flex items-center gap-2" wire:loading.remove.delay.longer>
<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>

View File

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

View File

@ -1,8 +1,8 @@
@props([
'text' => 'Restarting',
'status' => 'Restarting',
])
<x-loading wire:loading.delay />
<div class="flex items-center gap-2" wire:loading.remove.delay.longer>
<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>

View File

@ -1,8 +1,8 @@
@props([
'text' => 'Running',
'status' => 'Running',
])
<x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 " wire:loading.remove.delay.longer>
<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>

View File

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

View File

@ -1,8 +1,8 @@
@props([
'text' => 'Stopped',
'status' => 'Stopped',
])
<x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 " wire:loading.remove.delay.longer>
<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>

View File

@ -53,12 +53,12 @@
@foreach ($application->previews as $preview)
<div class="flex flex-col p-4 bg-coolgray-200">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (data_get($preview, 'status') === 'running')
<x-status.running />
@elseif (data_get($preview, 'status') === 'restarting')
<x-status.restarting />
@if (Str::of(data_get($preview, 'status'))->startsWith('running'))
<x-status.running :status="$status" />
@elseif(Str::of(data_get($preview, 'status'))->startsWith('restarting'))
<x-status.restarting :status="$status" />
@else
<x-status.stopped />
<x-status.stopped :status="$status" />
@endif
@if (data_get($preview, 'status') !== 'exited')
| <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
</a>
@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'"
@click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback
</a>
@ -58,6 +61,9 @@
<div x-cloak x-show="activeTab === 'previews'">
<livewire:project.application.previews :application="$application" />
</div>
<div x-cloak x-show="activeTab === 'health'">
<livewire:project.shared.health-checks :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'rollback'">
<livewire:project.application.rollback :application="$application" />
</div>