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 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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted | |||||||
|                     ], |                     ], | ||||||
|                 ); |                 ); | ||||||
|                 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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted | |||||||
|                     '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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted | |||||||
|             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 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); | |||||||
|     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 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted | |||||||
| 
 | 
 | ||||||
|             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 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted | |||||||
|                 } |                 } | ||||||
|                 $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> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Andras Bacsai
					Andras Bacsai