From 67693acce72a237574a85649022ee604186cf6db Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:14:39 +0200 Subject: [PATCH] fix(parser): preserve file volume state (#10843) --- bootstrap/helpers/parsers.php | 31 +--- tests/Feature/FileStorageParserStateTest.php | 171 +++++++++++++++++++ 2 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 tests/Feature/FileStorageParserStateTest.php diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 6632e1fd5..ea99e9993 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -370,8 +370,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $pullRequestId = $pull_request_id; $isPullRequest = $pullRequestId == 0 ? false : true; $server = data_get($resource, 'destination.server'); - $fileStorages = $resource->fileStorages(); - try { $yaml = Yaml::parse($compose); } catch (Exception) { @@ -703,14 +701,11 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $source = $parsed['source']; $target = $parsed['target']; // Mode is available in $parsed['mode'] if needed - $foundConfig = $fileStorages->whereMountPath($target)->first(); + $foundConfig = $originalResource->fileStorages()->whereMountPath($target)->first(); if (sourceIsLocal($source)) { $type = str('bind'); if ($foundConfig) { - $contentNotNull_temp = data_get($foundConfig, 'content'); - if ($contentNotNull_temp) { - $content = $contentNotNull_temp; - } + $content = data_get($foundConfig, 'content'); $isDirectory = data_get($foundConfig, 'is_directory'); } else { // By default, we cannot determine if the bind is a directory or not, so we set it to directory @@ -756,12 +751,9 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int } } - $foundConfig = $fileStorages->whereMountPath($target)->first(); + $foundConfig = $originalResource->fileStorages()->whereMountPath($target)->first(); if ($foundConfig) { - $contentNotNull_temp = data_get($foundConfig, 'content'); - if ($contentNotNull_temp) { - $content = $contentNotNull_temp; - } + $content = data_get($foundConfig, 'content'); $isDirectory = data_get($foundConfig, 'is_directory'); } else { // if isDirectory is not set (or false) & content is also not set, we assume it is a directory @@ -2070,7 +2062,6 @@ function serviceParser(Service $resource): Collection 'service_id' => $resource->id, ]); } - $fileStorages = $savedService->fileStorages(); if ($savedService->image !== $image) { $savedService->image = $image; $savedService->save(); @@ -2090,14 +2081,11 @@ function serviceParser(Service $resource): Collection $source = $parsed['source']; $target = $parsed['target']; // Mode is available in $parsed['mode'] if needed - $foundConfig = $fileStorages->whereMountPath($target)->first(); + $foundConfig = $originalResource->fileStorages()->whereMountPath($target)->first(); if (sourceIsLocal($source)) { $type = str('bind'); if ($foundConfig) { - $contentNotNull_temp = data_get($foundConfig, 'content'); - if ($contentNotNull_temp) { - $content = $contentNotNull_temp; - } + $content = data_get($foundConfig, 'content'); $isDirectory = data_get($foundConfig, 'is_directory'); } else { // By default, we cannot determine if the bind is a directory or not, so we set it to directory @@ -2143,12 +2131,9 @@ function serviceParser(Service $resource): Collection } } - $foundConfig = $fileStorages->whereMountPath($target)->first(); + $foundConfig = $originalResource->fileStorages()->whereMountPath($target)->first(); if ($foundConfig) { - $contentNotNull_temp = data_get($foundConfig, 'content'); - if ($contentNotNull_temp) { - $content = $contentNotNull_temp; - } + $content = data_get($foundConfig, 'content'); $isDirectory = data_get($foundConfig, 'is_directory'); } else { // if isDirectory is not set (or false) & content is also not set, we assume it is a directory diff --git a/tests/Feature/FileStorageParserStateTest.php b/tests/Feature/FileStorageParserStateTest.php new file mode 100644 index 000000000..bb39ad1d4 --- /dev/null +++ b/tests/Feature/FileStorageParserStateTest.php @@ -0,0 +1,171 @@ +create(); + $server = Server::factory()->create(['team_id' => $team->id]); + $destination = StandaloneDocker::where('server_id', $server->id)->first(); + $project = Project::factory()->create(['team_id' => $team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + + $this->destination = $destination; + $this->environment = $environment; +}); + +function makeComposeApplication(string $dockerComposeRaw): Application +{ + return Application::factory()->create([ + 'environment_id' => test()->environment->id, + 'destination_id' => test()->destination->id, + 'destination_type' => test()->destination->getMorphClass(), + 'build_pack' => 'dockercompose', + 'docker_compose_raw' => $dockerComposeRaw, + ]); +} + +/** + * @return array{0: Service, 1: ServiceApplication} + */ +function makeComposeService(string $dockerComposeRaw): array +{ + $service = Service::factory()->create([ + 'environment_id' => test()->environment->id, + 'server_id' => test()->destination->server_id, + 'destination_id' => test()->destination->id, + 'destination_type' => test()->destination->getMorphClass(), + 'docker_compose_raw' => $dockerComposeRaw, + ]); + + $serviceApplication = ServiceApplication::create([ + 'name' => 'app', + 'service_id' => $service->id, + ]); + + return [$service, $serviceApplication]; +} + +function seedFileVolume($resource, string $baseDir, string $fileName, string $mountPath, string $content): void +{ + LocalFileVolume::create([ + 'fs_path' => "{$baseDir}/{$fileName}", + 'mount_path' => $mountPath, + 'content' => $content, + 'is_directory' => false, + 'resource_id' => $resource->id, + 'resource_type' => $resource->getMorphClass(), + ]); +} + +it('preserves existing application file volume content when reparsing compose bind mounts', function () { + $application = makeComposeApplication(TWO_FILE_COMPOSE); + $baseDir = application_configuration_dir()."/{$application->uuid}"; + + seedFileVolume($application, $baseDir, 'wg_config.conf', '/app/ps/wg0.conf', 'test-conf'); + seedFileVolume($application, $baseDir, 'override_trays.json', '/app/ps/override_tray.json', '0'); + + applicationParser($application); + + $fileVolume = $application->fileStorages()->where('mount_path', '/app/ps/override_tray.json')->first(); + + expect($fileVolume->content)->toBe('0') + ->and($fileVolume->is_directory)->toBeFalse(); +}); + +it('keeps existing application file volumes as files when content is empty', function () { + $application = makeComposeApplication(TWO_FILE_COMPOSE); + $baseDir = application_configuration_dir()."/{$application->uuid}"; + + seedFileVolume($application, $baseDir, 'wg_config.conf', '/app/ps/wg0.conf', 'test-conf'); + seedFileVolume($application, $baseDir, 'override_trays.json', '/app/ps/override_tray.json', ''); + + applicationParser($application); + + $fileVolume = $application->fileStorages()->where('mount_path', '/app/ps/override_tray.json')->first(); + + expect($fileVolume->content)->toBe('') + ->and($fileVolume->is_directory)->toBeFalse(); +}); + +it('defaults new application bind mounts to directories', function () { + $application = makeComposeApplication(DATA_DIR_COMPOSE); + + applicationParser($application); + + $fileVolume = $application->fileStorages()->where('mount_path', '/app/data')->first(); + + expect($fileVolume->content)->toBeNull() + ->and($fileVolume->is_directory)->toBeTrue(); +}); + +it('preserves existing service file volume content when reparsing compose bind mounts', function () { + [$service, $serviceApplication] = makeComposeService(TWO_FILE_COMPOSE); + $baseDir = service_configuration_dir()."/{$service->uuid}"; + + seedFileVolume($serviceApplication, $baseDir, 'wg_config.conf', '/app/ps/wg0.conf', 'test-conf'); + seedFileVolume($serviceApplication, $baseDir, 'override_trays.json', '/app/ps/override_tray.json', '0'); + + serviceParser($service); + + $fileVolume = $serviceApplication->fileStorages()->where('mount_path', '/app/ps/override_tray.json')->first(); + + expect($fileVolume->content)->toBe('0') + ->and($fileVolume->is_directory)->toBeFalse(); +}); + +it('keeps existing service file volumes as files when content is empty', function () { + [$service, $serviceApplication] = makeComposeService(TWO_FILE_COMPOSE); + $baseDir = service_configuration_dir()."/{$service->uuid}"; + + seedFileVolume($serviceApplication, $baseDir, 'wg_config.conf', '/app/ps/wg0.conf', 'test-conf'); + seedFileVolume($serviceApplication, $baseDir, 'override_trays.json', '/app/ps/override_tray.json', ''); + + serviceParser($service); + + $fileVolume = $serviceApplication->fileStorages()->where('mount_path', '/app/ps/override_tray.json')->first(); + + expect($fileVolume->content)->toBe('') + ->and($fileVolume->is_directory)->toBeFalse(); +}); + +it('defaults new service bind mounts to directories', function () { + [$service, $serviceApplication] = makeComposeService(DATA_DIR_COMPOSE); + + serviceParser($service); + + $fileVolume = $serviceApplication->fileStorages()->where('mount_path', '/app/data')->first(); + + expect($fileVolume->content)->toBeNull() + ->and($fileVolume->is_directory)->toBeTrue(); +});