From 4e55c58a2810e83b1370b34e336d168aea3c4026 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:47:41 +0200 Subject: [PATCH] fix(storage): keep long-form bind sources with variable or vault references Docker Compose resolves these values from the .env file, so the parser no longer rejects or misresolves them. Also copy the expected parser fixtures when parser-snapshot-diff compares with a git ref. --- bootstrap/helpers/parsers.php | 6 ++++++ scripts/parser-snapshot-diff | 1 + tests/Feature/ComposeLongBindOptionsTest.php | 11 +++++++++++ 3 files changed, 18 insertions(+) diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 64d16c2c94..8f94775fc3 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -9,6 +9,7 @@ use App\Models\LocalPersistentVolume; use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; +use App\Support\RemoteSecretReferences; use App\Support\ValidationPatterns; use Illuminate\Support\Collection; use Illuminate\Support\Facades\File; @@ -255,6 +256,7 @@ function resolveComposeVolumeSourceDefault(string $source): string /** * Resolves a long-syntax bind source as Docker Compose does with the `.env` file that Coolify writes: * `${VAR:-default}` is the value of VAR when the resource sets it to a non-empty value, else the default. + * A value with a variable or vault reference stays for Docker Compose to resolve. * * @throws Exception If the value of the variable is not a shell-safe path */ @@ -273,6 +275,10 @@ function resolveComposeLongBindSource(string $source, Application|Service $resou if (blank($value)) { return resolveComposeVolumeSourceDefault($source); } + // Docker Compose expands variable references in the `.env` file, and Coolify writes vault secrets to it. + if (str_contains($value, '$') || RemoteSecretReferences::containsReference($value)) { + return $source; + } try { validateShellSafePath($value, 'volume source'); diff --git a/scripts/parser-snapshot-diff b/scripts/parser-snapshot-diff index fc2e1e5d07..225ac371d4 100755 --- a/scripts/parser-snapshot-diff +++ b/scripts/parser-snapshot-diff @@ -71,6 +71,7 @@ case "$mode" in cp -r "$repo/templates/compose" "$base/templates/compose" mkdir -p "$base/tests/Fixtures/parser" cp "$repo"/tests/Fixtures/parser/*.yaml "$base/tests/Fixtures/parser/" + cp -r "$repo/tests/Fixtures/parser/expected" "$base/tests/Fixtures/parser/expected" cp "$repo/tests/Feature/ServiceParserReferenceTest.php" "$base/tests/Feature/" parse_into "$base" "$work/base-expected" "$mode" ;; diff --git a/tests/Feature/ComposeLongBindOptionsTest.php b/tests/Feature/ComposeLongBindOptionsTest.php index df7ba9200c..7f65bb8ca7 100644 --- a/tests/Feature/ComposeLongBindOptionsTest.php +++ b/tests/Feature/ComposeLongBindOptionsTest.php @@ -224,3 +224,14 @@ it('uses the value that the resource sets for a ${VAR:-default} long-form bind s it('rejects an unsafe value for a ${VAR:-default} long-form bind source', function (string $kind) { parseLongBind($kind, longBindCompose("'\${AO_CONFIG_FILE:-./agent-orchestrator.yaml}'", LONG_BIND_READ_ONLY_OPTIONS), ['AO_CONFIG_FILE' => '/opt/x;touch /tmp/pwned']); })->with(['service', 'application'])->throws(Exception::class, 'the value of AO_CONFIG_FILE is not a safe path'); + +it('keeps a ${VAR:-default} long-form bind source for Docker Compose when the value of the variable is not a plain path', function (string $kind, string $value) { + $source = '${AO_CONFIG_FILE:-./agent-orchestrator.yaml}'; + $result = parseLongBind($kind, longBindCompose("'{$source}'", LONG_BIND_READ_ONLY_OPTIONS), ['BASE_DIR' => '/srv', 'AO_CONFIG_FILE' => $value]); + + expect($result['volume']['source'])->toBe($source); +})->with(['service', 'application'])->with([ + 'braced variable reference' => ['${BASE_DIR}/ao.yaml'], + 'variable reference' => ['$BASE_DIR/ao.yaml'], + 'vault reference' => ['{{vault.AO_CONFIG_FILE}}'], +]);