mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(upgrade): remove disk space checks that block Coolify updates
This commit is contained in:
@@ -3,8 +3,6 @@
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Server\UpgradeSkippedLowDiskSpace;
|
||||
use App\Services\CoolifyUpgradeDiskSpace;
|
||||
use App\Services\CoolifyUpgradeStatus;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -21,7 +19,7 @@ class UpdateCoolify
|
||||
|
||||
public ?string $currentVersion = null;
|
||||
|
||||
public function handle($manual_update = false, bool $skipDiskSpaceCheck = false)
|
||||
public function handle($manual_update = false)
|
||||
{
|
||||
if (isDev()) {
|
||||
Sleep::for(10)->seconds();
|
||||
@@ -112,16 +110,12 @@ class UpdateCoolify
|
||||
);
|
||||
}
|
||||
|
||||
if (! $skipDiskSpaceCheck && ! $this->ensureEnoughDiskSpace($manual_update)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->update($skipDiskSpaceCheck);
|
||||
$this->update();
|
||||
$settings->new_version_available = false;
|
||||
$settings->save();
|
||||
}
|
||||
|
||||
private function update(bool $skipDiskSpaceCheck)
|
||||
private function update()
|
||||
{
|
||||
$this->ensureNoUpgradeIsRunning();
|
||||
|
||||
@@ -129,54 +123,15 @@ class UpdateCoolify
|
||||
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
|
||||
$registryUrl = coolifyRegistryUrl();
|
||||
|
||||
$upgradeCommand = 'bash /data/coolify/source/upgrade.sh '.
|
||||
escapeshellarg($this->latestVersion).' '.
|
||||
escapeshellarg($latestHelperImageVersion).' '.
|
||||
escapeshellarg($registryUrl);
|
||||
if ($skipDiskSpaceCheck) {
|
||||
// Arguments 4 and 5: do not skip the backup, skip the disk space check.
|
||||
$upgradeCommand .= " 'false' 'true'";
|
||||
}
|
||||
|
||||
remote_process([
|
||||
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
|
||||
$upgradeCommand,
|
||||
'bash /data/coolify/source/upgrade.sh '.
|
||||
escapeshellarg($this->latestVersion).' '.
|
||||
escapeshellarg($latestHelperImageVersion).' '.
|
||||
escapeshellarg($registryUrl),
|
||||
], $this->server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic updates are skipped and the team is notified. Manual updates fail with an error.
|
||||
*/
|
||||
private function ensureEnoughDiskSpace(bool $manualUpdate): bool
|
||||
{
|
||||
$availableGb = $this->availableDiskSpaceGb();
|
||||
if (! CoolifyUpgradeDiskSpace::isLow($availableGb)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::warning('Upgrade skipped because of low disk space', [
|
||||
'target_version' => $this->latestVersion,
|
||||
'available_gb' => $availableGb,
|
||||
'required_gb' => CoolifyUpgradeDiskSpace::REQUIRED_GB,
|
||||
'manual_update' => $manualUpdate,
|
||||
]);
|
||||
|
||||
if ($manualUpdate) {
|
||||
throw new \Exception(
|
||||
"Not enough free disk space to upgrade: {$availableGb} GB free, ".CoolifyUpgradeDiskSpace::REQUIRED_GB.' GB required.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->server->team?->notify(new UpgradeSkippedLowDiskSpace($this->server, $this->latestVersion, $availableGb));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function availableDiskSpaceGb(): ?float
|
||||
{
|
||||
return app(CoolifyUpgradeDiskSpace::class)->availableGb($this->server);
|
||||
}
|
||||
|
||||
private function ensureNoUpgradeIsRunning(): void
|
||||
{
|
||||
$status = $this->readUpgradeStatus();
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Actions\Server\UpdateCoolify;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Services\CoolifyUpgradeDiskSpace;
|
||||
use App\Services\CoolifyUpgradeStatus;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -64,30 +62,19 @@ class Upgrade extends Component
|
||||
$this->isUpgradeAvailable = $hasNewerVersion && $newVersionAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the upgrade. Without $skipDiskSpaceCheck, the upgrade does not start when the disk is almost full.
|
||||
*
|
||||
* @return array{status: 'started'|'low_disk_space', available_gb?: float|null, required_gb?: int}|null
|
||||
*/
|
||||
public function upgrade(bool $skipDiskSpaceCheck = false)
|
||||
public function upgrade()
|
||||
{
|
||||
try {
|
||||
if (! isInstanceAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
if ($this->updateInProgress) {
|
||||
return null;
|
||||
}
|
||||
if (! $skipDiskSpaceCheck) {
|
||||
$diskSpace = $this->checkDiskSpace();
|
||||
if ($diskSpace['status'] === 'low_disk_space') {
|
||||
return $diskSpace;
|
||||
}
|
||||
return;
|
||||
}
|
||||
$this->updateInProgress = true;
|
||||
dispatch(function () use ($skipDiskSpaceCheck) {
|
||||
dispatch(function () {
|
||||
try {
|
||||
UpdateCoolify::run(manual_update: true, skipDiskSpaceCheck: $skipDiskSpaceCheck);
|
||||
UpdateCoolify::run(manual_update: true);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
@@ -98,50 +85,7 @@ class Upgrade extends Component
|
||||
'resource' => 'instance',
|
||||
'from_version' => config('constants.coolify.version'),
|
||||
'to_version' => get_latest_version_of_coolify(),
|
||||
'skip_disk_space_check' => $skipDiskSpaceCheck,
|
||||
]);
|
||||
|
||||
return ['status' => 'started'];
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{status: 'low_disk_space'|'ok', available_gb: float|null, required_gb: int}
|
||||
*/
|
||||
public function checkDiskSpace(): array
|
||||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$server = Server::find(0);
|
||||
$availableGb = $server ? app(CoolifyUpgradeDiskSpace::class)->availableGb($server) : null;
|
||||
|
||||
return [
|
||||
'status' => CoolifyUpgradeDiskSpace::isLow($availableGb) ? 'low_disk_space' : 'ok',
|
||||
'available_gb' => $availableGb,
|
||||
'required_gb' => CoolifyUpgradeDiskSpace::REQUIRED_GB,
|
||||
];
|
||||
}
|
||||
|
||||
public function runDockerCleanup()
|
||||
{
|
||||
try {
|
||||
if (! isInstanceAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
$server = Server::findOrFail(0);
|
||||
DockerCleanupJob::dispatch($server, true);
|
||||
auditLog('ui.server.docker_cleanup_started', [
|
||||
'team_id' => $server->team_id,
|
||||
'server_uuid' => $server->uuid,
|
||||
'server_name' => $server->name,
|
||||
'delete_unused_volumes' => false,
|
||||
'delete_unused_networks' => false,
|
||||
]);
|
||||
$this->dispatch('success', 'Docker cleanup started. When it is done, click "Check again".');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Server;
|
||||
|
||||
use App\Contracts\ThrottledNotification;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\CustomEmailNotification;
|
||||
use App\Notifications\Dto\DiscordMessage;
|
||||
use App\Notifications\Dto\PushoverMessage;
|
||||
use App\Notifications\Dto\SlackMessage;
|
||||
use App\Services\CoolifyUpgradeDiskSpace;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class UpgradeSkippedLowDiskSpace extends CustomEmailNotification implements ThrottledNotification
|
||||
{
|
||||
public int $requiredGb = CoolifyUpgradeDiskSpace::REQUIRED_GB;
|
||||
|
||||
public function __construct(public Server $server, public string $version, public float $availableGb)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return $notifiable->getEnabledChannels('server_disk_usage');
|
||||
}
|
||||
|
||||
public function throttleSubject(): ?Model
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
|
||||
public function throttleIntervalMinutes(): int
|
||||
{
|
||||
return 24 * 60;
|
||||
}
|
||||
|
||||
private function cleanupUrl(): string
|
||||
{
|
||||
return base_url().'/server/'.$this->server->uuid.'/docker-cleanup';
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage;
|
||||
$mail->subject("Coolify: Automatic update to {$this->version} skipped, not enough free disk space");
|
||||
$mail->view('emails.upgrade-skipped-low-disk-space', [
|
||||
'version' => $this->version,
|
||||
'available_gb' => $this->availableGb,
|
||||
'required_gb' => $this->requiredGb,
|
||||
'cleanup_url' => $this->cleanupUrl(),
|
||||
]);
|
||||
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): DiscordMessage
|
||||
{
|
||||
$message = new DiscordMessage(
|
||||
title: ':warning: Automatic update skipped',
|
||||
description: "Coolify did not update to {$this->version} because the server does not have enough free disk space.",
|
||||
color: DiscordMessage::warningColor(),
|
||||
isCritical: true,
|
||||
);
|
||||
|
||||
$message->addField('Free', "{$this->availableGb} GB", true);
|
||||
$message->addField('Required', "{$this->requiredGb} GB", true);
|
||||
$message->addField('What to do?', '[Docker cleanup]('.$this->cleanupUrl().')', true);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function toTelegram(): array
|
||||
{
|
||||
return [
|
||||
'message' => "Coolify: Automatic update to {$this->version} skipped.\nFree disk space: {$this->availableGb} GB. Required: {$this->requiredGb} GB.\nFree up disk space (for example, run a Docker cleanup): {$this->cleanupUrl()}",
|
||||
];
|
||||
}
|
||||
|
||||
public function toPushover(): PushoverMessage
|
||||
{
|
||||
return new PushoverMessage(
|
||||
title: 'Automatic update skipped',
|
||||
level: 'warning',
|
||||
message: "Coolify did not update to {$this->version} because the server does not have enough free disk space.<br/><br/><b>Free:</b> {$this->availableGb} GB.<br/><b>Required:</b> {$this->requiredGb} GB.",
|
||||
buttons: [
|
||||
'Docker cleanup' => $this->cleanupUrl(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function toSlack(): SlackMessage
|
||||
{
|
||||
$description = "Coolify did not update to {$this->version} because the server does not have enough free disk space.\n";
|
||||
$description .= "Free: {$this->availableGb} GB\n";
|
||||
$description .= "Required: {$this->requiredGb} GB\n\n";
|
||||
$description .= 'Free up disk space (for example, run a Docker cleanup): '.$this->cleanupUrl();
|
||||
|
||||
return new SlackMessage(
|
||||
title: 'Automatic update skipped',
|
||||
description: $description,
|
||||
color: SlackMessage::warningColor()
|
||||
);
|
||||
}
|
||||
|
||||
public function toWebhook(): array
|
||||
{
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Automatic update skipped, not enough free disk space',
|
||||
'event' => 'upgrade_skipped_low_disk_space',
|
||||
'version' => $this->version,
|
||||
'server_name' => $this->server->name,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'available_gb' => $this->availableGb,
|
||||
'required_gb' => $this->requiredGb,
|
||||
'url' => $this->cleanupUrl(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Server;
|
||||
|
||||
/**
|
||||
* An upgrade pulls new images and recreates the Coolify containers. When the disk becomes full
|
||||
* during an upgrade, Coolify can stop working (for example, an empty config cache file).
|
||||
*/
|
||||
class CoolifyUpgradeDiskSpace
|
||||
{
|
||||
public const REQUIRED_GB = 5;
|
||||
|
||||
public const UPGRADE_PATHS = ['/data/coolify'];
|
||||
|
||||
public const DEFAULT_DOCKER_ROOT_DIR = '/var/lib/docker';
|
||||
|
||||
/**
|
||||
* Free space in GB on the Docker data directory and /data/coolify. The smaller value is returned.
|
||||
* Returns null when the free space cannot be measured, so an unknown value never blocks an upgrade.
|
||||
*/
|
||||
public function availableGb(Server $server): ?float
|
||||
{
|
||||
$dockerRootDir = trim((string) instant_remote_process(
|
||||
["docker info --format '{{.DockerRootDir}}' 2>/dev/null || true"],
|
||||
$server,
|
||||
false
|
||||
));
|
||||
|
||||
$paths = collect([...self::UPGRADE_PATHS, $dockerRootDir ?: self::DEFAULT_DOCKER_ROOT_DIR])
|
||||
->map(fn (string $path) => escapeshellarg($path))
|
||||
->implode(' ');
|
||||
|
||||
$output = instant_remote_process(["df -Pk {$paths} 2>/dev/null || true"], $server, false);
|
||||
|
||||
return self::parseDfOutput((string) $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the smallest "Available" column (in 1K blocks) from `df -Pk` output and return it in GB.
|
||||
*/
|
||||
public static function parseDfOutput(string $output): ?float
|
||||
{
|
||||
$availableKb = collect(preg_split('/\R/', trim($output)))
|
||||
->skip(1)
|
||||
->map(fn (string $line) => preg_split('/\s+/', trim($line))[3] ?? null)
|
||||
->filter(fn (?string $value) => $value !== null && ctype_digit($value))
|
||||
->map(fn (string $value) => (int) $value)
|
||||
->min();
|
||||
|
||||
if ($availableKb === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round($availableKb / 1024 / 1024, 1);
|
||||
}
|
||||
|
||||
public static function isLow(?float $availableGb): bool
|
||||
{
|
||||
return $availableGb !== null && $availableGb < self::REQUIRED_GB;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ $sshCommandTimeout = (int) env('SSH_COMMAND_TIMEOUT', 3600);
|
||||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => env('COOLIFY_VERSION') ?: '4.4.4',
|
||||
'version' => env('COOLIFY_VERSION') ?: '4.4.5',
|
||||
'helper_version' => '1.0.17',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'autoupdate' => env('AUTOUPDATE'),
|
||||
|
||||
@@ -25,6 +25,7 @@ services:
|
||||
- ./databases:/var/www/html/storage/app/databases
|
||||
- ./services:/var/www/html/storage/app/services
|
||||
- ./backups:/var/www/html/storage/app/backups
|
||||
- ./images:/var/www/html/storage/app/images
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.4.4"
|
||||
"version": "4.4.5"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.5-rc.1"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<x-emails.layout>
|
||||
Coolify did not update to {{ $version }} because the server does not have enough free disk space ({{ $available_gb }} GB free, {{ $required_gb }} GB required).
|
||||
|
||||
Free up disk space, for example with a [Docker cleanup]({{ $cleanup_url }}). The next automatic update will try again.
|
||||
</x-emails.layout>
|
||||
@@ -137,53 +137,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{-- Low Disk Space View --}}
|
||||
<template x-if="!showProgress && lowDiskSpace">
|
||||
<div class="flex flex-col gap-4">
|
||||
<x-callout type="danger" title="Not enough free disk space">
|
||||
<p>
|
||||
The server has <span class="font-semibold" x-text="availableGb + ' GB'"></span> free.
|
||||
The upgrade needs at least <span class="font-semibold" x-text="requiredGb + ' GB'"></span>.
|
||||
If the disk becomes full during the upgrade, Coolify can stop working.
|
||||
</p>
|
||||
</x-callout>
|
||||
|
||||
<p class="text-[12px] leading-5" style="color: var(--coollabs-subtle)">
|
||||
Run a Docker cleanup to remove unused images, containers, and build cache, then check again.
|
||||
</p>
|
||||
|
||||
<template x-if="confirmOverride">
|
||||
<x-callout type="warning" title="Upgrade anyway?">
|
||||
<p>Only continue if you are sure that the upgrade has enough disk space.</p>
|
||||
</x-callout>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-end gap-2 border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
|
||||
<template x-if="!confirmOverride">
|
||||
<x-forms.button @click="confirmOverride = true" type="button">
|
||||
Upgrade anyway
|
||||
</x-forms.button>
|
||||
</template>
|
||||
<template x-if="confirmOverride">
|
||||
<x-forms.button @click="confirmed(true)" type="button"
|
||||
x-bind:disabled="checkingDiskSpace">
|
||||
Yes, upgrade anyway
|
||||
</x-forms.button>
|
||||
</template>
|
||||
<x-forms.button @click="$wire.runDockerCleanup()" type="button">
|
||||
Run Docker cleanup
|
||||
</x-forms.button>
|
||||
<x-forms.button @click="checkDiskSpaceAgain()" isHighlighted type="button"
|
||||
x-bind:disabled="checkingDiskSpace">
|
||||
<span x-text="checkingDiskSpace ? 'Checking…' : 'Check again'"></span>
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{-- Confirmation View --}}
|
||||
<template x-if="!showProgress && !lowDiskSpace">
|
||||
<template x-if="!showProgress">
|
||||
<div class="flex flex-col gap-4">
|
||||
<x-callout type="warning" title="Caution">
|
||||
<p>Any deployments running during the update process will fail.</p>
|
||||
@@ -205,9 +160,8 @@
|
||||
Simulate
|
||||
</x-forms.button>
|
||||
</template>
|
||||
<x-forms.button @click="confirmed()" isHighlighted type="button"
|
||||
x-bind:disabled="checkingDiskSpace">
|
||||
<span x-text="checkingDiskSpace ? 'Checking disk space…' : 'Upgrade now'"></span>
|
||||
<x-forms.button @click="confirmed" isHighlighted type="button">
|
||||
Upgrade now
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,11 +202,6 @@
|
||||
instanceWentDown: false,
|
||||
devMode: config.devMode || false,
|
||||
simulationInterval: null,
|
||||
lowDiskSpace: false,
|
||||
availableGb: null,
|
||||
requiredGb: null,
|
||||
confirmOverride: false,
|
||||
checkingDiskSpace: false,
|
||||
|
||||
simulateUpgrade() {
|
||||
if (!this.devMode) return;
|
||||
@@ -285,30 +234,13 @@
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
async confirmed(skipDiskSpaceCheck = false) {
|
||||
if (this.checkingDiskSpace) return;
|
||||
this.checkingDiskSpace = true;
|
||||
let result;
|
||||
try {
|
||||
// Trigger server-side upgrade script via Livewire. It does not start when the disk is almost full.
|
||||
result = await this.$wire.upgrade(skipDiskSpaceCheck);
|
||||
} finally {
|
||||
this.checkingDiskSpace = false;
|
||||
}
|
||||
if (result?.status === 'low_disk_space') {
|
||||
this.lowDiskSpace = true;
|
||||
this.availableGb = result.available_gb;
|
||||
this.requiredGb = result.required_gb;
|
||||
return;
|
||||
}
|
||||
if (result?.status !== 'started') return;
|
||||
|
||||
this.lowDiskSpace = false;
|
||||
this.confirmOverride = false;
|
||||
confirmed() {
|
||||
this.showProgress = true;
|
||||
this.currentStep = 1;
|
||||
this.currentStatus = 'Starting upgrade...';
|
||||
this.startTimer();
|
||||
// Trigger server-side upgrade script via Livewire
|
||||
this.$wire.$call('upgrade');
|
||||
// Start client-side status polling
|
||||
this.upgrade();
|
||||
// Prevent accidental navigation during upgrade
|
||||
@@ -319,22 +251,6 @@
|
||||
window.addEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
},
|
||||
|
||||
async checkDiskSpaceAgain() {
|
||||
if (this.checkingDiskSpace) return;
|
||||
this.checkingDiskSpace = true;
|
||||
try {
|
||||
const result = await this.$wire.checkDiskSpace();
|
||||
this.availableGb = result.available_gb;
|
||||
this.requiredGb = result.required_gb;
|
||||
this.lowDiskSpace = result.status === 'low_disk_space';
|
||||
if (!this.lowDiskSpace) {
|
||||
this.confirmOverride = false;
|
||||
}
|
||||
} finally {
|
||||
this.checkingDiskSpace = false;
|
||||
}
|
||||
},
|
||||
|
||||
startTimer() {
|
||||
this.startTime = Date.now();
|
||||
this.elapsedInterval = setInterval(() => {
|
||||
|
||||
@@ -13,8 +13,6 @@ else
|
||||
REGISTRY_URL="docker.io"
|
||||
fi
|
||||
SKIP_BACKUP=${4:-false}
|
||||
SKIP_DISK_SPACE_CHECK=${5:-false}
|
||||
REQUIRED_DISK_SPACE_GB=5
|
||||
STATUS_FILE="/data/coolify/source/.upgrade-status"
|
||||
|
||||
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
||||
@@ -55,24 +53,6 @@ echo "Helper Version: ${LATEST_HELPER_VERSION}" >>"$LOGFILE"
|
||||
echo "Registry URL: ${REGISTRY_URL}" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
|
||||
# Stop before anything changes when the disk is almost full.
|
||||
# A full disk during the upgrade can leave Coolify broken.
|
||||
if [ "$SKIP_DISK_SPACE_CHECK" != "true" ]; then
|
||||
DOCKER_ROOT_DIR=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null)
|
||||
AVAILABLE_KB=$(df -Pk /data/coolify "${DOCKER_ROOT_DIR:-/var/lib/docker}" 2>/dev/null | awk 'NR>1 && $4 ~ /^[0-9]+$/ {print $4}' | sort -n | head -n1)
|
||||
if [ -n "$AVAILABLE_KB" ] && [ "$AVAILABLE_KB" -lt $((REQUIRED_DISK_SPACE_GB * 1024 * 1024)) ]; then
|
||||
AVAILABLE_GB=$(awk -v kb="$AVAILABLE_KB" 'BEGIN {printf "%.1f", kb / 1024 / 1024}')
|
||||
log "ERROR: Not enough free disk space: ${AVAILABLE_GB} GB free, ${REQUIRED_DISK_SPACE_GB} GB required"
|
||||
write_status "error" "Not enough free disk space: ${AVAILABLE_GB} GB free, ${REQUIRED_DISK_SPACE_GB} GB required. Free up disk space and try again."
|
||||
echo " ERROR: Not enough free disk space: ${AVAILABLE_GB} GB free, ${REQUIRED_DISK_SPACE_GB} GB required."
|
||||
echo " Free up disk space (for example: docker system prune) and try again."
|
||||
exit 1
|
||||
fi
|
||||
log "Disk space check passed (${AVAILABLE_KB:-unknown} KB free)"
|
||||
else
|
||||
log "Disk space check skipped"
|
||||
fi
|
||||
|
||||
log_section "Step 1/6: Downloading configuration files"
|
||||
write_status "1" "Downloading configuration files"
|
||||
echo "1/6 Downloading latest configuration files..."
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Livewire\Upgrade;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Services\CoolifyUpgradeDiskSpace;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@@ -104,82 +98,3 @@ it('clears stale upgrade availability when current version is newer than cached
|
||||
|
||||
expect((bool) InstanceSettings::findOrFail(0)->new_version_available)->toBeFalse();
|
||||
});
|
||||
|
||||
function upgradeComponentTestLoginToRootTeam(string $role, ?float $availableGb = null): void
|
||||
{
|
||||
config(['constants.coolify.version' => '4.0.0-beta.998']);
|
||||
Cache::put('coolify:versions:all', ['coolify' => ['v4' => ['version' => '4.0.0-beta.999']]], 3600);
|
||||
InstanceSettings::forceCreate(['id' => 0, 'new_version_available' => true]);
|
||||
$rootTeam = Team::factory()->create(['id' => 0]);
|
||||
Server::forceCreate([
|
||||
'id' => 0,
|
||||
'name' => 'localhost',
|
||||
'ip' => '127.0.0.1',
|
||||
'user' => 'root',
|
||||
'team_id' => 0,
|
||||
'private_key_id' => 1,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$rootTeam->members()->attach($user->id, ['role' => $role]);
|
||||
test()->actingAs($user);
|
||||
session(['currentTeam' => ['id' => 0]]);
|
||||
|
||||
test()->mock(CoolifyUpgradeDiskSpace::class)
|
||||
->shouldReceive('availableGb')
|
||||
->andReturn($availableGb);
|
||||
}
|
||||
|
||||
it('does not start the upgrade when the disk is almost full', function () {
|
||||
upgradeComponentTestLoginToRootTeam('admin', 3.2);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->call('upgrade')
|
||||
->assertReturned(['status' => 'low_disk_space', 'available_gb' => 3.2, 'required_gb' => 5])
|
||||
->assertSet('updateInProgress', false);
|
||||
});
|
||||
|
||||
it('starts the upgrade when the disk has enough free space', function () {
|
||||
upgradeComponentTestLoginToRootTeam('admin', 42.0);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->call('upgrade')
|
||||
->assertReturned(['status' => 'started'])
|
||||
->assertSet('updateInProgress', true);
|
||||
});
|
||||
|
||||
it('starts the upgrade on a low disk when the admin confirms the override', function () {
|
||||
upgradeComponentTestLoginToRootTeam('admin', 3.2);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->call('upgrade', true)
|
||||
->assertReturned(['status' => 'started'])
|
||||
->assertSet('updateInProgress', true);
|
||||
});
|
||||
|
||||
it('starts a docker cleanup on the localhost server for an instance admin', function () {
|
||||
Queue::fake();
|
||||
upgradeComponentTestLoginToRootTeam('admin', 3.2);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->call('runDockerCleanup')
|
||||
->assertDispatched('success');
|
||||
|
||||
Queue::assertPushed(DockerCleanupJob::class, fn (DockerCleanupJob $job) => $job->server->id === 0 && $job->manualCleanup);
|
||||
});
|
||||
|
||||
it('does not let a root team member check disk space, clean up docker, or upgrade', function () {
|
||||
Queue::fake();
|
||||
upgradeComponentTestLoginToRootTeam('member', 3.2);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->call('checkDiskSpace')
|
||||
->assertForbidden();
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->call('runDockerCleanup')
|
||||
->call('upgrade', true)
|
||||
->assertSet('updateInProgress', false);
|
||||
|
||||
Queue::assertNotPushed(DockerCleanupJob::class);
|
||||
});
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Services\CoolifyUpgradeDiskSpace;
|
||||
|
||||
it('returns the smallest free space of all paths in GB', function () {
|
||||
$output = <<<'DF'
|
||||
Filesystem 1024-blocks Used Available Capacity Mounted on
|
||||
/dev/sda1 41152736 20000000 20971520 50% /
|
||||
/dev/sdb1 104857600 90000000 4718592 95% /var/lib/docker
|
||||
DF;
|
||||
|
||||
expect(CoolifyUpgradeDiskSpace::parseDfOutput($output))->toBe(4.5);
|
||||
});
|
||||
|
||||
it('returns null when the df output has no free space values', function (string $output) {
|
||||
expect(CoolifyUpgradeDiskSpace::parseDfOutput($output))->toBeNull();
|
||||
})->with([
|
||||
'empty' => '',
|
||||
'header only' => 'Filesystem 1024-blocks Used Available Capacity Mounted on',
|
||||
'error' => "df: /data/coolify: No such file or directory\nPermission denied",
|
||||
]);
|
||||
|
||||
it('treats only a known value below the minimum as low', function (?float $availableGb, bool $isLow) {
|
||||
expect(CoolifyUpgradeDiskSpace::isLow($availableGb))->toBe($isLow);
|
||||
})->with([
|
||||
'unknown' => [null, false],
|
||||
'below minimum' => [4.9, true],
|
||||
'at minimum' => [5.0, false],
|
||||
'above minimum' => [120.3, false],
|
||||
]);
|
||||
@@ -6,13 +6,11 @@ use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Notifications\Server\UpgradeSkippedLowDiskSpace;
|
||||
use App\Support\RemoteProcessCommand;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Once;
|
||||
@@ -55,48 +53,6 @@ function updateCoolifyTestActionWithStatus(string $status): UpdateCoolify
|
||||
};
|
||||
}
|
||||
|
||||
function updateCoolifyTestActionWithDiskSpace(?float $availableGb): UpdateCoolify
|
||||
{
|
||||
return new class($availableGb) extends UpdateCoolify
|
||||
{
|
||||
public function __construct(private ?float $availableGb) {}
|
||||
|
||||
protected function readUpgradeStatus(): ?string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function availableDiskSpaceGb(): ?float
|
||||
{
|
||||
return $this->availableGb;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function updateCoolifyTestPrepareUpgrade(): void
|
||||
{
|
||||
Queue::fake();
|
||||
config([
|
||||
'app.env' => 'testing',
|
||||
'constants.coolify.version' => '4.0.9',
|
||||
'constants.coolify.helper_version' => '1.0.14',
|
||||
'constants.coolify.upgrade_script_url' => 'https://cdn.example.com/upgrade.sh',
|
||||
'constants.ssh.mux_enabled' => false,
|
||||
]);
|
||||
|
||||
updateCoolifyTestCreateRootServerAndSettings([
|
||||
'is_auto_update_enabled' => true,
|
||||
'new_version_available' => true,
|
||||
'docker_registry_url' => 'docker.io',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.10']],
|
||||
], 200),
|
||||
]);
|
||||
}
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
@@ -407,50 +363,3 @@ it('starts an upgrade when the previous upgrade status is older than the lock ex
|
||||
expect(RemoteProcessCommand::read(Activity::query()->latest('id')->first()))
|
||||
->toContain("bash /data/coolify/source/upgrade.sh '4.0.10'");
|
||||
});
|
||||
|
||||
it('skips an automatic update and notifies the root team when the disk is almost full', function () {
|
||||
updateCoolifyTestPrepareUpgrade();
|
||||
Notification::fake();
|
||||
Team::findOrFail(0)->discordNotificationSettings()->update(['discord_enabled' => true, 'server_disk_usage_discord_notifications' => true]);
|
||||
|
||||
updateCoolifyTestActionWithDiskSpace(3.2)->handle();
|
||||
|
||||
expect(Activity::query()->count())->toBe(0)
|
||||
->and((bool) InstanceSettings::findOrFail(0)->new_version_available)->toBeTrue();
|
||||
Notification::assertSentTo(
|
||||
Team::findOrFail(0),
|
||||
UpgradeSkippedLowDiskSpace::class,
|
||||
fn (UpgradeSkippedLowDiskSpace $notification) => $notification->version === '4.0.10'
|
||||
&& $notification->availableGb === 3.2
|
||||
&& str_contains((string) $notification->toMail()->render(), '3.2 GB free'),
|
||||
);
|
||||
});
|
||||
|
||||
it('fails a manual update when the disk is almost full', function () {
|
||||
updateCoolifyTestPrepareUpgrade();
|
||||
Notification::fake();
|
||||
|
||||
expect(fn () => updateCoolifyTestActionWithDiskSpace(3.2)->handle(manual_update: true))
|
||||
->toThrow(Exception::class, 'Not enough free disk space to upgrade: 3.2 GB free, 5 GB required.');
|
||||
|
||||
expect(Activity::query()->count())->toBe(0);
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
|
||||
it('upgrades when the free disk space cannot be measured', function () {
|
||||
updateCoolifyTestPrepareUpgrade();
|
||||
|
||||
updateCoolifyTestActionWithDiskSpace(null)->handle();
|
||||
|
||||
expect(RemoteProcessCommand::read(Activity::query()->latest('id')->first()))
|
||||
->toEndWith("bash /data/coolify/source/upgrade.sh '4.0.10' '1.0.14' 'docker.io'");
|
||||
});
|
||||
|
||||
it('upgrades and tells the upgrade script to skip its disk space check when the user overrides it', function () {
|
||||
updateCoolifyTestPrepareUpgrade();
|
||||
|
||||
updateCoolifyTestActionWithDiskSpace(3.2)->handle(manual_update: true, skipDiskSpaceCheck: true);
|
||||
|
||||
expect(RemoteProcessCommand::read(Activity::query()->latest('id')->first()))
|
||||
->toEndWith("bash /data/coolify/source/upgrade.sh '4.0.10' '1.0.14' 'docker.io' 'false' 'true'");
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.4.4"
|
||||
"version": "4.4.5"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.5-rc.1"
|
||||
|
||||
Reference in New Issue
Block a user