feat: healthcheck for apps
This commit is contained in:
		
							
								
								
									
										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 bool $newVersionIsHealthy = false; | ||||
|     private ApplicationDeploymentQueue $application_deployment_queue; | ||||
|     private Application $application; | ||||
|     private string $deployment_uuid; | ||||
| @@ -315,7 +316,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted | ||||
|                     ], | ||||
|                 ); | ||||
|                 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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted | ||||
|                     'restart' => RESTART_MODE, | ||||
|                     'environment' => $environment_variables, | ||||
|                     'labels' => generateLabelsApplication($this->application, $this->preview), | ||||
|                     'expose' => $ports, | ||||
|                     // 'expose' => $ports,
 | ||||
|                     'networks' => [ | ||||
|                         $this->destination->network, | ||||
|                     ], | ||||
| @@ -632,15 +637,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted | ||||
|             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 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); | ||||
|     private function stop_running_container() | ||||
|     { | ||||
|         if ($this->currently_running_container_name) { | ||||
|             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], | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -99,6 +99,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted | ||||
| 
 | ||||
|             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 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted | ||||
|                 } | ||||
|                 $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('-'); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|                 </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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Andras Bacsai
					Andras Bacsai