mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(compose): let the build interpolate required runtime-only variables
Closes #12363 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e7a46bd790
commit
bd9708d79b
@@ -38,6 +38,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\TimeoutExceededException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Sleep;
|
||||
@@ -919,7 +920,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
|
||||
|
||||
// Save build-time .env file BEFORE the build
|
||||
$this->save_buildtime_environment_variables($buildEnvironment);
|
||||
$buildEnvironment ??= $this->generate_buildtime_environment_variables();
|
||||
$this->save_buildtime_environment_variables($buildEnvironment->merge($this->compose_build_interpolation_variables($composeFile, $buildEnvironment)));
|
||||
|
||||
if ($this->docker_compose_custom_build_command) {
|
||||
// Auto-inject -f (compose file) and --env-file flags using helper function
|
||||
@@ -985,6 +987,46 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `docker compose build` interpolates the whole Compose file, so a required runtime-only variable
|
||||
* (`${KEY:?}` in `environment:`) fails the build when the build-time .env file does not define it.
|
||||
* This gives the build the runtime .env lines of the variables that the file uses outside its build
|
||||
* sections. A variable that a build section or a build secret can read is left out, so a
|
||||
* runtime-only value never becomes a build argument or part of an image.
|
||||
*
|
||||
* @return Collection<int, string>
|
||||
*/
|
||||
private function compose_build_interpolation_variables(mixed $composeFile, Collection $buildEnvironment): Collection
|
||||
{
|
||||
$compose = is_string($composeFile) ? parseDockerComposeYaml($composeFile) : convertToArray($composeFile);
|
||||
$references = fn (array $values): Collection => collect(Arr::flatten($values))
|
||||
->flatMap(fn (mixed $value): Collection => extractDockerComposeEnvironmentVariableReferences($value));
|
||||
|
||||
$readByBuilds = collect(data_get($compose, 'secrets', []))->pluck('environment')->filter();
|
||||
foreach (data_get($compose, 'services', []) as $serviceName => $service) {
|
||||
$build = data_get($service, 'build');
|
||||
if ($build === null) {
|
||||
continue;
|
||||
}
|
||||
$args = data_get($build, 'args', []);
|
||||
$argNames = is_array($args) && array_is_list($args)
|
||||
? array_map(fn (mixed $arg): string => explode('=', (string) $arg, 2)[0], $args)
|
||||
: array_keys((array) $args);
|
||||
$readByBuilds = $readByBuilds->merge($argNames)->merge($references((array) $build));
|
||||
unset($compose['services'][$serviceName]['build']);
|
||||
}
|
||||
|
||||
$buildKeys = $buildEnvironment->map(fn (string $line): string => explode('=', $line, 2)[0]);
|
||||
$keys = $references($compose)->unique()->diff($readByBuilds)->diff($buildKeys);
|
||||
if ($keys->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->generate_runtime_environment_variables()
|
||||
->filter(fn (string $line): bool => $keys->contains(explode('=', $line, 2)[0]))
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the Compose file for this deployment and writes the volume warnings of the parser
|
||||
* (for example an external volume that the resource does not use yet) to the deployment log.
|
||||
|
||||
@@ -2076,3 +2076,70 @@ it('gives a preview from before v4.5.1 the production values it had', function (
|
||||
expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('DB_PASSWORD=') && str($env)->contains('production-secret')))->toBeTrue()
|
||||
->and($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('APP_NAME=') && str($env)->contains('coolify')))->toBeTrue();
|
||||
});
|
||||
|
||||
it('lets the Compose build interpolate runtime-only variables without passing them to the build', function (int $pullRequestId) {
|
||||
$compose = <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
MAPPED_ARG: '${RUNTIME_IN_BUILD_ARGS}'
|
||||
LISTED_FROM_ENV: null
|
||||
environment:
|
||||
FERNET_KEY: '${FERNET_KEY:?}'
|
||||
OPTIONAL_REQUIRED: '${OPTIONAL_REQUIRED?}'
|
||||
BUILD_VALUE: '${BUILD_VALUE}'
|
||||
worker:
|
||||
build:
|
||||
context: ./worker
|
||||
args:
|
||||
- LISTED_ARG
|
||||
environment:
|
||||
FERNET_KEY: '${FERNET_KEY:?}'
|
||||
YAML;
|
||||
|
||||
[$application, $server] = makeDeploymentControlVarFixture([
|
||||
'build_pack' => 'dockercompose',
|
||||
'docker_compose_raw' => $compose,
|
||||
'docker_compose' => $compose,
|
||||
'docker_compose_domains' => '[]',
|
||||
]);
|
||||
$application->settings()->update(['inject_build_args_to_dockerfile' => false]);
|
||||
|
||||
createApplicationEnvironmentVariable($application, ['key' => 'FERNET_KEY', 'value' => 'runtime-fernet-key', 'is_buildtime' => false]);
|
||||
createApplicationEnvironmentVariable($application, ['key' => 'OPTIONAL_REQUIRED', 'value' => '', 'is_buildtime' => false]);
|
||||
createApplicationEnvironmentVariable($application, ['key' => 'RUNTIME_IN_BUILD_ARGS', 'value' => 'runtime-mapped-arg', 'is_buildtime' => false]);
|
||||
createApplicationEnvironmentVariable($application, ['key' => 'LISTED_FROM_ENV', 'value' => 'runtime-null-arg', 'is_buildtime' => false]);
|
||||
createApplicationEnvironmentVariable($application, ['key' => 'LISTED_ARG', 'value' => 'runtime-listed-arg', 'is_buildtime' => false]);
|
||||
createApplicationEnvironmentVariable($application, ['key' => 'BUILD_VALUE', 'value' => 'build-value']);
|
||||
|
||||
[$job, $reflection] = makeControlVarFilteringJob($application->fresh(), $server, [
|
||||
'pull_request_id' => $pullRequestId,
|
||||
'remote_secrets_cache' => [],
|
||||
'force_rebuild' => false,
|
||||
'docker_compose_location' => '/docker-compose.yaml',
|
||||
]);
|
||||
|
||||
invokeDeploymentJobMethod($job, $reflection, 'generate_build_env_variables');
|
||||
$buildEnvironment = invokeDeploymentJobMethod($job, $reflection, 'generate_buildtime_environment_variables');
|
||||
invokeDeploymentJobMethod($job, $reflection, 'build_docker_compose_images', parseDockerComposeYaml($compose), $buildEnvironment);
|
||||
|
||||
$buildEnvFile = $job->writtenArtifacts[ApplicationDeploymentJob::BUILD_TIME_ENV_PATH];
|
||||
$buildCommand = collect($job->recordedCommands)->flatten()->first(fn (mixed $command): bool => is_string($command) && str_contains($command, ' build --pull'));
|
||||
$buildArgs = readDeploymentJobProperty($job, $reflection, 'build_args')->implode(' ');
|
||||
|
||||
expect($buildEnvFile)->toContain('FERNET_KEY="runtime-fernet-key"')
|
||||
->toContain('OPTIONAL_REQUIRED=""')
|
||||
->toContain('BUILD_VALUE=')
|
||||
->not->toContain('runtime-mapped-arg')
|
||||
->not->toContain('runtime-null-arg')
|
||||
->not->toContain('runtime-listed-arg')
|
||||
->and($buildCommand)->toContain('--env-file '.ApplicationDeploymentJob::BUILD_TIME_ENV_PATH)
|
||||
->and($buildArgs)->toContain('BUILD_VALUE')
|
||||
->not->toContain('FERNET_KEY')
|
||||
->not->toContain('runtime-');
|
||||
})->with([
|
||||
'production' => 0,
|
||||
'preview' => 42,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user