mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(compose): warn about legacy external volumes and list external volumes
Legacy Compose applications keep the old name of an external volume. The deployment log now tells the user why and how to move to the external volume. The storage page shows external volumes as a read-only list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2c6e221db5
commit
862e021f2d
@@ -402,6 +402,36 @@ function legacyApplicationComposeVolumeName(Application $resource, string $sourc
|
||||
return $resource->compose_parsing_version === '2' ? "{$resource->uuid}-{$name}" : $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a warning when a legacy Compose application (parser version 1 or 2) does not use an
|
||||
* external volume as written. These parsers keep the old volume name (see
|
||||
* legacyApplicationComposeVolumeName()), so the resource keeps its data. The parser version 1 keeps
|
||||
* the name of a production volume, so it uses the external volume and gets no warning.
|
||||
*
|
||||
* The volume names are not validated here, so that the legacy parsers keep their old behavior. The
|
||||
* warning shows the Docker volume name only when it is a literal, valid Docker volume name.
|
||||
*
|
||||
* @param iterable<array-key, mixed> $topLevelVolumes the top-level volumes as declared in the Compose file
|
||||
*/
|
||||
function warnLegacyApplicationComposeExternalVolume(Application $resource, iterable $topLevelVolumes, string $source, int $pull_request_id): void
|
||||
{
|
||||
$declaration = collect($topLevelVolumes)->get($source);
|
||||
if (! isComposeExternalVolume($declaration)) {
|
||||
return;
|
||||
}
|
||||
$keptName = legacyApplicationComposeVolumeName($resource, $source, $pull_request_id);
|
||||
if ($keptName === $source) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dockerVolume = data_get($declaration, 'name') ?? data_get($declaration, 'external.name') ?? $source;
|
||||
$external = ! is_string($dockerVolume) || $dockerVolume === $source || preg_match(ValidationPatterns::VOLUME_NAME_PATTERN, $dockerVolume) !== 1
|
||||
? 'external'
|
||||
: "external (Docker volume '{$dockerVolume}')";
|
||||
|
||||
$resource->addComposeVolumeWarning("Volume '{$source}' is declared as {$external}, but this application uses an old Compose parser, so Coolify uses '{$keptName}'. To use the external volume, copy your data into it, then clone this application or create it again.");
|
||||
}
|
||||
|
||||
/**
|
||||
* The warning for an external volume that the resource does not use yet (see useComposeExternalVolumeAsWritten()).
|
||||
* The volume names are validated and contain no secrets.
|
||||
@@ -454,6 +484,70 @@ function composeExternalVolumeDockerNames(?string $compose): array
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* The external volumes that the services of a Compose file mount, one entry for each volume and
|
||||
* service. Returns an empty list for a Compose file that is empty or not valid YAML.
|
||||
*
|
||||
* @return list<array{key: string, dockerName: string, service: string, mountPaths: list<string>}>
|
||||
*/
|
||||
function composeExternalVolumeMounts(?string $compose): array
|
||||
{
|
||||
if (blank($compose)) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
$yaml = Yaml::parse($compose);
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
$topLevelVolumes = data_get($yaml, 'volumes');
|
||||
$services = data_get($yaml, 'services');
|
||||
if (! is_array($topLevelVolumes) || ! is_array($services)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$mounts = [];
|
||||
foreach ($services as $serviceName => $service) {
|
||||
$serviceVolumes = data_get($service, 'volumes');
|
||||
if (! is_array($serviceVolumes)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($serviceVolumes as $volume) {
|
||||
$parsed = match (true) {
|
||||
is_string($volume) => parseDockerVolumeString($volume),
|
||||
is_array($volume) => $volume,
|
||||
default => [],
|
||||
};
|
||||
// parseDockerVolumeString() returns Stringable values.
|
||||
$source = data_get($parsed, 'source');
|
||||
$target = data_get($parsed, 'target');
|
||||
if (! (is_scalar($source) || $source instanceof Stringable) || ! (is_scalar($target) || $target instanceof Stringable)) {
|
||||
continue;
|
||||
}
|
||||
$source = (string) $source;
|
||||
$target = (string) $target;
|
||||
$declaration = $topLevelVolumes[$source] ?? null;
|
||||
if (! isComposeExternalVolume($declaration)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dockerName = data_get($declaration, 'name') ?? data_get($declaration, 'external.name') ?? $source;
|
||||
$mountKey = $source."\0".$serviceName;
|
||||
$mounts[$mountKey] ??= [
|
||||
'key' => $source,
|
||||
'dockerName' => is_scalar($dockerName) ? (string) $dockerName : $source,
|
||||
'service' => (string) $serviceName,
|
||||
'mountPaths' => [],
|
||||
];
|
||||
if (! in_array($target, $mounts[$mountKey]['mountPaths'], true)) {
|
||||
$mounts[$mountKey]['mountPaths'][] = $target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($mounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* The key and the name of an external volume must be literal Docker volume names. Docker Compose
|
||||
* can resolve variables in `name:`, but then only the deployment `.env` sets which existing volume
|
||||
|
||||
@@ -3393,8 +3393,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$server = $resource->destination->server;
|
||||
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
|
||||
// Legacy Compose applications (parser versions 1 and 2) never stored their volumes, so Coolify
|
||||
// cannot tell which external volume already holds data. They keep the old volume names.
|
||||
$externalTopLevelVolumes = collect([]);
|
||||
// cannot tell which external volume already holds data. They keep the old volume names, and
|
||||
// warnLegacyApplicationComposeExternalVolume() tells the user about it.
|
||||
$declaredTopLevelVolumes = collect($topLevelVolumes->all());
|
||||
if ($pull_request_id !== 0) {
|
||||
$topLevelVolumes = collect([]);
|
||||
}
|
||||
@@ -3427,7 +3428,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
if ($pull_request_id !== 0) {
|
||||
$definedNetwork = collect(["{$resource->uuid}-$pull_request_id"]);
|
||||
}
|
||||
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $externalTopLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $server, $pull_request_id, $preview_id) {
|
||||
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $declaredTopLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $server, $pull_request_id, $preview_id) {
|
||||
$serviceVolumes = collect(data_get($service, 'volumes', []));
|
||||
$servicePorts = collect(data_get($service, 'ports', []));
|
||||
$serviceNetworks = collect(data_get($service, 'networks', []));
|
||||
@@ -3466,7 +3467,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$containerName = "$serviceName-$baseName";
|
||||
if ($resource->compose_parsing_version === '1') {
|
||||
if (count($serviceVolumes) > 0) {
|
||||
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $externalTopLevelVolumes, $pull_request_id) {
|
||||
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $declaredTopLevelVolumes, $pull_request_id) {
|
||||
if (is_string($volume)) {
|
||||
$volume = str($volume);
|
||||
if ($volume->contains(':') && ! $volume->startsWith('/')) {
|
||||
@@ -3484,9 +3485,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$name = addPreviewDeploymentSuffix($name, $pull_request_id);
|
||||
}
|
||||
$volume = str("$name:$mount");
|
||||
} elseif (useComposeExternalVolumeAsWritten($resource, $resource, $externalTopLevelVolumes, $name->value(), legacyApplicationComposeVolumeName($resource, $name->value(), $pull_request_id))) {
|
||||
// An external volume is used as written.
|
||||
} else {
|
||||
warnLegacyApplicationComposeExternalVolume($resource, $declaredTopLevelVolumes, $name->value(), $pull_request_id);
|
||||
if ($pull_request_id !== 0) {
|
||||
$name = addPreviewDeploymentSuffix($name, $pull_request_id);
|
||||
$volume = str("$name:$mount");
|
||||
@@ -3553,10 +3553,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
} else {
|
||||
data_set($volume, 'source', $source.':'.$target);
|
||||
}
|
||||
} elseif (useComposeExternalVolumeAsWritten($resource, $resource, $externalTopLevelVolumes, (string) $source, legacyApplicationComposeVolumeName($resource, (string) $source, $pull_request_id))) {
|
||||
// An external volume is used as written.
|
||||
data_set($volume, 'source', $source.':'.$target.($read_only ? ':ro' : ''));
|
||||
} else {
|
||||
warnLegacyApplicationComposeExternalVolume($resource, $declaredTopLevelVolumes, (string) $source, $pull_request_id);
|
||||
if ($pull_request_id !== 0) {
|
||||
$source = addPreviewDeploymentSuffix($source, $pull_request_id);
|
||||
}
|
||||
@@ -3595,7 +3593,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
}
|
||||
} elseif ($resource->compose_parsing_version === '2') {
|
||||
if (count($serviceVolumes) > 0) {
|
||||
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $externalTopLevelVolumes, $pull_request_id) {
|
||||
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $declaredTopLevelVolumes, $pull_request_id) {
|
||||
if (is_string($volume)) {
|
||||
$volume = str($volume);
|
||||
if ($volume->contains(':') && ! $volume->startsWith('/')) {
|
||||
@@ -3613,9 +3611,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$name = addPreviewDeploymentSuffix($name, $pull_request_id);
|
||||
}
|
||||
$volume = str("$name:$mount");
|
||||
} elseif (useComposeExternalVolumeAsWritten($resource, $resource, $externalTopLevelVolumes, $name->value(), legacyApplicationComposeVolumeName($resource, $name->value(), $pull_request_id))) {
|
||||
// An external volume is used as written.
|
||||
} else {
|
||||
warnLegacyApplicationComposeExternalVolume($resource, $declaredTopLevelVolumes, $name->value(), $pull_request_id);
|
||||
if ($pull_request_id !== 0) {
|
||||
$uuid = $resource->uuid;
|
||||
$name = $uuid.'-'.addPreviewDeploymentSuffix($name, $pull_request_id);
|
||||
@@ -3684,10 +3681,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
} else {
|
||||
data_set($volume, 'source', $source.':'.$target);
|
||||
}
|
||||
} elseif (useComposeExternalVolumeAsWritten($resource, $resource, $externalTopLevelVolumes, (string) $source, legacyApplicationComposeVolumeName($resource, (string) $source, $pull_request_id))) {
|
||||
// An external volume is used as written.
|
||||
data_set($volume, 'source', $source.':'.$target.($read_only ? ':ro' : ''));
|
||||
} else {
|
||||
warnLegacyApplicationComposeExternalVolume($resource, $declaredTopLevelVolumes, (string) $source, $pull_request_id);
|
||||
if ($pull_request_id === 0) {
|
||||
$source = $uuid."-$source";
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user