mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 10:05:47 -05:00
fix(storage): support compound modes and safe volume index rollback
Parse comma-separated Docker volume modes correctly, preserve colons in volume sources, and prevent unsafe rollback when sibling mounts conflict.
This commit is contained in:
@@ -380,13 +380,15 @@ class LocalFileVolume extends BaseModel
|
||||
foreach ($services as $service) {
|
||||
foreach (data_get($service, 'volumes', []) as $volume) {
|
||||
if (is_string($volume)) {
|
||||
$parts = explode(':', $volume);
|
||||
if (count($parts) < 2) {
|
||||
$parsedVolume = parseDockerVolumeString($volume);
|
||||
$source = $parsedVolume['source'];
|
||||
$target = $parsedVolume['target'];
|
||||
if ($source === null || $target === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->matchesComposeVolume($parts[0], $parts[1], $mainDirectory)) {
|
||||
$options = array_map('trim', explode(',', $parts[2] ?? ''));
|
||||
if ($this->matchesComposeVolume($source->value(), $target->value(), $mainDirectory)) {
|
||||
$options = array_map('trim', explode(',', $parsedVolume['mode']?->value() ?? ''));
|
||||
|
||||
return in_array('ro', $options, true);
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ function parseDockerVolumeString(string $volumeString): array
|
||||
$possibleMode = substr($remaining, $lastColon + 1);
|
||||
$validModes = ['ro', 'rw', 'z', 'Z', 'rslave', 'rprivate', 'rshared', 'slave', 'private', 'shared', 'cached', 'delegated', 'consistent'];
|
||||
|
||||
if (in_array($possibleMode, $validModes)) {
|
||||
if (array_diff(array_map('trim', explode(',', $possibleMode)), $validModes) === []) {
|
||||
$mode = $possibleMode;
|
||||
$target = substr($remaining, 0, $lastColon);
|
||||
} else {
|
||||
@@ -208,7 +208,7 @@ function parseDockerVolumeString(string $volumeString): array
|
||||
// Check if the last part is a valid Docker volume mode
|
||||
$validModes = ['ro', 'rw', 'z', 'Z', 'rslave', 'rprivate', 'rshared', 'slave', 'private', 'shared', 'cached', 'delegated', 'consistent'];
|
||||
|
||||
if (in_array($possibleMode, $validModes)) {
|
||||
if (array_diff(array_map('trim', explode(',', $possibleMode)), $validModes) === []) {
|
||||
// It's a mode
|
||||
// Examples: "gitea:/data:ro" or "./data:/app/data:rw"
|
||||
$mode = $possibleMode;
|
||||
@@ -275,7 +275,7 @@ function parseDockerVolumeString(string $volumeString): array
|
||||
$possibleMode = substr($remaining, $lastColon + 1);
|
||||
$validModes = ['ro', 'rw', 'z', 'Z', 'rslave', 'rprivate', 'rshared', 'slave', 'private', 'shared', 'cached', 'delegated', 'consistent'];
|
||||
|
||||
if (in_array($possibleMode, $validModes)) {
|
||||
if (array_diff(array_map('trim', explode(',', $possibleMode)), $validModes) === []) {
|
||||
$mode = $possibleMode;
|
||||
$target = substr($remaining, 0, $lastColon);
|
||||
} else {
|
||||
|
||||
@@ -35,8 +35,24 @@ return new class extends Migration
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$hasIncompatibleSiblingMounts = DB::table('local_file_volumes')
|
||||
->select(['mount_path', 'resource_id', 'resource_type'])
|
||||
->groupBy(['mount_path', 'resource_id', 'resource_type'])
|
||||
->havingRaw('COUNT(*) > 1')
|
||||
->exists();
|
||||
|
||||
if ($hasIncompatibleSiblingMounts) {
|
||||
throw new RuntimeException(
|
||||
'Cannot roll back the local file volume unique index while sibling file mounts exist.'
|
||||
);
|
||||
}
|
||||
|
||||
Schema::table('local_file_volumes', function (Blueprint $table) {
|
||||
$table->dropUnique('local_file_volumes_source_mount_resource_unique');
|
||||
$table->unique(
|
||||
['mount_path', 'resource_id', 'resource_type'],
|
||||
'local_file_volumes_mount_path_resource_id_resource_type_unique'
|
||||
);
|
||||
$table->dropColumn('fs_path_hash');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,6 +171,20 @@ YAML;
|
||||
expect($volume->isReadOnlyVolume())->toBeTrue();
|
||||
});
|
||||
|
||||
it('isReadOnlyVolume preserves colons in environment variable defaults', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
database:
|
||||
image: 'postgres:alpine'
|
||||
volumes:
|
||||
- '${VOLUME_DB_PATH:-db}:/var/lib/data:ro'
|
||||
YAML;
|
||||
|
||||
$volume = makeReadOnlyVolumeFixture($compose, 'db', '/var/lib/data');
|
||||
|
||||
expect($volume->isReadOnlyVolume())->toBeTrue();
|
||||
});
|
||||
|
||||
it('isReadOnlyVolume disambiguates sibling rows with different :ro flags', function () {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function localFileVolumesUniqueIndexMigration(): object
|
||||
{
|
||||
return require database_path('migrations/2026_08_18_120000_update_local_file_volumes_unique_index.php');
|
||||
}
|
||||
|
||||
it('refuses to roll back when sibling file mounts violate the previous uniqueness contract', function () {
|
||||
$migration = localFileVolumesUniqueIndexMigration();
|
||||
|
||||
foreach (['/host/one', '/host/two'] as $index => $fsPath) {
|
||||
DB::table('local_file_volumes')->insert([
|
||||
'uuid' => "volume-{$index}",
|
||||
'fs_path' => $fsPath,
|
||||
'fs_path_hash' => hash('sha256', $fsPath),
|
||||
'mount_path' => '/container/file',
|
||||
'resource_id' => 123,
|
||||
'resource_type' => 'App\\Models\\Application',
|
||||
]);
|
||||
}
|
||||
|
||||
expect(fn () => $migration->down())
|
||||
->toThrow(RuntimeException::class, 'Cannot roll back');
|
||||
|
||||
expect(Schema::hasColumn('local_file_volumes', 'fs_path_hash'))->toBeTrue();
|
||||
expect(collect(Schema::getIndexes('local_file_volumes'))->pluck('name'))
|
||||
->toContain('local_file_volumes_source_mount_resource_unique');
|
||||
});
|
||||
|
||||
it('restores the previous unique index when rollback data is compatible', function () {
|
||||
$migration = localFileVolumesUniqueIndexMigration();
|
||||
|
||||
$migration->down();
|
||||
|
||||
expect(Schema::hasColumn('local_file_volumes', 'fs_path_hash'))->toBeFalse();
|
||||
expect(collect(Schema::getIndexes('local_file_volumes'))->pluck('name'))
|
||||
->toContain('local_file_volumes_mount_path_resource_id_resource_type_unique');
|
||||
});
|
||||
Reference in New Issue
Block a user