Files
coolify/app/Actions/Shared/EnsureContentFilesOnServer.php
T
Andras BacsaiandClaude Opus 5.5 7bd8baa7f0 fix(multi-server): make applications on several servers consistent
- Stop the application on every server, also when one server is not functional.
- Write file mounts on every server of the application, not only on the main server.
- Deploy the exact commit, rollback flag and image tag of the main deployment to the
  additional servers, so a rollback reverts all servers.
- Pull the pushed image on additional servers for inline Dockerfile applications
  instead of building it again on each server.
- Fail multi-server deployments without a Docker image name, also from webhooks and the API.
- Send one deployment notification after all servers finish (parent_deployment_uuid).
- Block persistent volumes on multi-server applications (API, Livewire, SQLite connect)
  and warn in the deployment log for existing applications that have both.
- Remove dead code in push_to_docker_registry().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:38:57 +02:00

101 lines
3.8 KiB
PHP

<?php
namespace App\Actions\Shared;
use App\Models\LocalFileVolume;
use App\Models\Server;
use Closure;
use Lorisleiva\Actions\Concerns\AsAction;
/**
* `docker compose up` creates a missing bind source as an empty directory. The queued
* ServerStorageSaveJob writes new content files, but it can run after the containers start. This
* action writes each missing content file before `docker compose up`.
*
* It checks all files with one server command. It writes a file only when the path is missing or
* is an empty directory. It does not change a file that exists, and it does not delete a directory
* that is not empty.
*/
class EnsureContentFilesOnServer
{
use AsAction;
/**
* @param iterable<LocalFileVolume> $fileStorages
* @param Closure(string, string): void $log Gets a log line and its type (`stdout` or `stderr`). Log lines never contain file content.
* @return int The number of files that Coolify wrote
*/
public function handle(iterable $fileStorages, Server $server, Closure $log): int
{
$candidates = [];
foreach ($fileStorages as $fileStorage) {
if (! $fileStorage->hasContentToWrite()) {
continue;
}
try {
$candidates[] = [$fileStorage, $fileStorage->contentPathOnServer()];
} catch (\Throwable $e) {
$log('Warning: '.$this->plainText($e->getMessage()), 'stderr');
}
}
if ($candidates === []) {
return 0;
}
$states = LocalFileVolume::remoteFileStates(array_column($candidates, 1), $server);
$toWrite = [];
foreach ($candidates as $index => [$fileStorage, $path]) {
match ($states[$index]) {
'missing', 'empty-directory' => $toWrite[] = [$fileStorage, $path],
'file' => null,
'directory' => $log("Warning: A directory that is not empty is at {$path} on the server. Coolify did not write the configuration file for {$fileStorage->mount_path}. Remove the directory on the server, then start the resource again.", 'stderr'),
'other' => $log("Warning: A symbolic link or a special file is at {$path} on the server. Coolify did not write the configuration file for {$fileStorage->mount_path}.", 'stderr'),
default => $log("Warning: Coolify cannot check the configuration file {$path} on the server.", 'stderr'),
};
}
if ($toWrite === []) {
return 0;
}
$count = count($toWrite);
$log($count === 1 ? 'Writing 1 missing configuration file.' : "Writing {$count} missing configuration files.", 'stdout');
$written = 0;
foreach ($toWrite as [$fileStorage, $path]) {
try {
$fileStorage->saveStorageOnServer($server);
$written++;
} catch (\Throwable $e) {
$log("Warning: Coolify cannot write the configuration file {$path}: ".$this->plainText($e->getMessage()), 'stderr');
}
}
return $written;
}
/**
* Runs the action and returns its log lines as `echo` commands, for a command list that runs
* later in an activity log.
*
* @param iterable<LocalFileVolume> $fileStorages
* @return list<string>
*/
public static function echoCommands(iterable $fileStorages, Server $server): array
{
$commands = [];
static::run($fileStorages, $server, function (string $message) use (&$commands): void {
$commands[] = 'echo '.escapeshellarg($message);
});
return $commands;
}
private function plainText(string $message): string
{
return trim(preg_replace('/\s+/', ' ', strip_tags(str_replace('<br>', ' ', $message))));
}
}