Merge remote-tracking branch 'origin/next' into fix/rollback-uses-correct-commit

This commit is contained in:
Andras Bacsai committed 2026-02-27 23:24:08 +01:00
commit 530037c213
53 files changed
+1788 -298

No files matched your search

+3 -1
View File
@@ -30,12 +30,14 @@ class InstallDocker
);
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
$base64Cert = base64_encode($serverCert->ssl_certificate);
$commands = collect([
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
"rm -rf $caCertPath/coolify-ca.crt",
"echo '{$serverCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
"echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
remote_process($commands, $server);
+1 -1
View File
@@ -40,7 +40,7 @@ class Kernel extends ConsoleKernel
}
// $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
$this->scheduleInstance->command('cleanup:redis')->weekly();
$this->scheduleInstance->command('cleanup:redis --clear-locks')->daily();
if (isDev()) {
// Instance Jobs
@@ -1002,7 +1002,7 @@ class ApplicationsController extends Controller
if ($return instanceof \Illuminate\Http\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', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled'];
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled'];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
@@ -2460,7 +2460,7 @@ class ApplicationsController extends Controller
$this->authorize('update', $application);
$server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
$validationRules = [
'name' => 'string|max:255',
@@ -290,9 +290,12 @@ class ServersController extends Controller
}
$uuid = $request->get('uuid');
if ($uuid) {
$domains = Application::getDomainsByUuid($uuid);
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $uuid)->first();
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
return response()->json(serializeApiResponse($domains));
return response()->json(serializeApiResponse($application->fqdns));
}
$projects = Project::where('team_id', $teamId)->get();
$domains = collect();
+62 -22
View File
@@ -686,8 +686,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Inject build arguments after build subcommand if not using build secrets
if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
$build_args_string = $this->build_args->implode(' ');
// Escape single quotes for bash -c context used by executeInDocker
$build_args_string = str_replace("'", "'\\''", $build_args_string);
// Inject build args right after 'build' subcommand (not at the end)
$original_command = $build_command;
@@ -699,9 +697,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), 'hidden' => true],
);
try {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), 'hidden' => true],
);
} catch (\RuntimeException $e) {
if (str_contains($e->getMessage(), "matching `'") || str_contains($e->getMessage(), 'unexpected EOF')) {
throw new DeploymentException("Custom build command failed due to shell syntax error. Please check your command for special characters (like unmatched quotes): {$this->docker_compose_custom_build_command}");
}
throw $e;
}
} else {
$command = "{$this->coolify_variables} docker compose";
// Prepend DOCKER_BUILDKIT=1 if BuildKit is supported
@@ -718,8 +724,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
$build_args_string = $this->build_args->implode(' ');
// Escape single quotes for bash -c context used by executeInDocker
$build_args_string = str_replace("'", "'\\''", $build_args_string);
$command .= " {$build_args_string}";
$this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.');
}
@@ -765,9 +769,18 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
$this->write_deployment_configurations();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$start_command}"), 'hidden' => true],
);
try {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$start_command}"), 'hidden' => true],
);
} catch (\RuntimeException $e) {
if (str_contains($e->getMessage(), "matching `'") || str_contains($e->getMessage(), 'unexpected EOF')) {
throw new DeploymentException("Custom start command failed due to shell syntax error. Please check your command for special characters (like unmatched quotes): {$this->docker_compose_custom_start_command}");
}
throw $e;
}
} else {
$this->write_deployment_configurations();
$this->docker_compose_location = '/docker-compose.yaml';
@@ -1797,7 +1810,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$counter = 1;
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
$healthcheckLabel = $this->application->health_check_type === 'cmd' ? 'Healthcheck command' : 'Healthcheck URL';
$this->application_deployment_queue->addLogEntry("{$healthcheckLabel} (inside the container): {$this->full_healthcheck_url}");
}
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
$sleeptime = 0;
@@ -2755,29 +2769,55 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function generate_healthcheck_commands()
{
// Handle CMD type healthcheck
if ($this->application->health_check_type === 'cmd' && ! empty($this->application->health_check_command)) {
$this->full_healthcheck_url = $this->application->health_check_command;
return $this->application->health_check_command;
}
// HTTP type healthcheck (default)
if (! $this->application->health_check_port) {
$health_check_port = $this->application->ports_exposes_array[0];
$health_check_port = (int) $this->application->ports_exposes_array[0];
} else {
$health_check_port = $this->application->health_check_port;
$health_check_port = (int) $this->application->health_check_port;
}
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
$health_check_port = 80;
}
if ($this->application->health_check_path) {
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}";
$generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || exit 1",
];
$method = $this->sanitizeHealthCheckValue($this->application->health_check_method, '/^[A-Z]+$/', 'GET');
$scheme = $this->sanitizeHealthCheckValue($this->application->health_check_scheme, '/^https?$/', 'http');
$host = $this->sanitizeHealthCheckValue($this->application->health_check_host, '/^[a-zA-Z0-9.\-_]+$/', 'localhost');
$path = $this->application->health_check_path
? $this->sanitizeHealthCheckValue($this->application->health_check_path, '#^[a-zA-Z0-9/\-_.~%]+$#', '/')
: null;
$url = escapeshellarg("{$scheme}://{$host}:{$health_check_port}".($path ?? '/'));
$method = escapeshellarg($method);
if ($path) {
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$scheme}://{$host}:{$health_check_port}{$path}";
} else {
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/";
$generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || exit 1",
];
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$scheme}://{$host}:{$health_check_port}/";
}
$generated_healthchecks_commands = [
"curl -s -X {$method} -f {$url} > /dev/null || wget -q -O- {$url} > /dev/null || exit 1",
];
return implode(' ', $generated_healthchecks_commands);
}
private function sanitizeHealthCheckValue(string $value, string $pattern, string $default): string
{
if (preg_match($pattern, $value)) {
return $value;
}
return $default;
}
private function pull_latest_image($image)
{
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
+42
View File
@@ -15,7 +15,9 @@ use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
class ScheduledJobManager implements ShouldQueue
{
@@ -54,6 +56,11 @@ class ScheduledJobManager implements ShouldQueue
*/
public function middleware(): array
{
// Self-healing: clear any stale lock before WithoutOverlapping tries to acquire it.
// Stale locks (TTL = -1) can occur during upgrades, Redis restarts, or edge cases.
// @see https://github.com/coollabsio/coolify/issues/8327
self::clearStaleLockIfPresent();
return [
(new WithoutOverlapping('scheduled-job-manager'))
->expireAfter(90) // Lock expires after 90s to handle high-load environments with many tasks
@@ -61,6 +68,34 @@ class ScheduledJobManager implements ShouldQueue
];
}
/**
* Clear a stale WithoutOverlapping lock if it has no TTL (TTL = -1).
*
* This provides continuous self-healing since it runs every time the job is dispatched.
* Stale locks permanently block all scheduled job executions with no user-visible error.
*/
private static function clearStaleLockIfPresent(): void
{
try {
$cachePrefix = config('cache.prefix', '');
$lockKey = $cachePrefix.'laravel-queue-overlap:'.self::class.':scheduled-job-manager';
$ttl = Redis::connection('default')->ttl($lockKey);
if ($ttl === -1) {
Redis::connection('default')->del($lockKey);
Log::channel('scheduled')->warning('Cleared stale ScheduledJobManager lock', [
'lock_key' => $lockKey,
]);
}
} catch (\Throwable $e) {
// Never let lock cleanup failure prevent the job from running
Log::channel('scheduled-errors')->error('Failed to check/clear stale lock', [
'error' => $e->getMessage(),
]);
}
}
public function handle(): void
{
// Freeze the execution time at the start of the job
@@ -108,6 +143,13 @@ class ScheduledJobManager implements ShouldQueue
'dispatched' => $this->dispatchedCount,
'skipped' => $this->skippedCount,
]);
// Write heartbeat so the UI can detect when the scheduler has stopped
try {
Cache::put('scheduled-job-manager:heartbeat', now()->toIso8601String(), 300);
} catch (\Throwable) {
// Non-critical; don't let heartbeat failure affect the job
}
}
private function processScheduledBackups(): void
+29 -10
View File
@@ -16,19 +16,25 @@ class HealthChecks extends Component
#[Validate(['boolean'])]
public bool $healthCheckEnabled = false;
#[Validate(['string'])]
#[Validate(['string', 'in:http,cmd'])]
public string $healthCheckType = 'http';
#[Validate(['nullable', 'required_if:healthCheckType,cmd', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'])]
public ?string $healthCheckCommand = null;
#[Validate(['required', 'string', 'in:GET,HEAD,POST,OPTIONS'])]
public string $healthCheckMethod;
#[Validate(['string'])]
#[Validate(['required', 'string', 'in:http,https'])]
public string $healthCheckScheme;
#[Validate(['string'])]
#[Validate(['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'])]
public string $healthCheckHost;
#[Validate(['nullable', 'string'])]
#[Validate(['nullable', 'integer', 'min:1', 'max:65535'])]
public ?string $healthCheckPort = null;
#[Validate(['string'])]
#[Validate(['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'])]
public string $healthCheckPath;
#[Validate(['integer'])]
@@ -54,12 +60,14 @@ class HealthChecks extends Component
protected $rules = [
'healthCheckEnabled' => 'boolean',
'healthCheckPath' => 'string',
'healthCheckPort' => 'nullable|string',
'healthCheckHost' => 'string',
'healthCheckMethod' => 'string',
'healthCheckType' => 'string|in:http,cmd',
'healthCheckCommand' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
'healthCheckPath' => ['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
'healthCheckPort' => 'nullable|integer|min:1|max:65535',
'healthCheckHost' => ['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
'healthCheckMethod' => 'required|string|in:GET,HEAD,POST,OPTIONS',
'healthCheckReturnCode' => 'integer',
'healthCheckScheme' => 'string',
'healthCheckScheme' => 'required|string|in:http,https',
'healthCheckResponseText' => 'nullable|string',
'healthCheckInterval' => 'integer|min:1',
'healthCheckTimeout' => 'integer|min:1',
@@ -81,6 +89,8 @@ class HealthChecks extends Component
// Sync to model
$this->resource->health_check_enabled = $this->healthCheckEnabled;
$this->resource->health_check_type = $this->healthCheckType;
$this->resource->health_check_command = $this->healthCheckCommand;
$this->resource->health_check_method = $this->healthCheckMethod;
$this->resource->health_check_scheme = $this->healthCheckScheme;
$this->resource->health_check_host = $this->healthCheckHost;
@@ -98,6 +108,8 @@ class HealthChecks extends Component
} else {
// Sync from model
$this->healthCheckEnabled = $this->resource->health_check_enabled;
$this->healthCheckType = $this->resource->health_check_type ?? 'http';
$this->healthCheckCommand = $this->resource->health_check_command;
$this->healthCheckMethod = $this->resource->health_check_method;
$this->healthCheckScheme = $this->resource->health_check_scheme;
$this->healthCheckHost = $this->resource->health_check_host;
@@ -116,9 +128,12 @@ class HealthChecks extends Component
public function instantSave()
{
$this->authorize('update', $this->resource);
$this->validate();
// Sync component properties to model
$this->resource->health_check_enabled = $this->healthCheckEnabled;
$this->resource->health_check_type = $this->healthCheckType;
$this->resource->health_check_command = $this->healthCheckCommand;
$this->resource->health_check_method = $this->healthCheckMethod;
$this->resource->health_check_scheme = $this->healthCheckScheme;
$this->resource->health_check_host = $this->healthCheckHost;
@@ -143,6 +158,8 @@ class HealthChecks extends Component
// Sync component properties to model
$this->resource->health_check_enabled = $this->healthCheckEnabled;
$this->resource->health_check_type = $this->healthCheckType;
$this->resource->health_check_command = $this->healthCheckCommand;
$this->resource->health_check_method = $this->healthCheckMethod;
$this->resource->health_check_scheme = $this->healthCheckScheme;
$this->resource->health_check_host = $this->healthCheckHost;
@@ -171,6 +188,8 @@ class HealthChecks extends Component
// Sync component properties to model
$this->resource->health_check_enabled = $this->healthCheckEnabled;
$this->resource->health_check_type = $this->healthCheckType;
$this->resource->health_check_command = $this->healthCheckCommand;
$this->resource->health_check_method = $this->healthCheckMethod;
$this->resource->health_check_scheme = $this->healthCheckScheme;
$this->resource->health_check_host = $this->healthCheckHost;
@@ -49,9 +49,10 @@ class ResourceOperations extends Component
{
$this->authorize('update', $this->resource);
$new_destination = StandaloneDocker::find($destination_id);
$teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
$new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
if (! $new_destination) {
$new_destination = SwarmDocker::find($destination_id);
$new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
}
if (! $new_destination) {
return $this->addError('destination_id', 'Destination not found.');
@@ -352,7 +353,7 @@ class ResourceOperations extends Component
{
try {
$this->authorize('update', $this->resource);
$new_environment = Environment::findOrFail($environment_id);
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
$this->resource->update([
'environment_id' => $environment_id,
]);
+10 -2
View File
@@ -60,10 +60,16 @@ class Show extends Component
throw new \Exception('Certificate content cannot be empty.');
}
if (! openssl_x509_read($this->certificateContent)) {
$parsedCert = openssl_x509_read($this->certificateContent);
if (! $parsedCert) {
throw new \Exception('Invalid certificate format.');
}
if (! openssl_x509_export($parsedCert, $cleanedCertificate)) {
throw new \Exception('Failed to process certificate.');
}
$this->certificateContent = $cleanedCertificate;
if ($this->caCertificate) {
$this->caCertificate->ssl_certificate = $this->certificateContent;
$this->caCertificate->save();
@@ -114,12 +120,14 @@ class Show extends Component
{
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
$base64Cert = base64_encode($this->certificateContent);
$commands = collect([
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
"rm -rf $caCertPath/coolify-ca.crt",
"echo '{$this->certificateContent}' > $caCertPath/coolify-ca.crt",
"echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
+52
View File
@@ -3,8 +3,13 @@
namespace App\Livewire\Server;
use App\Jobs\DockerCleanupJob;
use App\Models\DockerCleanupExecution;
use App\Models\Server;
use Cron\CronExpression;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -34,6 +39,53 @@ class DockerCleanup extends Component
#[Validate('boolean')]
public bool $disableApplicationImageRetention = false;
#[Computed]
public function isCleanupStale(): bool
{
try {
$lastExecution = DockerCleanupExecution::where('server_id', $this->server->id)
->orderBy('created_at', 'desc')
->first();
if (! $lastExecution) {
return false;
}
$frequency = $this->server->settings->docker_cleanup_frequency ?? '0 0 * * *';
if (isset(VALID_CRON_STRINGS[$frequency])) {
$frequency = VALID_CRON_STRINGS[$frequency];
}
$cron = new CronExpression($frequency);
$now = Carbon::now();
$nextRun = Carbon::parse($cron->getNextRunDate($now));
$afterThat = Carbon::parse($cron->getNextRunDate($nextRun));
$intervalMinutes = $nextRun->diffInMinutes($afterThat);
$threshold = max($intervalMinutes * 2, 10);
return Carbon::parse($lastExecution->created_at)->diffInMinutes($now) > $threshold;
} catch (\Throwable) {
return false;
}
}
#[Computed]
public function lastExecutionTime(): ?string
{
return DockerCleanupExecution::where('server_id', $this->server->id)
->orderBy('created_at', 'desc')
->first()
?->created_at
?->diffForHumans();
}
#[Computed]
public function isSchedulerHealthy(): bool
{
return Cache::get('scheduled-job-manager:heartbeat') !== null;
}
public function mount(string $server_uuid)
{
try {
+2 -10
View File
@@ -61,6 +61,8 @@ use Visus\Cuid2\Cuid2;
'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
'health_check_type' => ['type' => 'string', 'description' => 'Health check type: http or cmd.', 'enum' => ['http', 'cmd']],
'health_check_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check command for CMD type.'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
@@ -1961,16 +1963,6 @@ class Application extends BaseModel
}
}
public static function getDomainsByUuid(string $uuid): array
{
$application = self::where('uuid', $uuid)->first();
if ($application) {
return $application->fqdns;
}
return [];
}
public function getLimits(): array
{
+1 -1
View File
@@ -237,7 +237,7 @@ class PrivateKey extends BaseModel
$testSuccess = $disk->put($testFilename, 'test');
if (! $testSuccess) {
throw new \Exception('SSH keys storage directory is not writable');
throw new \Exception('SSH keys storage directory is not writable. Run on the host: sudo chown -R 9999 /data/coolify/ssh && sudo chmod -R 700 /data/coolify/ssh && docker restart coolify');
}
// Clean up test file
+3 -1
View File
@@ -1452,12 +1452,14 @@ $schema://$host {
$certificateContent = $caCertificate->ssl_certificate;
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
$base64Cert = base64_encode($certificateContent);
$commands = collect([
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
"rm -rf $caCertPath/coolify-ca.crt",
"echo '{$certificateContent}' > $caCertPath/coolify-ca.crt",
"echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
+4 -8
View File
@@ -37,8 +37,7 @@ class StandaloneDockerPolicy
*/
public function update(User $user, StandaloneDocker $standaloneDocker): bool
{
// return $user->isAdmin() && $user->teams->contains('id', $standaloneDocker->server->team_id);
return true;
return $user->teams->contains('id', $standaloneDocker->server->team_id);
}
/**
@@ -46,8 +45,7 @@ class StandaloneDockerPolicy
*/
public function delete(User $user, StandaloneDocker $standaloneDocker): bool
{
// return $user->isAdmin() && $user->teams->contains('id', $standaloneDocker->server->team_id);
return true;
return $user->teams->contains('id', $standaloneDocker->server->team_id);
}
/**
@@ -55,8 +53,7 @@ class StandaloneDockerPolicy
*/
public function restore(User $user, StandaloneDocker $standaloneDocker): bool
{
// return false;
return true;
return false;
}
/**
@@ -64,7 +61,6 @@ class StandaloneDockerPolicy
*/
public function forceDelete(User $user, StandaloneDocker $standaloneDocker): bool
{
// return false;
return true;
return false;
}
}
+4 -8
View File
@@ -37,8 +37,7 @@ class SwarmDockerPolicy
*/
public function update(User $user, SwarmDocker $swarmDocker): bool
{
// return $user->isAdmin() && $user->teams->contains('id', $swarmDocker->server->team_id);
return true;
return $user->teams->contains('id', $swarmDocker->server->team_id);
}
/**
@@ -46,8 +45,7 @@ class SwarmDockerPolicy
*/
public function delete(User $user, SwarmDocker $swarmDocker): bool
{
// return $user->isAdmin() && $user->teams->contains('id', $swarmDocker->server->team_id);
return true;
return $user->teams->contains('id', $swarmDocker->server->team_id);
}
/**
@@ -55,8 +53,7 @@ class SwarmDockerPolicy
*/
public function restore(User $user, SwarmDocker $swarmDocker): bool
{
// return false;
return true;
return false;
}
/**
@@ -64,7 +61,6 @@ class SwarmDockerPolicy
*/
public function forceDelete(User $user, SwarmDocker $swarmDocker): bool
{
// return false;
return true;
return false;
}
}