diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 43ba58e59..431dc18ec 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -822,10 +822,10 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int } elseif ($type->value() === 'volume') { if ($topLevel->get('volumes')->has($source->value())) { $temp = $topLevel->get('volumes')->get($source->value()); - if (data_get($temp, 'driver_opts.type') === 'cifs') { - continue; - } - if (data_get($temp, 'driver_opts.type') === 'nfs') { + if (data_get($temp, 'driver_opts.type') === 'cifs' || data_get($temp, 'driver_opts.type') === 'nfs') { + dispatch(new ServerFilesFromServerJob($originalResource)); + $volumesParsed->put($index, $volume); + continue; } } @@ -868,6 +868,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $volumesParsed->put($index, $volume); } } + $volumesParsed = $volumesParsed->values(); if ($depends_on?->count() > 0) { if ($isPullRequest) { @@ -2045,10 +2046,10 @@ function serviceParser(Service $resource): Collection } elseif ($type->value() === 'volume') { if ($topLevel->get('volumes')->has($source->value())) { $temp = $topLevel->get('volumes')->get($source->value()); - if (data_get($temp, 'driver_opts.type') === 'cifs') { - continue; - } - if (data_get($temp, 'driver_opts.type') === 'nfs') { + if (data_get($temp, 'driver_opts.type') === 'cifs' || data_get($temp, 'driver_opts.type') === 'nfs') { + dispatch(new ServerFilesFromServerJob($originalResource)); + $volumesParsed->put($index, $volume); + continue; } } @@ -2088,6 +2089,7 @@ function serviceParser(Service $resource): Collection $volumesParsed->put($index, $volume); } } + $volumesParsed = $volumesParsed->values(); if (! $use_network_mode) { if ($topLevel->get('networks')?->count() > 0) { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 2173e7619..d61daa028 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1608,7 +1608,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } elseif ($type->value() === 'volume') { if ($topLevelVolumes->has($source->value())) { $v = $topLevelVolumes->get($source->value()); - if (data_get($v, 'driver_opts.type') === 'cifs') { + if (data_get($v, 'driver_opts.type') === 'cifs' || data_get($v, 'driver_opts.type') === 'nfs') { return $volume; } } @@ -2177,7 +2177,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $volume = str("$name:$mount"); if ($topLevelVolumes->has($name)) { $v = $topLevelVolumes->get($name); - if (data_get($v, 'driver_opts.type') === 'cifs') { + if (data_get($v, 'driver_opts.type') === 'cifs' || data_get($v, 'driver_opts.type') === 'nfs') { // Do nothing } else { if (is_null(data_get($v, 'name'))) { @@ -2193,7 +2193,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } else { if ($topLevelVolumes->has($name->value())) { $v = $topLevelVolumes->get($name->value()); - if (data_get($v, 'driver_opts.type') === 'cifs') { + if (data_get($v, 'driver_opts.type') === 'cifs' || data_get($v, 'driver_opts.type') === 'nfs') { // Do nothing } else { if (is_null(data_get($v, 'name'))) { @@ -2250,7 +2250,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if (! str($source)->startsWith('/')) { if ($topLevelVolumes->has($source)) { $v = $topLevelVolumes->get($source); - if (data_get($v, 'driver_opts.type') === 'cifs') { + if (data_get($v, 'driver_opts.type') === 'cifs' || data_get($v, 'driver_opts.type') === 'nfs') { // Do nothing } else { if (is_null(data_get($v, 'name'))) { @@ -2302,7 +2302,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $volume = str("$name:$mount"); if ($topLevelVolumes->has($name)) { $v = $topLevelVolumes->get($name); - if (data_get($v, 'driver_opts.type') === 'cifs') { + if (data_get($v, 'driver_opts.type') === 'cifs' || data_get($v, 'driver_opts.type') === 'nfs') { // Do nothing } else { if (is_null(data_get($v, 'name'))) { @@ -2321,7 +2321,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $volume = str("$name:$mount"); if ($topLevelVolumes->has($name->value())) { $v = $topLevelVolumes->get($name->value()); - if (data_get($v, 'driver_opts.type') === 'cifs') { + if (data_get($v, 'driver_opts.type') === 'cifs' || data_get($v, 'driver_opts.type') === 'nfs') { // Do nothing } else { if (is_null(data_get($v, 'name'))) { @@ -2378,7 +2378,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if (! str($source)->startsWith('/')) { if ($topLevelVolumes->has($source)) { $v = $topLevelVolumes->get($source); - if (data_get($v, 'driver_opts.type') === 'cifs') { + if (data_get($v, 'driver_opts.type') === 'cifs' || data_get($v, 'driver_opts.type') === 'nfs') { // Do nothing } else { if (is_null(data_get($v, 'name'))) { diff --git a/tests/Unit/NfsVolumePreservationTest.php b/tests/Unit/NfsVolumePreservationTest.php new file mode 100644 index 000000000..e949f22b9 --- /dev/null +++ b/tests/Unit/NfsVolumePreservationTest.php @@ -0,0 +1,106 @@ +put(0, 'uuid_test-cache:/cache'); + $volumesParsed->put(3, '/data/coolify/applications/uuid/configs/config.yml:/config.yml'); + $volumesParsed->put(4, '/etc/localtime:/etc/localtime:ro'); + + $payload = ['volumes' => $volumesParsed->toArray()]; + $yaml = Yaml::dump($payload, 10, 2); + + expect($yaml)->toContain('0:'); + expect($yaml)->not->toContain('- uuid_test-cache'); +}); + +test('sequential collection keys produce valid YAML list for volumes', function () { + $volumesParsed = collect([]); + $volumesParsed->put(0, 'uuid_test-cache:/cache'); + $volumesParsed->put(3, '/data/coolify/applications/uuid/configs/config.yml:/config.yml'); + $volumesParsed->put(4, '/etc/localtime:/etc/localtime:ro'); + + $volumesParsed = $volumesParsed->values(); + + $payload = ['volumes' => $volumesParsed->toArray()]; + $yaml = Yaml::dump($payload, 10, 2); + + expect($yaml)->toContain("- 'uuid_test-cache:/cache'"); + expect($yaml)->toContain("- '/data/coolify/applications/uuid/configs/config.yml:/config.yml'"); + expect($yaml)->toContain("- '/etc/localtime:/etc/localtime:ro'"); + expect($yaml)->not->toContain('0:'); +}); + +test('NFS volumes are preserved when mixed with persistent and bind volumes', function () { + $volumes = [ + 'test_cache:/cache', + 'test_stores:/stores', + 'test_backups:/backups', + './configs/config.yml:/config.yml', + '/etc/localtime:/etc/localtime:ro', + ]; + + $topLevelVolumes = collect([ + 'test_cache' => ['driver' => 'local'], + 'test_stores' => ['driver_opts' => ['type' => 'nfs', 'o' => 'addr=192.168.68.61', 'device' => ':/mnt/stores']], + 'test_backups' => ['driver_opts' => ['type' => 'nfs', 'o' => 'addr=192.168.68.61', 'device' => ':/mnt/backups']], + ]); + + $volumesParsed = collect([]); + + foreach ($volumes as $index => $volume) { + $parsed = parseDockerVolumeString($volume); + $source = $parsed['source']; + + if (sourceIsLocal($source)) { + $volumesParsed->put($index, 'transformed_bind_'.$index); + } else { + if ($topLevelVolumes->has($source->value())) { + $temp = $topLevelVolumes->get($source->value()); + if (data_get($temp, 'driver_opts.type') === 'cifs' || data_get($temp, 'driver_opts.type') === 'nfs') { + $volumesParsed->put($index, $volume); + + continue; + } + } + $volumesParsed->put($index, 'uuid_transformed_'.$source->value().':'.$parsed['target']->value()); + } + } + + $volumesParsed = $volumesParsed->values(); + + expect($volumesParsed)->toHaveCount(5); + expect($volumesParsed->contains('test_stores:/stores'))->toBeTrue(); + expect($volumesParsed->contains('test_backups:/backups'))->toBeTrue(); + expect(array_keys($volumesParsed->toArray()))->toBe([0, 1, 2, 3, 4]); + + $yaml = Yaml::dump(['volumes' => $volumesParsed->toArray()], 10, 2); + expect($yaml)->toContain("- 'test_stores:/stores'"); + expect($yaml)->toContain("- 'test_backups:/backups'"); + expect($yaml)->not->toContain('0:'); + expect($yaml)->not->toContain('1:'); +}); + +test('CIFS volumes are preserved the same way as NFS volumes', function () { + $topLevelVolumes = collect([ + 'cifs_share' => ['driver_opts' => ['type' => 'cifs', 'o' => 'addr=192.168.1.1', 'device' => '//server/share']], + ]); + + $volumesParsed = collect([]); + $volume = 'cifs_share:/data'; + $parsed = parseDockerVolumeString($volume); + $source = $parsed['source']; + + if ($topLevelVolumes->has($source->value())) { + $temp = $topLevelVolumes->get($source->value()); + if (data_get($temp, 'driver_opts.type') === 'cifs' || data_get($temp, 'driver_opts.type') === 'nfs') { + $volumesParsed->put(0, $volume); + } + } + + $volumesParsed = $volumesParsed->values(); + + expect($volumesParsed)->toHaveCount(1); + expect($volumesParsed->first())->toBe('cifs_share:/data'); +});