fix(parsers): resolve shared variables in compose environment
Extract shared variable resolution logic into a reusable helper function
`resolveSharedEnvironmentVariables()` and apply it in applicationParser and
serviceParser to ensure patterns like {{environment.VAR}}, {{project.VAR}},
and {{team.VAR}} are properly resolved in the compose environment section.
Without this, unresolved {{...}} strings would take precedence over resolved
values from the .env file (env_file:) in docker-compose configurations.
This commit is contained in:
parent
709e5e882e
commit
7cfc6746c7
4 changed files with 189 additions and 31 deletions
|
|
@ -214,37 +214,7 @@ protected function isShared(): Attribute
|
|||
|
||||
private function get_real_environment_variables(?string $environment_variable = null, $resource = null)
|
||||
{
|
||||
if ((is_null($environment_variable) && $environment_variable === '') || is_null($resource)) {
|
||||
return null;
|
||||
}
|
||||
$environment_variable = trim($environment_variable);
|
||||
$sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/');
|
||||
if ($sharedEnvsFound->isEmpty()) {
|
||||
return $environment_variable;
|
||||
}
|
||||
foreach ($sharedEnvsFound as $sharedEnv) {
|
||||
$type = str($sharedEnv)->trim()->match('/(.*?)\./');
|
||||
if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
continue;
|
||||
}
|
||||
$variable = str($sharedEnv)->trim()->match('/\.(.*)/');
|
||||
if ($type->value() === 'environment') {
|
||||
$id = $resource->environment->id;
|
||||
} elseif ($type->value() === 'project') {
|
||||
$id = $resource->environment->project->id;
|
||||
} elseif ($type->value() === 'team') {
|
||||
$id = $resource->team()->id;
|
||||
}
|
||||
if (is_null($id)) {
|
||||
continue;
|
||||
}
|
||||
$environment_variable_found = SharedEnvironmentVariable::where('type', $type)->where('key', $variable)->where('team_id', $resource->team()->id)->where("{$type}_id", $id)->first();
|
||||
if ($environment_variable_found) {
|
||||
$environment_variable = str($environment_variable)->replace("{{{$sharedEnv}}}", $environment_variable_found->value);
|
||||
}
|
||||
}
|
||||
|
||||
return str($environment_variable)->value();
|
||||
return resolveSharedEnvironmentVariables($environment_variable, $resource);
|
||||
}
|
||||
|
||||
private function get_environment_variables(?string $environment_variable = null): ?string
|
||||
|
|
|
|||
|
|
@ -1294,6 +1294,13 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
// Otherwise keep empty string as-is
|
||||
}
|
||||
|
||||
// Resolve shared variable patterns like {{environment.VAR}}, {{project.VAR}}, {{team.VAR}}
|
||||
// Without this, literal {{...}} strings end up in the compose environment: section,
|
||||
// which takes precedence over the resolved values in the .env file (env_file:)
|
||||
if (is_string($value) && str_contains($value, '{{')) {
|
||||
$value = resolveSharedEnvironmentVariables($value, $resource);
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
}
|
||||
|
|
@ -2558,6 +2565,13 @@ function serviceParser(Service $resource): Collection
|
|||
// Otherwise keep empty string as-is
|
||||
}
|
||||
|
||||
// Resolve shared variable patterns like {{environment.VAR}}, {{project.VAR}}, {{team.VAR}}
|
||||
// Without this, literal {{...}} strings end up in the compose environment: section,
|
||||
// which takes precedence over the resolved values in the .env file (env_file:)
|
||||
if (is_string($value) && str_contains($value, '{{')) {
|
||||
$value = resolveSharedEnvironmentVariables($value, $resource);
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3972,3 +3972,49 @@ function downsampleLTTB(array $data, int $threshold): array
|
|||
|
||||
return $sampled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve shared environment variable patterns like {{environment.VAR}}, {{project.VAR}}, {{team.VAR}}.
|
||||
*
|
||||
* This is the canonical implementation used by both EnvironmentVariable::realValue and the compose parsers
|
||||
* to ensure shared variable references are replaced with their actual values.
|
||||
*/
|
||||
function resolveSharedEnvironmentVariables(?string $value, $resource): ?string
|
||||
{
|
||||
if (is_null($value) || $value === '' || is_null($resource)) {
|
||||
return $value;
|
||||
}
|
||||
$value = trim($value);
|
||||
$sharedEnvsFound = str($value)->matchAll('/{{(.*?)}}/');
|
||||
if ($sharedEnvsFound->isEmpty()) {
|
||||
return $value;
|
||||
}
|
||||
foreach ($sharedEnvsFound as $sharedEnv) {
|
||||
$type = str($sharedEnv)->trim()->match('/(.*?)\./');
|
||||
if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
continue;
|
||||
}
|
||||
$variable = str($sharedEnv)->trim()->match('/\.(.*)/');
|
||||
$id = null;
|
||||
if ($type->value() === 'environment') {
|
||||
$id = $resource->environment->id;
|
||||
} elseif ($type->value() === 'project') {
|
||||
$id = $resource->environment->project->id;
|
||||
} elseif ($type->value() === 'team') {
|
||||
$id = $resource->team()->id;
|
||||
}
|
||||
if (is_null($id)) {
|
||||
continue;
|
||||
}
|
||||
$found = \App\Models\SharedEnvironmentVariable::where('type', $type)
|
||||
->where('key', $variable)
|
||||
->where('team_id', $resource->team()->id)
|
||||
->where("{$type}_id", $id)
|
||||
->first();
|
||||
if ($found) {
|
||||
$value = str($value)->replace("{{{$sharedEnv}}}", $found->value);
|
||||
}
|
||||
}
|
||||
|
||||
return str($value)->value();
|
||||
}
|
||||
|
|
|
|||
128
tests/Feature/SharedVariableComposeResolutionTest.php
Normal file
128
tests/Feature/SharedVariableComposeResolutionTest.php
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user->teams()->attach($this->team);
|
||||
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create([
|
||||
'project_id' => $this->project->id,
|
||||
]);
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('resolveSharedEnvironmentVariables resolves environment-scoped variable', function () {
|
||||
SharedEnvironmentVariable::create([
|
||||
'key' => 'DRAGONFLY_URL',
|
||||
'value' => 'redis://dragonfly:6379',
|
||||
'type' => 'environment',
|
||||
'environment_id' => $this->environment->id,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$resolved = resolveSharedEnvironmentVariables('{{environment.DRAGONFLY_URL}}', $this->application);
|
||||
expect($resolved)->toBe('redis://dragonfly:6379');
|
||||
});
|
||||
|
||||
test('resolveSharedEnvironmentVariables resolves project-scoped variable', function () {
|
||||
SharedEnvironmentVariable::create([
|
||||
'key' => 'DB_HOST',
|
||||
'value' => 'postgres.internal',
|
||||
'type' => 'project',
|
||||
'project_id' => $this->project->id,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$resolved = resolveSharedEnvironmentVariables('{{project.DB_HOST}}', $this->application);
|
||||
expect($resolved)->toBe('postgres.internal');
|
||||
});
|
||||
|
||||
test('resolveSharedEnvironmentVariables resolves team-scoped variable', function () {
|
||||
SharedEnvironmentVariable::create([
|
||||
'key' => 'GLOBAL_API_KEY',
|
||||
'value' => 'sk-123456',
|
||||
'type' => 'team',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$resolved = resolveSharedEnvironmentVariables('{{team.GLOBAL_API_KEY}}', $this->application);
|
||||
expect($resolved)->toBe('sk-123456');
|
||||
});
|
||||
|
||||
test('resolveSharedEnvironmentVariables returns original when no match found', function () {
|
||||
$resolved = resolveSharedEnvironmentVariables('{{environment.NONEXISTENT}}', $this->application);
|
||||
expect($resolved)->toBe('{{environment.NONEXISTENT}}');
|
||||
});
|
||||
|
||||
test('resolveSharedEnvironmentVariables handles null and empty values', function () {
|
||||
expect(resolveSharedEnvironmentVariables(null, $this->application))->toBeNull();
|
||||
expect(resolveSharedEnvironmentVariables('', $this->application))->toBe('');
|
||||
expect(resolveSharedEnvironmentVariables('plain-value', $this->application))->toBe('plain-value');
|
||||
});
|
||||
|
||||
test('resolveSharedEnvironmentVariables resolves multiple variables in one string', function () {
|
||||
SharedEnvironmentVariable::create([
|
||||
'key' => 'HOST',
|
||||
'value' => 'myhost',
|
||||
'type' => 'environment',
|
||||
'environment_id' => $this->environment->id,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
SharedEnvironmentVariable::create([
|
||||
'key' => 'PORT',
|
||||
'value' => '6379',
|
||||
'type' => 'environment',
|
||||
'environment_id' => $this->environment->id,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$resolved = resolveSharedEnvironmentVariables('redis://{{environment.HOST}}:{{environment.PORT}}', $this->application);
|
||||
expect($resolved)->toBe('redis://myhost:6379');
|
||||
});
|
||||
|
||||
test('resolveSharedEnvironmentVariables handles spaces in pattern', function () {
|
||||
SharedEnvironmentVariable::create([
|
||||
'key' => 'MY_VAR',
|
||||
'value' => 'resolved-value',
|
||||
'type' => 'environment',
|
||||
'environment_id' => $this->environment->id,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$resolved = resolveSharedEnvironmentVariables('{{ environment.MY_VAR }}', $this->application);
|
||||
expect($resolved)->toBe('resolved-value');
|
||||
});
|
||||
|
||||
test('EnvironmentVariable real_value still resolves shared variables after refactor', function () {
|
||||
SharedEnvironmentVariable::create([
|
||||
'key' => 'DRAGONFLY_URL',
|
||||
'value' => 'redis://dragonfly:6379',
|
||||
'type' => 'environment',
|
||||
'environment_id' => $this->environment->id,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$env = EnvironmentVariable::create([
|
||||
'key' => 'REDIS_URL',
|
||||
'value' => '{{environment.DRAGONFLY_URL}}',
|
||||
'resourceable_id' => $this->application->id,
|
||||
'resourceable_type' => $this->application->getMorphClass(),
|
||||
]);
|
||||
|
||||
expect($env->real_value)->toBe('redis://dragonfly:6379');
|
||||
});
|
||||
Loading…
Reference in a new issue