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}}'], +]);