mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
fix(api): accept settings that the UI can change but the API rejected
- Applications: is_container_label_readonly_enabled (turning it back on regenerates labels), include_coolify_default_variables, and dockerfile_target_build on create. - Databases: save is_log_drain_enabled on update instead of dropping it, include_coolify_default_variables, and disable_local_backup on backups (only with save_s3). - Services: include_coolify_default_variables, and redirect on service applications (www/non-www needs both hosts). - Servers: wildcard_domain, server_timezone, backup_compression_cpu_percentage, and the proxy error page. Closes #11092
This commit is contained in:
@@ -67,6 +67,16 @@ class UpdateServiceApplicationFromApi
|
||||
$serviceApplication->setNoindexDomains($payload['noindex_domains'] ?? []);
|
||||
}
|
||||
|
||||
if (array_key_exists('redirect', $payload)) {
|
||||
if (in_array($payload['redirect'], ['www', 'non-www'], true) && ! $this->hasWwwAndNonWwwDomains($serviceApplication->fqdn)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['redirect' => ['The www and non-www redirects need both a www and a non-www domain in url.']],
|
||||
], 422);
|
||||
}
|
||||
$serviceApplication->redirect = $payload['redirect'];
|
||||
}
|
||||
|
||||
if (array_key_exists('human_name', $payload)) {
|
||||
$serviceApplication->human_name = $payload['human_name'];
|
||||
}
|
||||
@@ -130,4 +140,14 @@ class UpdateServiceApplicationFromApi
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function hasWwwAndNonWwwDomains(?string $fqdn): bool
|
||||
{
|
||||
$hosts = str($fqdn ?? '')->explode(',')
|
||||
->map(fn (string $url) => strtolower((string) parse_url(trim($url), PHP_URL_HOST)))
|
||||
->filter();
|
||||
|
||||
return $hosts->contains(fn (string $host) => str_starts_with($host, 'www.'))
|
||||
&& $hosts->contains(fn (string $host) => ! str_starts_with($host, 'www.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class ApplicationsController extends Controller
|
||||
'include_source_commit_in_build',
|
||||
'is_env_sorting_enabled',
|
||||
'redact_all_env_values_in_logs',
|
||||
'include_coolify_default_variables',
|
||||
'is_pr_deployments_public_enabled',
|
||||
'stop_grace_period',
|
||||
'docker_images_to_keep',
|
||||
@@ -60,6 +61,7 @@ class ApplicationsController extends Controller
|
||||
'is_stripprefix_enabled',
|
||||
'is_permanent_redirect_enabled',
|
||||
'traefik_certificate_resolver',
|
||||
'is_container_label_readonly_enabled',
|
||||
'is_raw_compose_deployment_enabled',
|
||||
'is_log_drain_enabled',
|
||||
'is_gpu_enabled',
|
||||
@@ -81,10 +83,12 @@ class ApplicationsController extends Controller
|
||||
'include_source_commit_in_build',
|
||||
'is_env_sorting_enabled',
|
||||
'redact_all_env_values_in_logs',
|
||||
'include_coolify_default_variables',
|
||||
'is_pr_deployments_public_enabled',
|
||||
'is_gzip_enabled',
|
||||
'is_stripprefix_enabled',
|
||||
'is_permanent_redirect_enabled',
|
||||
'is_container_label_readonly_enabled',
|
||||
'is_raw_compose_deployment_enabled',
|
||||
'is_log_drain_enabled',
|
||||
'is_gpu_enabled',
|
||||
@@ -231,15 +235,18 @@ class ApplicationsController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
$wasLabelManagementEnabled = $application->settings->is_container_label_readonly_enabled;
|
||||
|
||||
$application->settings->fill($settings)->save();
|
||||
|
||||
$regenerateLabels = ! $application->wasRecentlyCreated
|
||||
&& $application->settings->is_container_label_readonly_enabled
|
||||
&& (array_key_exists('is_gzip_enabled', $settings)
|
||||
&& (! $wasLabelManagementEnabled
|
||||
|| array_key_exists('is_gzip_enabled', $settings)
|
||||
|| array_key_exists('is_stripprefix_enabled', $settings)
|
||||
|| array_key_exists('is_permanent_redirect_enabled', $settings)
|
||||
|| array_key_exists('traefik_certificate_resolver', $settings));
|
||||
|
||||
$application->settings->fill($settings)->save();
|
||||
|
||||
if ($regenerateLabels) {
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->save();
|
||||
@@ -431,6 +438,7 @@ class ApplicationsController extends Controller
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
|
||||
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'],
|
||||
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
|
||||
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
|
||||
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
|
||||
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
|
||||
@@ -457,6 +465,7 @@ class ApplicationsController extends Controller
|
||||
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
|
||||
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
|
||||
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
|
||||
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
@@ -464,6 +473,7 @@ class ApplicationsController extends Controller
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -637,6 +647,7 @@ class ApplicationsController extends Controller
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
|
||||
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository'],
|
||||
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
|
||||
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
|
||||
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
|
||||
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
|
||||
@@ -663,6 +674,7 @@ class ApplicationsController extends Controller
|
||||
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
|
||||
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
|
||||
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
|
||||
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
@@ -670,6 +682,7 @@ class ApplicationsController extends Controller
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -843,6 +856,7 @@ class ApplicationsController extends Controller
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
|
||||
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'],
|
||||
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
|
||||
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
|
||||
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
|
||||
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
|
||||
@@ -869,6 +883,7 @@ class ApplicationsController extends Controller
|
||||
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
|
||||
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
|
||||
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
|
||||
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
@@ -876,6 +891,7 @@ class ApplicationsController extends Controller
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -1046,6 +1062,7 @@ class ApplicationsController extends Controller
|
||||
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
|
||||
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
|
||||
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
|
||||
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
@@ -1053,6 +1070,7 @@ class ApplicationsController extends Controller
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -1219,6 +1237,7 @@ class ApplicationsController extends Controller
|
||||
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
|
||||
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
|
||||
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
|
||||
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
@@ -1226,6 +1245,7 @@ class ApplicationsController extends Controller
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -1337,7 +1357,7 @@ class ApplicationsController extends Controller
|
||||
if ($return instanceof JsonResponse) {
|
||||
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', 'is_preview_deployments_enabled', 'domains', 'noindex_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', 'use_build_secrets', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'http_basic_auth_hash_algorithm', 'http_basic_auth_bcrypt_cost', 'http_basic_auth_argon2id_memory_cost', 'http_basic_auth_argon2id_time_cost', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', ...self::APPLICATION_SETTING_FIELDS];
|
||||
$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', 'is_preview_deployments_enabled', 'domains', 'noindex_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', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'use_build_secrets', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'http_basic_auth_hash_algorithm', 'http_basic_auth_bcrypt_cost', 'http_basic_auth_argon2id_memory_cost', 'http_basic_auth_argon2id_time_cost', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', ...self::APPLICATION_SETTING_FIELDS];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255',
|
||||
@@ -3207,6 +3227,7 @@ class ApplicationsController extends Controller
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
|
||||
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'],
|
||||
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
|
||||
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
|
||||
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
|
||||
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
|
||||
@@ -3237,6 +3258,7 @@ class ApplicationsController extends Controller
|
||||
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
|
||||
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
|
||||
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
|
||||
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
@@ -3244,6 +3266,7 @@ class ApplicationsController extends Controller
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -3348,7 +3371,7 @@ class ApplicationsController extends Controller
|
||||
|
||||
$this->authorize('update', $application);
|
||||
|
||||
if ($request->has('redact_all_env_values_in_logs')) {
|
||||
if ($request->has('redact_all_env_values_in_logs') || $request->has('include_coolify_default_variables')) {
|
||||
$this->authorize('manageEnvironment', $application);
|
||||
}
|
||||
|
||||
|
||||
@@ -594,6 +594,8 @@ class DatabasesController extends Controller
|
||||
'health_check_retries' => ['type' => 'integer', 'description' => 'Healthcheck retries count.', 'minimum' => 1, 'default' => 5],
|
||||
'health_check_start_period' => ['type' => 'integer', 'description' => 'Healthcheck start period in seconds.', 'minimum' => 0, 'default' => 5],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values, database passwords, and remote secrets are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Restart the database to apply it.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Send the database logs to the log drain of the server. Log drain must be enabled on the server. Restart the database to apply it.'],
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -654,6 +656,8 @@ class DatabasesController extends Controller
|
||||
'limits_cpu_shares' => 'numeric',
|
||||
'instant_deploy' => 'boolean',
|
||||
'redact_all_env_values_in_logs' => 'boolean',
|
||||
'include_coolify_default_variables' => 'boolean',
|
||||
'is_log_drain_enabled' => 'boolean',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@@ -665,6 +669,8 @@ class DatabasesController extends Controller
|
||||
$uuid = $request->uuid;
|
||||
$instantDeploy = $request->boolean('instant_deploy');
|
||||
$redactAllEnvValuesInLogs = $request->has('redact_all_env_values_in_logs') ? $request->boolean('redact_all_env_values_in_logs') : null;
|
||||
$isLogDrainEnabled = $request->has('is_log_drain_enabled') ? $request->boolean('is_log_drain_enabled') : null;
|
||||
$includeCoolifyDefaultVariables = $request->has('include_coolify_default_variables') ? $request->boolean('include_coolify_default_variables') : null;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
$database = queryDatabaseByUuidWithinTeam($uuid, $teamId);
|
||||
if (! $database) {
|
||||
@@ -678,10 +684,17 @@ class DatabasesController extends Controller
|
||||
$this->authorize('manage', $database);
|
||||
}
|
||||
|
||||
if ($redactAllEnvValuesInLogs !== null) {
|
||||
if ($redactAllEnvValuesInLogs !== null || $includeCoolifyDefaultVariables !== null) {
|
||||
$this->authorize('manageEnvironment', $database);
|
||||
}
|
||||
|
||||
if ($isLogDrainEnabled && ! $database->destination->server->isLogDrainEnabled()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['is_log_drain_enabled' => ['Log drain is not enabled on the server. Enable it on the server first.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($request->has('is_public') && (bool) $request->is_public !== (bool) $database->is_public) {
|
||||
$forbidden = $this->deployAbilityForbiddenResponse($request, 'Making a database public or private starts or stops its proxy on the server, so it needs a token with the deploy permission.');
|
||||
if ($forbidden) {
|
||||
@@ -935,6 +948,12 @@ class DatabasesController extends Controller
|
||||
if ($redactAllEnvValuesInLogs !== null) {
|
||||
$database->redact_all_env_values_in_logs = $redactAllEnvValuesInLogs;
|
||||
}
|
||||
if ($isLogDrainEnabled !== null) {
|
||||
$database->is_log_drain_enabled = $isLogDrainEnabled;
|
||||
}
|
||||
if ($includeCoolifyDefaultVariables !== null) {
|
||||
$database->include_coolify_default_variables = $includeCoolifyDefaultVariables;
|
||||
}
|
||||
$database->update($request->only($allowedFields));
|
||||
// The Redis password is stored in the REDIS_PASSWORD variable, not in a column.
|
||||
if ($database instanceof StandaloneRedis && filled($request->redis_password)) {
|
||||
@@ -961,6 +980,8 @@ class DatabasesController extends Controller
|
||||
'changed_fields' => [
|
||||
...array_values(array_intersect($allowedFields, array_keys($request->all()))),
|
||||
...($redactAllEnvValuesInLogs === null ? [] : ['redact_all_env_values_in_logs']),
|
||||
...($isLogDrainEnabled === null ? [] : ['is_log_drain_enabled']),
|
||||
...($includeCoolifyDefaultVariables === null ? [] : ['include_coolify_default_variables']),
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -969,6 +990,14 @@ class DatabasesController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
private function disableLocalBackupWithoutS3Response(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['disable_local_backup' => ['Local backups can only be disabled when save_s3 is true.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Backup',
|
||||
description: 'Create a new scheduled backup configuration for a database',
|
||||
@@ -1001,6 +1030,7 @@ class DatabasesController extends Controller
|
||||
'frequency' => ['type' => 'string', 'description' => 'Backup frequency (cron expression or: every_minute, hourly, daily, weekly, monthly, yearly)'],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled', 'default' => true],
|
||||
'save_s3' => ['type' => 'boolean', 'description' => 'Whether to save backups to S3', 'default' => false],
|
||||
'disable_local_backup' => ['type' => 'boolean', 'description' => 'Delete the local backup file after the upload to S3. Only allowed when save_s3 is true.', 'default' => false],
|
||||
's3_storage_uuid' => ['type' => 'string', 'description' => 'Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination. Either field is required if save_s3 is true.'],
|
||||
's3_storage_uuids' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'UUIDs of every S3 storage the backup is uploaded to. Replaces the current destinations. When s3_storage_uuid is also sent, it must be in this list and becomes the primary destination; otherwise the first entry is the primary destination.'],
|
||||
'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'],
|
||||
@@ -1054,7 +1084,7 @@ class DatabasesController extends Controller
|
||||
)]
|
||||
public function create_backup(Request $request)
|
||||
{
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 's3_storage_uuids', 'timeout', 'missing_backup_notification_days'];
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 's3_storage_uuids', 'timeout', 'missing_backup_notification_days', 'disable_local_backup'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -1071,6 +1101,7 @@ class DatabasesController extends Controller
|
||||
'frequency' => 'required|string',
|
||||
'enabled' => 'boolean',
|
||||
'save_s3' => 'boolean',
|
||||
'disable_local_backup' => 'boolean',
|
||||
'dump_all' => 'boolean',
|
||||
'backup_now' => 'boolean|nullable',
|
||||
's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable',
|
||||
@@ -1138,6 +1169,9 @@ class DatabasesController extends Controller
|
||||
'errors' => ['s3_storage_uuid' => ['The s3_storage_uuid or s3_storage_uuids field is required when save_s3 is true.']],
|
||||
], 422);
|
||||
}
|
||||
if ($request->boolean('disable_local_backup') && ! $request->boolean('save_s3')) {
|
||||
return $this->disableLocalBackupWithoutS3Response();
|
||||
}
|
||||
|
||||
// Check for extra fields
|
||||
$extraFields = array_diff(array_keys($request->all()), $backupConfigFields, ['backup_now']);
|
||||
@@ -1256,6 +1290,7 @@ class DatabasesController extends Controller
|
||||
type: 'object',
|
||||
properties: [
|
||||
'save_s3' => ['type' => 'boolean', 'description' => 'Whether data is saved in s3 or not'],
|
||||
'disable_local_backup' => ['type' => 'boolean', 'description' => 'Delete the local backup file after the upload to S3. Only allowed when save_s3 is true. Turning save_s3 off also turns this off.'],
|
||||
's3_storage_uuid' => ['type' => 'string', 'description' => 'Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination; when it is already a destination, it becomes the primary destination and the other destinations stay.'],
|
||||
's3_storage_uuids' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'UUIDs of every S3 storage the backup is uploaded to. Replaces the current destinations. When s3_storage_uuid is also sent, it must be in this list and becomes the primary destination; otherwise the first entry is the primary destination.'],
|
||||
'backup_now' => ['type' => 'boolean', 'description' => 'Whether to take a backup now or not. Needs a token with the deploy permission.'],
|
||||
@@ -1304,7 +1339,7 @@ class DatabasesController extends Controller
|
||||
)]
|
||||
public function update_backup(Request $request)
|
||||
{
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 's3_storage_uuids', 'timeout', 'missing_backup_notification_days'];
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 's3_storage_uuids', 'timeout', 'missing_backup_notification_days', 'disable_local_backup'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -1317,6 +1352,7 @@ class DatabasesController extends Controller
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'save_s3' => 'boolean',
|
||||
'disable_local_backup' => 'boolean',
|
||||
'backup_now' => 'boolean|nullable',
|
||||
'enabled' => 'boolean',
|
||||
'dump_all' => 'boolean',
|
||||
@@ -1409,11 +1445,18 @@ class DatabasesController extends Controller
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
if ($request->boolean('disable_local_backup') && ! $saveToS3) {
|
||||
return $this->disableLocalBackupWithoutS3Response();
|
||||
}
|
||||
|
||||
$backupData = Arr::except($request->only($backupConfigFields), ['s3_storage_uuid', 's3_storage_uuids']);
|
||||
if ($s3Storages !== null) {
|
||||
$backupData['s3_storage_id'] = $s3Storages->first()?->id;
|
||||
}
|
||||
// Without S3 the backup must stay on the server, as in the UI.
|
||||
if (! $saveToS3) {
|
||||
$backupData['disable_local_backup'] = false;
|
||||
}
|
||||
|
||||
// Validate databases_to_backup input
|
||||
if (! empty($backupData['databases_to_backup'])) {
|
||||
|
||||
@@ -56,6 +56,7 @@ class ServerProxyController extends Controller
|
||||
'status' => data_get($server->proxy, 'status'),
|
||||
'redirect_enabled' => (bool) data_get($server->proxy, 'redirect_enabled', true),
|
||||
'redirect_url' => data_get($server->proxy, 'redirect_url'),
|
||||
'proxy_error_page' => $server->proxy_error_page,
|
||||
'generate_exact_labels' => (bool) ($server->settings->generate_exact_labels ?? false),
|
||||
'traefik_certificate_resolver' => $server->traefikCertificateResolver(),
|
||||
];
|
||||
@@ -91,6 +92,7 @@ class ServerProxyController extends Controller
|
||||
new OA\Property(property: 'status', type: 'string', nullable: true, example: 'running'),
|
||||
new OA\Property(property: 'redirect_enabled', type: 'boolean', example: true),
|
||||
new OA\Property(property: 'redirect_url', type: 'string', nullable: true, example: 'https://example.com'),
|
||||
new OA\Property(property: 'proxy_error_page', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'generate_exact_labels', type: 'boolean', example: false),
|
||||
new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true, example: 'letsencrypt', description: 'Traefik certificate resolver for generated HTTPS routers. Null when the proxy uses its own certificates.'),
|
||||
new OA\Property(property: 'configuration', type: 'string', nullable: true, description: 'Docker Compose proxy configuration when stored in the database. Only present with read:sensitive.'),
|
||||
@@ -136,6 +138,7 @@ class ServerProxyController extends Controller
|
||||
properties: [
|
||||
new OA\Property(property: 'redirect_enabled', type: 'boolean'),
|
||||
new OA\Property(property: 'redirect_url', type: 'string', nullable: true, description: 'Public http(s) redirect URL, or null to clear.'),
|
||||
new OA\Property(property: 'proxy_error_page', type: 'string', nullable: true, description: 'Custom HTML error page that the proxy shows for unknown hosts (when redirect_url is empty) and for resources in maintenance mode. Null restores the default page.'),
|
||||
new OA\Property(property: 'generate_exact_labels', type: 'boolean'),
|
||||
new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true, description: 'Traefik certificate resolver for generated HTTPS routers (letters, digits, _ and -). Null or empty to use your own certificates. Redeploy resources to apply.'),
|
||||
new OA\Property(property: 'proxy_type', type: 'string', enum: ['traefik', 'caddy', 'nginx', 'none'], description: 'Proxy type (case-insensitive). Changing it starts the new proxy on the server and needs the deploy permission.'),
|
||||
@@ -153,6 +156,7 @@ class ServerProxyController extends Controller
|
||||
new OA\Property(property: 'status', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'redirect_enabled', type: 'boolean'),
|
||||
new OA\Property(property: 'redirect_url', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'proxy_error_page', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'generate_exact_labels', type: 'boolean'),
|
||||
new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'configuration', type: 'string', nullable: true),
|
||||
@@ -178,10 +182,11 @@ class ServerProxyController extends Controller
|
||||
return $return;
|
||||
}
|
||||
|
||||
$allowedFields = ['redirect_enabled', 'redirect_url', 'generate_exact_labels', 'traefik_certificate_resolver', 'proxy_type'];
|
||||
$allowedFields = ['redirect_enabled', 'redirect_url', 'proxy_error_page', 'generate_exact_labels', 'traefik_certificate_resolver', 'proxy_type'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'redirect_enabled' => 'boolean',
|
||||
'redirect_url' => ['nullable', 'string', new SafeExternalUrl],
|
||||
'proxy_error_page' => ['nullable', 'string', proxyPageSizeRule('custom error page')],
|
||||
'generate_exact_labels' => 'boolean',
|
||||
'traefik_certificate_resolver' => ['nullable', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN],
|
||||
'proxy_type' => 'string|nullable',
|
||||
@@ -236,6 +241,11 @@ class ServerProxyController extends Controller
|
||||
$redirectChanged = true;
|
||||
}
|
||||
|
||||
if ($request->exists('proxy_error_page')) {
|
||||
$server->proxy_error_page = $request->input('proxy_error_page') ?: null;
|
||||
$redirectChanged = true;
|
||||
}
|
||||
|
||||
if ($redirectChanged) {
|
||||
$server->save();
|
||||
}
|
||||
@@ -314,6 +324,7 @@ class ServerProxyController extends Controller
|
||||
new OA\Property(property: 'status', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'redirect_enabled', type: 'boolean'),
|
||||
new OA\Property(property: 'redirect_url', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'proxy_error_page', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'generate_exact_labels', type: 'boolean'),
|
||||
new OA\Property(property: 'configuration', type: 'string', nullable: true),
|
||||
]
|
||||
|
||||
@@ -705,6 +705,9 @@ class ServersController extends Controller
|
||||
'server_disk_usage_check_frequency' => ['type' => 'string', 'description' => 'Cron expression for disk usage check frequency.'],
|
||||
'server_disk_usage_notification_interval_hours' => ['type' => 'integer', 'description' => 'Minimum hours between high disk usage notifications (1-720). Default: 24.'],
|
||||
'connection_timeout' => ['type' => 'integer', 'description' => 'SSH connection timeout in seconds (1-300). Default: 10.'],
|
||||
'wildcard_domain' => ['type' => 'string', 'nullable' => true, 'description' => 'Wildcard domain (URL) used to generate domains for new resources on this server. Null removes it.'],
|
||||
'server_timezone' => ['type' => 'string', 'description' => 'Timezone of the server, for example Europe/Berlin. Scheduled tasks and backups use it.'],
|
||||
'backup_compression_cpu_percentage' => ['type' => 'integer', 'enum' => [25, 50, 75, 100], 'description' => 'Maximum CPU percentage that backup compression can use. Default: 25.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -742,7 +745,7 @@ class ServersController extends Controller
|
||||
)]
|
||||
public function update_server(Request $request)
|
||||
{
|
||||
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'server_role', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'server_disk_usage_notification_interval_hours', 'connection_timeout', 'is_terminal_enabled'];
|
||||
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'server_role', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'server_disk_usage_notification_interval_hours', 'connection_timeout', 'is_terminal_enabled', 'wildcard_domain', 'server_timezone', 'backup_compression_cpu_percentage'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -772,6 +775,13 @@ class ServersController extends Controller
|
||||
'server_disk_usage_notification_interval_hours' => 'integer|min:1|max:720',
|
||||
'connection_timeout' => 'integer|min:1|max:300',
|
||||
'is_terminal_enabled' => 'boolean|nullable',
|
||||
'wildcard_domain' => 'nullable|url',
|
||||
'server_timezone' => ['string', function (string $attribute, mixed $value, \Closure $fail): void {
|
||||
if (! validate_timezone($value)) {
|
||||
$fail('The server_timezone must be a valid timezone.');
|
||||
}
|
||||
}],
|
||||
'backup_compression_cpu_percentage' => 'integer|in:25,50,75,100',
|
||||
], [
|
||||
...ValidationPatterns::serverUsernameMessages(),
|
||||
'name.not_regex' => 'The name must not contain control characters such as new lines.',
|
||||
@@ -886,10 +896,13 @@ class ServersController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
$advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'server_disk_usage_notification_interval_hours', 'connection_timeout']);
|
||||
$advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'server_disk_usage_notification_interval_hours', 'connection_timeout', 'server_timezone', 'backup_compression_cpu_percentage']);
|
||||
if (! empty($advancedSettings)) {
|
||||
$server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value)));
|
||||
}
|
||||
if ($request->has('wildcard_domain')) {
|
||||
$server->settings()->update(['wildcard_domain' => $request->input('wildcard_domain')]);
|
||||
}
|
||||
|
||||
if ($proxyTypeChanged) {
|
||||
$server->changeProxy($request->proxy_type, async: true);
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Actions\Service\DeployServiceApplication;
|
||||
use App\Actions\Service\RestartServiceApplication;
|
||||
use App\Actions\Service\StopServiceApplication;
|
||||
use App\Actions\Service\UpdateServiceApplicationFromApi;
|
||||
use App\Enums\RedirectTypes;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\ServerSetting;
|
||||
@@ -16,6 +17,7 @@ use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class ServiceApplicationsController extends Controller
|
||||
@@ -263,6 +265,7 @@ class ServiceApplicationsController extends Controller
|
||||
'is_stripprefix_enabled' => new OA\Property(property: 'is_stripprefix_enabled', type: 'boolean', nullable: true),
|
||||
'is_force_https_enabled' => new OA\Property(property: 'is_force_https_enabled', type: 'boolean', nullable: true),
|
||||
'is_permanent_redirect_enabled' => new OA\Property(property: 'is_permanent_redirect_enabled', type: 'boolean', nullable: true, description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'),
|
||||
'redirect' => new OA\Property(property: 'redirect', type: 'string', enum: ['both', 'www', 'non-www'], description: 'How to handle www and non-www domains. "www" redirects non-www to www, "non-www" redirects www to non-www. Both need a www and a non-www domain in url.'),
|
||||
'max_restart_count' => new OA\Property(property: 'max_restart_count', type: 'integer', minimum: 0, nullable: true, description: 'Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.'),
|
||||
]
|
||||
)
|
||||
@@ -339,6 +342,7 @@ class ServiceApplicationsController extends Controller
|
||||
'is_stripprefix_enabled',
|
||||
'is_force_https_enabled',
|
||||
'is_permanent_redirect_enabled',
|
||||
'redirect',
|
||||
'max_restart_count',
|
||||
];
|
||||
|
||||
@@ -356,6 +360,7 @@ class ServiceApplicationsController extends Controller
|
||||
'is_stripprefix_enabled' => 'sometimes|boolean',
|
||||
'is_force_https_enabled' => 'sometimes|boolean',
|
||||
'is_permanent_redirect_enabled' => 'sometimes|boolean',
|
||||
'redirect' => ['sometimes', Rule::enum(RedirectTypes::class)],
|
||||
'max_restart_count' => 'sometimes|integer|min:0',
|
||||
];
|
||||
|
||||
|
||||
@@ -1150,6 +1150,7 @@ class ServicesController extends Controller
|
||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'default' => true, 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -1238,7 +1239,7 @@ class ServicesController extends Controller
|
||||
|
||||
$this->authorize('update', $service);
|
||||
|
||||
if ($request->has('redact_all_env_values_in_logs')) {
|
||||
if ($request->has('redact_all_env_values_in_logs') || $request->has('include_coolify_default_variables')) {
|
||||
$this->authorize('manageEnvironment', $service);
|
||||
}
|
||||
|
||||
@@ -1247,7 +1248,7 @@ class ServicesController extends Controller
|
||||
$this->authorize('deploy', $service);
|
||||
}
|
||||
|
||||
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'redact_all_env_values_in_logs'];
|
||||
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'redact_all_env_values_in_logs', 'include_coolify_default_variables'];
|
||||
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
@@ -1262,6 +1263,7 @@ class ServicesController extends Controller
|
||||
'force_domain_override' => 'boolean',
|
||||
'is_container_label_escape_enabled' => 'boolean',
|
||||
'redact_all_env_values_in_logs' => 'boolean',
|
||||
'include_coolify_default_variables' => 'boolean',
|
||||
];
|
||||
$validationMessages = [
|
||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||
@@ -1332,6 +1334,9 @@ class ServicesController extends Controller
|
||||
if ($request->has('redact_all_env_values_in_logs')) {
|
||||
$service->redact_all_env_values_in_logs = $request->boolean('redact_all_env_values_in_logs');
|
||||
}
|
||||
if ($request->has('include_coolify_default_variables')) {
|
||||
$service->include_coolify_default_variables = $request->boolean('include_coolify_default_variables');
|
||||
}
|
||||
$previousDnsHostnames = $this->serviceApplicationDnsHostnames($service);
|
||||
$service->save();
|
||||
|
||||
|
||||
@@ -204,11 +204,13 @@ function sharedDataApplications(?Application $application = null): array
|
||||
'include_source_commit_in_build' => 'boolean',
|
||||
'is_env_sorting_enabled' => 'boolean',
|
||||
'redact_all_env_values_in_logs' => 'boolean',
|
||||
'include_coolify_default_variables' => 'boolean',
|
||||
'is_pr_deployments_public_enabled' => 'boolean',
|
||||
'is_gzip_enabled' => 'boolean',
|
||||
'is_stripprefix_enabled' => 'boolean',
|
||||
'is_permanent_redirect_enabled' => 'boolean',
|
||||
'traefik_certificate_resolver' => ['nullable', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN],
|
||||
'is_container_label_readonly_enabled' => 'boolean',
|
||||
'is_raw_compose_deployment_enabled' => 'boolean',
|
||||
'is_log_drain_enabled' => 'boolean',
|
||||
'is_gpu_enabled' => 'boolean',
|
||||
@@ -485,6 +487,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||
$request->offsetUnset('inject_build_args_to_dockerfile');
|
||||
$request->offsetUnset('is_env_sorting_enabled');
|
||||
$request->offsetUnset('redact_all_env_values_in_logs');
|
||||
$request->offsetUnset('include_coolify_default_variables');
|
||||
$request->offsetUnset('is_pr_deployments_public_enabled');
|
||||
$request->offsetUnset('stop_grace_period');
|
||||
$request->offsetUnset('docker_images_to_keep');
|
||||
@@ -492,6 +495,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||
$request->offsetUnset('is_stripprefix_enabled');
|
||||
$request->offsetUnset('is_permanent_redirect_enabled');
|
||||
$request->offsetUnset('traefik_certificate_resolver');
|
||||
$request->offsetUnset('is_container_label_readonly_enabled');
|
||||
$request->offsetUnset('is_raw_compose_deployment_enabled');
|
||||
$request->offsetUnset('is_log_drain_enabled');
|
||||
$request->offsetUnset('is_gpu_enabled');
|
||||
|
||||
+155
@@ -808,6 +808,11 @@
|
||||
"type": "string",
|
||||
"description": "The Dockerfile location in the repository."
|
||||
},
|
||||
"dockerfile_target_build": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The Docker build target (stage) in the Dockerfile."
|
||||
},
|
||||
"docker_compose_location": {
|
||||
"type": "string",
|
||||
"description": "The Docker Compose location."
|
||||
@@ -893,6 +898,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
|
||||
},
|
||||
"include_coolify_default_variables": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
|
||||
},
|
||||
"is_pr_deployments_public_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Make pull request deployments public."
|
||||
@@ -927,6 +937,11 @@
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_container_label_readonly_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -1445,6 +1460,11 @@
|
||||
"type": "string",
|
||||
"description": "The Dockerfile location in the repository"
|
||||
},
|
||||
"dockerfile_target_build": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The Docker build target (stage) in the Dockerfile."
|
||||
},
|
||||
"docker_compose_location": {
|
||||
"type": "string",
|
||||
"description": "The Docker Compose location."
|
||||
@@ -1530,6 +1550,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
|
||||
},
|
||||
"include_coolify_default_variables": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
|
||||
},
|
||||
"is_pr_deployments_public_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Make pull request deployments public."
|
||||
@@ -1564,6 +1589,11 @@
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_container_label_readonly_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -2082,6 +2112,11 @@
|
||||
"type": "string",
|
||||
"description": "The Dockerfile location in the repository."
|
||||
},
|
||||
"dockerfile_target_build": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The Docker build target (stage) in the Dockerfile."
|
||||
},
|
||||
"docker_compose_location": {
|
||||
"type": "string",
|
||||
"description": "The Docker Compose location."
|
||||
@@ -2167,6 +2202,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
|
||||
},
|
||||
"include_coolify_default_variables": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
|
||||
},
|
||||
"is_pr_deployments_public_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Make pull request deployments public."
|
||||
@@ -2201,6 +2241,11 @@
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_container_label_readonly_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -2699,6 +2744,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
|
||||
},
|
||||
"include_coolify_default_variables": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
|
||||
},
|
||||
"is_pr_deployments_public_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Make pull request deployments public."
|
||||
@@ -2733,6 +2783,11 @@
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_container_label_readonly_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -3211,6 +3266,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
|
||||
},
|
||||
"include_coolify_default_variables": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
|
||||
},
|
||||
"is_pr_deployments_public_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Make pull request deployments public."
|
||||
@@ -3245,6 +3305,11 @@
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_container_label_readonly_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -3871,6 +3936,11 @@
|
||||
"type": "string",
|
||||
"description": "The Dockerfile location in the repository."
|
||||
},
|
||||
"dockerfile_target_build": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The Docker build target (stage) in the Dockerfile."
|
||||
},
|
||||
"docker_compose_location": {
|
||||
"type": "string",
|
||||
"description": "The Docker Compose location."
|
||||
@@ -3981,6 +4051,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
|
||||
},
|
||||
"include_coolify_default_variables": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
|
||||
},
|
||||
"is_pr_deployments_public_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Make pull request deployments public."
|
||||
@@ -4015,6 +4090,11 @@
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_container_label_readonly_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -7025,6 +7105,11 @@
|
||||
"description": "Whether to save backups to S3",
|
||||
"default": false
|
||||
},
|
||||
"disable_local_backup": {
|
||||
"type": "boolean",
|
||||
"description": "Delete the local backup file after the upload to S3. Only allowed when save_s3 is true.",
|
||||
"default": false
|
||||
},
|
||||
"s3_storage_uuid": {
|
||||
"type": "string",
|
||||
"description": "Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination. Either field is required if save_s3 is true."
|
||||
@@ -7502,6 +7587,14 @@
|
||||
"redact_all_env_values_in_logs": {
|
||||
"type": "boolean",
|
||||
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values, database passwords, and remote secrets are redacted. Requires team admin permissions."
|
||||
},
|
||||
"include_coolify_default_variables": {
|
||||
"type": "boolean",
|
||||
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Restart the database to apply it."
|
||||
},
|
||||
"is_log_drain_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Send the database logs to the log drain of the server. Log drain must be enabled on the server. Restart the database to apply it."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -7652,6 +7745,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Whether data is saved in s3 or not"
|
||||
},
|
||||
"disable_local_backup": {
|
||||
"type": "boolean",
|
||||
"description": "Delete the local backup file after the upload to S3. Only allowed when save_s3 is true. Turning save_s3 off also turns this off."
|
||||
},
|
||||
"s3_storage_uuid": {
|
||||
"type": "string",
|
||||
"description": "Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination; when it is already a destination, it becomes the primary destination and the other destinations stay."
|
||||
@@ -17199,6 +17296,12 @@
|
||||
],
|
||||
"example": "https:\/\/example.com"
|
||||
},
|
||||
"proxy_error_page": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"generate_exact_labels": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
@@ -17274,6 +17377,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"proxy_error_page": {
|
||||
"description": "Custom HTML error page that the proxy shows for unknown hosts (when redirect_url is empty) and for resources in maintenance mode. Null restores the default page.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"generate_exact_labels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -17328,6 +17438,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"proxy_error_page": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"generate_exact_labels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -17442,6 +17558,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"proxy_error_page": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"generate_exact_labels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -18448,6 +18570,25 @@
|
||||
"connection_timeout": {
|
||||
"type": "integer",
|
||||
"description": "SSH connection timeout in seconds (1-300). Default: 10."
|
||||
},
|
||||
"wildcard_domain": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Wildcard domain (URL) used to generate domains for new resources on this server. Null removes it."
|
||||
},
|
||||
"server_timezone": {
|
||||
"type": "string",
|
||||
"description": "Timezone of the server, for example Europe\/Berlin. Scheduled tasks and backups use it."
|
||||
},
|
||||
"backup_compression_cpu_percentage": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
25,
|
||||
50,
|
||||
75,
|
||||
100
|
||||
],
|
||||
"description": "Maximum CPU percentage that backup compression can use. Default: 25."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -18913,6 +19054,15 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"redirect": {
|
||||
"description": "How to handle www and non-www domains. \"www\" redirects non-www to www, \"non-www\" redirects www to non-www. Both need a www and a non-www domain in url.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"both",
|
||||
"www",
|
||||
"non-www"
|
||||
]
|
||||
},
|
||||
"max_restart_count": {
|
||||
"description": "Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.",
|
||||
"type": [
|
||||
@@ -20461,6 +20611,11 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
|
||||
},
|
||||
"include_coolify_default_variables": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
+102
@@ -529,6 +529,10 @@ paths:
|
||||
dockerfile_location:
|
||||
type: string
|
||||
description: 'The Dockerfile location in the repository.'
|
||||
dockerfile_target_build:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The Docker build target (stage) in the Dockerfile.'
|
||||
docker_compose_location:
|
||||
type: string
|
||||
description: 'The Docker Compose location.'
|
||||
@@ -577,6 +581,10 @@ paths:
|
||||
redact_all_env_values_in_logs:
|
||||
type: boolean
|
||||
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
|
||||
include_coolify_default_variables:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
|
||||
is_pr_deployments_public_enabled:
|
||||
type: boolean
|
||||
description: 'Make pull request deployments public.'
|
||||
@@ -604,6 +612,10 @@ paths:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_container_label_readonly_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -950,6 +962,10 @@ paths:
|
||||
dockerfile_location:
|
||||
type: string
|
||||
description: 'The Dockerfile location in the repository'
|
||||
dockerfile_target_build:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The Docker build target (stage) in the Dockerfile.'
|
||||
docker_compose_location:
|
||||
type: string
|
||||
description: 'The Docker Compose location.'
|
||||
@@ -998,6 +1014,10 @@ paths:
|
||||
redact_all_env_values_in_logs:
|
||||
type: boolean
|
||||
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
|
||||
include_coolify_default_variables:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
|
||||
is_pr_deployments_public_enabled:
|
||||
type: boolean
|
||||
description: 'Make pull request deployments public.'
|
||||
@@ -1025,6 +1045,10 @@ paths:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_container_label_readonly_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -1371,6 +1395,10 @@ paths:
|
||||
dockerfile_location:
|
||||
type: string
|
||||
description: 'The Dockerfile location in the repository.'
|
||||
dockerfile_target_build:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The Docker build target (stage) in the Dockerfile.'
|
||||
docker_compose_location:
|
||||
type: string
|
||||
description: 'The Docker Compose location.'
|
||||
@@ -1419,6 +1447,10 @@ paths:
|
||||
redact_all_env_values_in_logs:
|
||||
type: boolean
|
||||
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
|
||||
include_coolify_default_variables:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
|
||||
is_pr_deployments_public_enabled:
|
||||
type: boolean
|
||||
description: 'Make pull request deployments public.'
|
||||
@@ -1446,6 +1478,10 @@ paths:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_container_label_readonly_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -1781,6 +1817,10 @@ paths:
|
||||
redact_all_env_values_in_logs:
|
||||
type: boolean
|
||||
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
|
||||
include_coolify_default_variables:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
|
||||
is_pr_deployments_public_enabled:
|
||||
type: boolean
|
||||
description: 'Make pull request deployments public.'
|
||||
@@ -1808,6 +1848,10 @@ paths:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_container_label_readonly_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -2129,6 +2173,10 @@ paths:
|
||||
redact_all_env_values_in_logs:
|
||||
type: boolean
|
||||
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
|
||||
include_coolify_default_variables:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
|
||||
is_pr_deployments_public_enabled:
|
||||
type: boolean
|
||||
description: 'Make pull request deployments public.'
|
||||
@@ -2156,6 +2204,10 @@ paths:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_container_label_readonly_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -2574,6 +2626,10 @@ paths:
|
||||
dockerfile_location:
|
||||
type: string
|
||||
description: 'The Dockerfile location in the repository.'
|
||||
dockerfile_target_build:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The Docker build target (stage) in the Dockerfile.'
|
||||
docker_compose_location:
|
||||
type: string
|
||||
description: 'The Docker Compose location.'
|
||||
@@ -2640,6 +2696,10 @@ paths:
|
||||
redact_all_env_values_in_logs:
|
||||
type: boolean
|
||||
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
|
||||
include_coolify_default_variables:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
|
||||
is_pr_deployments_public_enabled:
|
||||
type: boolean
|
||||
description: 'Make pull request deployments public.'
|
||||
@@ -2667,6 +2727,10 @@ paths:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_container_label_readonly_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -4519,6 +4583,10 @@ paths:
|
||||
type: boolean
|
||||
description: 'Whether to save backups to S3'
|
||||
default: false
|
||||
disable_local_backup:
|
||||
type: boolean
|
||||
description: 'Delete the local backup file after the upload to S3. Only allowed when save_s3 is true.'
|
||||
default: false
|
||||
s3_storage_uuid:
|
||||
type: string
|
||||
description: 'Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination. Either field is required if save_s3 is true.'
|
||||
@@ -4860,6 +4928,12 @@ paths:
|
||||
redact_all_env_values_in_logs:
|
||||
type: boolean
|
||||
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values, database passwords, and remote secrets are redacted. Requires team admin permissions.'
|
||||
include_coolify_default_variables:
|
||||
type: boolean
|
||||
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Restart the database to apply it.'
|
||||
is_log_drain_enabled:
|
||||
type: boolean
|
||||
description: 'Send the database logs to the log drain of the server. Log drain must be enabled on the server. Restart the database to apply it.'
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
@@ -4958,6 +5032,9 @@ paths:
|
||||
save_s3:
|
||||
type: boolean
|
||||
description: 'Whether data is saved in s3 or not'
|
||||
disable_local_backup:
|
||||
type: boolean
|
||||
description: 'Delete the local backup file after the upload to S3. Only allowed when save_s3 is true. Turning save_s3 off also turns this off.'
|
||||
s3_storage_uuid:
|
||||
type: string
|
||||
description: 'Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination; when it is already a destination, it becomes the primary destination and the other destinations stay.'
|
||||
@@ -10926,6 +11003,7 @@ paths:
|
||||
status: { type: [string, 'null'], example: running }
|
||||
redirect_enabled: { type: boolean, example: true }
|
||||
redirect_url: { type: [string, 'null'], example: 'https://example.com' }
|
||||
proxy_error_page: { type: [string, 'null'] }
|
||||
generate_exact_labels: { type: boolean, example: false }
|
||||
traefik_certificate_resolver: { description: 'Traefik certificate resolver for generated HTTPS routers. Null when the proxy uses its own certificates.', type: [string, 'null'], example: letsencrypt }
|
||||
configuration: { description: 'Docker Compose proxy configuration when stored in the database. Only present with read:sensitive.', type: [string, 'null'] }
|
||||
@@ -10964,6 +11042,9 @@ paths:
|
||||
redirect_url:
|
||||
description: 'Public http(s) redirect URL, or null to clear.'
|
||||
type: [string, 'null']
|
||||
proxy_error_page:
|
||||
description: 'Custom HTML error page that the proxy shows for unknown hosts (when redirect_url is empty) and for resources in maintenance mode. Null restores the default page.'
|
||||
type: [string, 'null']
|
||||
generate_exact_labels:
|
||||
type: boolean
|
||||
traefik_certificate_resolver:
|
||||
@@ -10985,6 +11066,7 @@ paths:
|
||||
status: { type: [string, 'null'] }
|
||||
redirect_enabled: { type: boolean }
|
||||
redirect_url: { type: [string, 'null'] }
|
||||
proxy_error_page: { type: [string, 'null'] }
|
||||
generate_exact_labels: { type: boolean }
|
||||
traefik_certificate_resolver: { type: [string, 'null'] }
|
||||
configuration: { type: [string, 'null'] }
|
||||
@@ -11041,6 +11123,7 @@ paths:
|
||||
status: { type: [string, 'null'] }
|
||||
redirect_enabled: { type: boolean }
|
||||
redirect_url: { type: [string, 'null'] }
|
||||
proxy_error_page: { type: [string, 'null'] }
|
||||
generate_exact_labels: { type: boolean }
|
||||
configuration: { type: [string, 'null'] }
|
||||
type: object
|
||||
@@ -11674,6 +11757,17 @@ paths:
|
||||
connection_timeout:
|
||||
type: integer
|
||||
description: 'SSH connection timeout in seconds (1-300). Default: 10.'
|
||||
wildcard_domain:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Wildcard domain (URL) used to generate domains for new resources on this server. Null removes it.'
|
||||
server_timezone:
|
||||
type: string
|
||||
description: 'Timezone of the server, for example Europe/Berlin. Scheduled tasks and backups use it.'
|
||||
backup_compression_cpu_percentage:
|
||||
type: integer
|
||||
enum: [25, 50, 75, 100]
|
||||
description: 'Maximum CPU percentage that backup compression can use. Default: 25.'
|
||||
type: object
|
||||
responses:
|
||||
'403':
|
||||
@@ -11937,6 +12031,10 @@ paths:
|
||||
is_permanent_redirect_enabled:
|
||||
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
|
||||
type: [boolean, 'null']
|
||||
redirect:
|
||||
description: 'How to handle www and non-www domains. "www" redirects non-www to www, "non-www" redirects www to non-www. Both need a www and a non-www domain in url.'
|
||||
type: string
|
||||
enum: [both, www, non-www]
|
||||
max_restart_count:
|
||||
description: 'Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.'
|
||||
type: [integer, 'null']
|
||||
@@ -12929,6 +13027,10 @@ paths:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
|
||||
include_coolify_default_variables:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
|
||||
@@ -704,3 +704,78 @@ test('application creation rejects an unsafe Traefik certificate resolver', func
|
||||
|
||||
expect(Application::query()->where('git_repository', 'like', '%resolver-test%')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('PATCH /api/v1/applications/{uuid} turns off label management and keeps custom labels with new domains', function () {
|
||||
$this->application->settings->update(['is_container_label_readonly_enabled' => true]);
|
||||
$this->application->update([
|
||||
'fqdn' => 'https://app.example.com',
|
||||
'custom_labels' => base64_encode('sentinel-label=true'),
|
||||
]);
|
||||
|
||||
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||
'is_container_label_readonly_enabled' => false,
|
||||
'domains' => 'https://new.example.com',
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$application = $this->application->fresh();
|
||||
|
||||
expect($application->settings->is_container_label_readonly_enabled)->toBeFalse()
|
||||
->and($application->fqdn)->toBe('https://new.example.com')
|
||||
->and(base64_decode($application->custom_labels))->toBe('sentinel-label=true');
|
||||
});
|
||||
|
||||
test('PATCH /api/v1/applications/{uuid} regenerates labels when label management is turned back on', function () {
|
||||
$this->application->settings->update(['is_container_label_readonly_enabled' => false]);
|
||||
$this->application->update([
|
||||
'fqdn' => 'https://app.example.com',
|
||||
'custom_labels' => base64_encode('sentinel-label=true'),
|
||||
]);
|
||||
|
||||
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||
'is_container_label_readonly_enabled' => true,
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$application = $this->application->fresh();
|
||||
$labels = $application->parseContainerLabels();
|
||||
|
||||
expect($application->settings->is_container_label_readonly_enabled)->toBeTrue()
|
||||
->and($labels)->not->toContain('sentinel-label=true')
|
||||
->and($labels)->toContain('app.example.com');
|
||||
});
|
||||
|
||||
test('application creation keeps custom labels when label management is off', function () {
|
||||
Queue::fake();
|
||||
|
||||
$response = $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->postJson('/api/v1/applications/public', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'git_repository' => 'https://gitlab.com/coolify/readonly-labels-test',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
'domains' => 'https://app.example.com',
|
||||
'custom_labels' => base64_encode('sentinel-label=true'),
|
||||
'is_container_label_readonly_enabled' => false,
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
|
||||
|
||||
expect($application->settings->is_container_label_readonly_enabled)->toBeFalse()
|
||||
->and(base64_decode($application->custom_labels))->toBe('sentinel-label=true');
|
||||
});
|
||||
|
||||
test('rejects an invalid is_container_label_readonly_enabled value', function () {
|
||||
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||
'is_container_label_readonly_enabled' => 'not-a-boolean',
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('is_container_label_readonly_enabled');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
Server::flushIdentityMap();
|
||||
config()->set('app.maintenance.store', 'array');
|
||||
config()->set('app.maintenance.driver', 'file');
|
||||
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$this->team->members()->attach($user->id, ['role' => 'owner']);
|
||||
|
||||
$plainTextToken = Str::random(40);
|
||||
$token = $user->tokens()->create([
|
||||
'name' => 'ui-settings-api-parity-test',
|
||||
'token' => hash('sha256', $plainTextToken),
|
||||
'abilities' => ['*'],
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
$this->headers = ['Authorization' => 'Bearer '.$token->getKey().'|'.$plainTextToken];
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->firstOrFail();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
|
||||
$this->database = StandalonePostgresql::create([
|
||||
'name' => 'db',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'app',
|
||||
'image' => 'postgres:16',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
});
|
||||
|
||||
function parityS3Storage(Team $team): S3Storage
|
||||
{
|
||||
return S3Storage::create([
|
||||
'name' => 's3',
|
||||
'region' => 'us-east-1',
|
||||
'key' => 'key',
|
||||
'secret' => 'secret',
|
||||
'bucket' => 'bucket',
|
||||
'endpoint' => 'https://s3.example.com',
|
||||
'team_id' => $team->id,
|
||||
'is_usable' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
function parityServiceApplication(object $ctx): ServiceApplication
|
||||
{
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $ctx->environment->id,
|
||||
'server_id' => $ctx->server->id,
|
||||
'destination_id' => $ctx->destination->id,
|
||||
'destination_type' => $ctx->destination->getMorphClass(),
|
||||
'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n",
|
||||
]);
|
||||
|
||||
return ServiceApplication::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'web',
|
||||
'service_id' => $service->id,
|
||||
'image' => 'nginx:alpine',
|
||||
]);
|
||||
}
|
||||
|
||||
describe('applications', function () {
|
||||
test('creation accepts dockerfile_target_build', function () {
|
||||
$response = $this->withHeaders($this->headers)->postJson('/api/v1/applications/public', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'git_repository' => 'https://gitlab.com/coolify/target-build-test',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'dockerfile',
|
||||
'ports_exposes' => '3000',
|
||||
'dockerfile_target_build' => 'production',
|
||||
])->assertCreated();
|
||||
|
||||
expect(Application::where('uuid', $response->json('uuid'))->firstOrFail()->dockerfile_target_build)->toBe('production');
|
||||
});
|
||||
|
||||
test('PATCH updates include_coolify_default_variables', function () {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/applications/{$application->uuid}", ['include_coolify_default_variables' => false])
|
||||
->assertOk();
|
||||
|
||||
expect($application->fresh()->settings->include_coolify_default_variables)->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('databases', function () {
|
||||
test('PATCH saves is_log_drain_enabled when the server has a log drain', function () {
|
||||
$this->server->settings->update(['is_logdrain_newrelic_enabled' => true]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/databases/{$this->database->uuid}", ['is_log_drain_enabled' => true])
|
||||
->assertOk();
|
||||
|
||||
expect($this->database->fresh()->is_log_drain_enabled)->toBeTruthy();
|
||||
});
|
||||
|
||||
test('PATCH rejects is_log_drain_enabled when the server has no log drain', function () {
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/databases/{$this->database->uuid}", ['is_log_drain_enabled' => true])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('is_log_drain_enabled');
|
||||
|
||||
expect($this->database->fresh()->is_log_drain_enabled)->toBeFalsy();
|
||||
});
|
||||
|
||||
test('PATCH updates include_coolify_default_variables', function () {
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/databases/{$this->database->uuid}", ['include_coolify_default_variables' => false])
|
||||
->assertOk();
|
||||
|
||||
expect($this->database->fresh()->include_coolify_default_variables)->toBeFalse();
|
||||
});
|
||||
|
||||
test('backup creation accepts disable_local_backup with S3', function () {
|
||||
$s3 = parityS3Storage($this->team);
|
||||
|
||||
$response = $this->withHeaders($this->headers)->postJson("/api/v1/databases/{$this->database->uuid}/backups", [
|
||||
'frequency' => 'daily',
|
||||
'save_s3' => true,
|
||||
's3_storage_uuid' => $s3->uuid,
|
||||
'disable_local_backup' => true,
|
||||
])->assertCreated();
|
||||
|
||||
expect(ScheduledDatabaseBackup::where('uuid', $response->json('uuid'))->firstOrFail()->disable_local_backup)->toBeTruthy();
|
||||
});
|
||||
|
||||
test('backup creation rejects disable_local_backup without S3', function () {
|
||||
$this->withHeaders($this->headers)->postJson("/api/v1/databases/{$this->database->uuid}/backups", [
|
||||
'frequency' => 'daily',
|
||||
'disable_local_backup' => true,
|
||||
])->assertUnprocessable()
|
||||
->assertJsonValidationErrors('disable_local_backup');
|
||||
|
||||
expect(ScheduledDatabaseBackup::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('backup update turns local backups back on when S3 is turned off', function () {
|
||||
$s3 = parityS3Storage($this->team);
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'frequency' => 'daily',
|
||||
'save_s3' => true,
|
||||
's3_storage_id' => $s3->id,
|
||||
'disable_local_backup' => true,
|
||||
'database_id' => $this->database->id,
|
||||
'database_type' => $this->database->getMorphClass(),
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/databases/{$this->database->uuid}/backups/{$backup->uuid}", ['save_s3' => false])
|
||||
->assertOk();
|
||||
|
||||
expect($backup->fresh()->disable_local_backup)->toBeFalsy();
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/databases/{$this->database->uuid}/backups/{$backup->uuid}", ['disable_local_backup' => true])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('disable_local_backup');
|
||||
});
|
||||
});
|
||||
|
||||
describe('services', function () {
|
||||
test('PATCH updates include_coolify_default_variables', function () {
|
||||
$serviceApplication = parityServiceApplication($this);
|
||||
$service = $serviceApplication->service;
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/services/{$service->uuid}", ['include_coolify_default_variables' => false])
|
||||
->assertOk();
|
||||
|
||||
expect($service->fresh()->include_coolify_default_variables)->toBeFalse();
|
||||
});
|
||||
|
||||
test('service application PATCH sets a www redirect when both hosts exist', function () {
|
||||
$serviceApplication = parityServiceApplication($this);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/services/{$serviceApplication->service->uuid}/applications/{$serviceApplication->uuid}", [
|
||||
'url' => 'https://example.com,https://www.example.com',
|
||||
'redirect' => 'www',
|
||||
])->assertOk();
|
||||
|
||||
expect($serviceApplication->fresh()->redirect)->toBe('www');
|
||||
});
|
||||
|
||||
test('service application PATCH rejects a www redirect without a www host', function () {
|
||||
$serviceApplication = parityServiceApplication($this);
|
||||
$serviceApplication->update(['fqdn' => 'https://example.com']);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/services/{$serviceApplication->service->uuid}/applications/{$serviceApplication->uuid}", [
|
||||
'redirect' => 'www',
|
||||
])->assertUnprocessable()
|
||||
->assertJsonValidationErrors('redirect');
|
||||
|
||||
expect($serviceApplication->fresh()->redirect)->not->toBe('www');
|
||||
});
|
||||
|
||||
test('service application PATCH rejects an unknown redirect', function () {
|
||||
$serviceApplication = parityServiceApplication($this);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/services/{$serviceApplication->service->uuid}/applications/{$serviceApplication->uuid}", [
|
||||
'redirect' => 'sometimes',
|
||||
])->assertUnprocessable()
|
||||
->assertJsonValidationErrors('redirect');
|
||||
});
|
||||
});
|
||||
|
||||
describe('servers', function () {
|
||||
test('PATCH updates wildcard domain, timezone and backup compression', function () {
|
||||
$this->withHeaders($this->headers)->patchJson("/api/v1/servers/{$this->server->uuid}", [
|
||||
'wildcard_domain' => 'https://apps.example.com',
|
||||
'server_timezone' => 'Europe/Berlin',
|
||||
'backup_compression_cpu_percentage' => 50,
|
||||
])->assertCreated();
|
||||
|
||||
expect($this->server->settings->fresh())
|
||||
->wildcard_domain->toBe('https://apps.example.com')
|
||||
->server_timezone->toBe('Europe/Berlin')
|
||||
->backup_compression_cpu_percentage->toBe(50);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/servers/{$this->server->uuid}", ['wildcard_domain' => null])
|
||||
->assertCreated();
|
||||
|
||||
expect($this->server->settings->fresh()->wildcard_domain)->toBeNull();
|
||||
});
|
||||
|
||||
test('PATCH rejects invalid server settings', function (string $field, mixed $value) {
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/servers/{$this->server->uuid}", [$field => $value])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors($field);
|
||||
})->with([
|
||||
'wildcard domain' => ['wildcard_domain', 'not a url'],
|
||||
'timezone' => ['server_timezone', 'Mars/Olympus'],
|
||||
'compression' => ['backup_compression_cpu_percentage', 30],
|
||||
]);
|
||||
|
||||
test('proxy PATCH sets and clears the custom error page', function () {
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/servers/{$this->server->uuid}/proxy", ['proxy_error_page' => '<h1>Not here</h1>'])
|
||||
->assertOk()
|
||||
->assertJsonPath('proxy_error_page', '<h1>Not here</h1>');
|
||||
|
||||
expect($this->server->fresh()->proxy_error_page)->toBe('<h1>Not here</h1>');
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/servers/{$this->server->uuid}/proxy", ['proxy_error_page' => null])
|
||||
->assertOk();
|
||||
|
||||
expect($this->server->fresh()->proxy_error_page)->toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user