mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(service): store the Compose default for bare variable references
Closes #12412
This commit is contained in:
@@ -2422,6 +2422,7 @@ function serviceParser(Service $resource): Collection
|
||||
->orWhere('key', 'LIKE', '%}');
|
||||
})
|
||||
->delete();
|
||||
$existingEnvironmentVariableKeys = $resource->environment_variables()->pluck('key');
|
||||
|
||||
$topLevel = collect([
|
||||
'volumes' => collect(data_get($yaml, 'volumes', [])),
|
||||
@@ -3671,6 +3672,7 @@ function serviceParser(Service $resource): Collection
|
||||
data_forget($resource, 'environment_variables_preview');
|
||||
$resource->save();
|
||||
|
||||
fillDockerComposeDefaultsForBareReferences($resource, $existingEnvironmentVariableKeys);
|
||||
syncDockerComposeEnvironmentVariableRequiredState($resource);
|
||||
|
||||
return $topLevel;
|
||||
|
||||
@@ -5580,6 +5580,63 @@ function dockerComposeEnvironmentVariableInfo(?string $dockerCompose, string $ke
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Docker Compose gives a bare $VAR or ${VAR} reference no default, and the service parser does not store
|
||||
* defaults. So `/$POSTGRES_DB` next to `${POSTGRES_DB:-umami}` in another service resolves to an empty value.
|
||||
* For new variables, store the default when the Compose file has a bare reference and exactly one default.
|
||||
*
|
||||
* @param Collection<int, string> $existingKeys
|
||||
*/
|
||||
function fillDockerComposeDefaultsForBareReferences(Service $resource, Collection $existingKeys): void
|
||||
{
|
||||
$dockerCompose = $resource->docker_compose_raw;
|
||||
if (blank($dockerCompose)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource->environment_variables()
|
||||
->whereNull('value')
|
||||
->whereNotIn('key', $existingKeys)
|
||||
->get()
|
||||
->each(function (EnvironmentVariable $environmentVariable) use ($dockerCompose): void {
|
||||
$default = dockerComposeBareReferenceDefault($dockerCompose, $environmentVariable->key);
|
||||
// A nested default (`${A:-${B}}`) must stay with Compose: stored, it would be escaped to a literal.
|
||||
if (filled($default) && ! str_contains($default, '$')) {
|
||||
$environmentVariable->update(['value' => $default]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** The single default of a variable that the Compose file also references without a default, otherwise null. */
|
||||
function dockerComposeBareReferenceDefault(string $dockerCompose, string $key): ?string
|
||||
{
|
||||
$quotedKey = preg_quote($key, '/');
|
||||
if (preg_match('/(?<!\$)\$(?:'.$quotedKey.'(?![A-Za-z0-9_])|\{'.$quotedKey.'\})/', $dockerCompose) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$defaults = [];
|
||||
$offset = 0;
|
||||
while (($referenceStart = strpos($dockerCompose, '${'.$key, $offset)) !== false) {
|
||||
$offset = $referenceStart + 2;
|
||||
if ($referenceStart > 0 && $dockerCompose[$referenceStart - 1] === '$') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reference = extractBalancedBraceContent(substr($dockerCompose, $referenceStart));
|
||||
$split = $reference === null ? null : splitOnOperatorOutsideNested($reference['content']);
|
||||
if ($split === null || $split['variable'] !== $key) {
|
||||
continue;
|
||||
}
|
||||
if (str_contains($split['operator'], '?')) {
|
||||
return null;
|
||||
}
|
||||
$defaults[$split['default']] = true;
|
||||
}
|
||||
|
||||
return count($defaults) === 1 ? (string) array_key_first($defaults) : null;
|
||||
}
|
||||
|
||||
function dockerComposeEnvironmentVariableIsRequired(?string $dockerCompose, string $key): bool
|
||||
{
|
||||
if (blank($dockerCompose)) {
|
||||
|
||||
@@ -396,6 +396,45 @@ YAML;
|
||||
->and(Yaml::parse($service->fresh()->docker_compose_raw))->toBe(Yaml::parse($dockerCompose));
|
||||
});
|
||||
|
||||
it('stores the Compose default for a variable that another service references without one', function (string $dockerCompose) {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id,
|
||||
'docker_compose_raw' => $dockerCompose,
|
||||
]);
|
||||
|
||||
$service->parse();
|
||||
|
||||
expect($service->environment_variables()->where('key', 'POSTGRES_DB')->value('value'))->toBe('umami');
|
||||
})->with([
|
||||
'reference before default' => "services:\n app:\n image: nginx\n environment:\n DATABASE_URL: postgres://user:pass@postgresql:5432/\$POSTGRES_DB\n postgresql:\n image: postgres:16-alpine\n environment:\n POSTGRES_DB: \${POSTGRES_DB:-umami}\n",
|
||||
'default before reference' => "services:\n postgresql:\n image: postgres:16-alpine\n environment:\n POSTGRES_DB: \${POSTGRES_DB:-umami}\n app:\n image: nginx\n environment:\n DATABASE_URL: postgres://user:pass@postgresql:5432/\${POSTGRES_DB}\n",
|
||||
]);
|
||||
|
||||
it('does not store a nested Compose default for a bare reference', function () {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id,
|
||||
'docker_compose_raw' => "services:\n app:\n image: alpine\n environment:\n DB: \${DB:-\${APP_NAME}}\n URL: postgres://db/\$DB\n",
|
||||
]);
|
||||
|
||||
$service->parse();
|
||||
|
||||
expect($service->environment_variables()->where('key', 'DB')->firstOrFail()->value)->toBeNull();
|
||||
});
|
||||
|
||||
it('does not store a Compose default when the defaults for a bare reference conflict', function () {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id,
|
||||
'docker_compose_raw' => "services:\n app:\n image: alpine\n environment:\n LEVEL: \${LEVEL:-info}\n LOG: level=\$LEVEL\n worker:\n image: alpine\n environment:\n LEVEL: \${LEVEL:-warn}\n",
|
||||
]);
|
||||
|
||||
$service->parse();
|
||||
|
||||
expect($service->environment_variables()->where('key', 'LEVEL')->firstOrFail()->value)->toBeNull();
|
||||
});
|
||||
|
||||
it('keeps editable and literal rows when services use the same key differently', function () {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
|
||||
Reference in New Issue
Block a user