feat: healthcheck for apps
This commit is contained in:
parent
67078fdc71
commit
3fc544e0b9
39
app/Http/Livewire/Project/Shared/HealthChecks.php
Normal file
39
app/Http/Livewire/Project/Shared/HealthChecks.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@ -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],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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('-');
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user