fix(deployment): enhance Dockerfile modification for build-time variables and secrets during deployment in case of docker compose buildpack

This commit is contained in:
Andras Bacsai
2025-09-19 13:46:00 +02:00
parent bfaefed1ae
commit 593c1b4767

View File

@@ -606,6 +606,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
'hidden' => true, 'hidden' => true,
]); ]);
// Modify Dockerfiles for ARGs and build secrets
$this->modify_dockerfiles_for_compose($composeFile);
// Build new container to limit downtime. // Build new container to limit downtime.
$this->application_deployment_queue->addLogEntry('Pulling & building required images.'); $this->application_deployment_queue->addLogEntry('Pulling & building required images.');
@@ -632,6 +635,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else { } else {
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull"; $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
} }
if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
$build_args_string = $this->build_args->implode(' ');
$command .= " {$build_args_string}";
$this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.');
}
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true], [executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
); );
@@ -2830,8 +2840,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// Get environment variables for secrets // Get environment variables for secrets
$variables = $this->pull_request_id === 0 $variables = $this->pull_request_id === 0
? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get()
: $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get();
if ($variables->isEmpty()) { if ($variables->isEmpty()) {
return; return;
@@ -2868,6 +2878,164 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} }
} }
private function modify_dockerfiles_for_compose($composeFile)
{
if ($this->application->build_pack !== 'dockercompose') {
return;
}
$variables = $this->pull_request_id === 0
? $this->application->environment_variables()
->where('key', 'not like', 'NIXPACKS_%')
->where('is_buildtime', true)
->get()
: $this->application->environment_variables_preview()
->where('key', 'not like', 'NIXPACKS_%')
->where('is_buildtime', true)
->get();
if ($variables->isEmpty()) {
$this->application_deployment_queue->addLogEntry('No build-time variables to add to Dockerfiles.');
return;
}
$services = data_get($composeFile, 'services', []);
foreach ($services as $serviceName => $service) {
if (! isset($service['build'])) {
continue;
}
$context = '.';
$dockerfile = 'Dockerfile';
if (is_string($service['build'])) {
$context = $service['build'];
} elseif (is_array($service['build'])) {
$context = data_get($service['build'], 'context', '.');
$dockerfile = data_get($service['build'], 'dockerfile', 'Dockerfile');
}
$dockerfilePath = rtrim($context, '/').'/'.ltrim($dockerfile, '/');
if (str_starts_with($dockerfilePath, './')) {
$dockerfilePath = substr($dockerfilePath, 2);
}
if (str_starts_with($dockerfilePath, '/')) {
$dockerfilePath = substr($dockerfilePath, 1);
}
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "test -f {$this->workdir}/{$dockerfilePath} && echo 'exists' || echo 'not found'"),
'hidden' => true,
'save' => 'dockerfile_check_'.$serviceName,
]);
if (str($this->saved_outputs->get('dockerfile_check_'.$serviceName))->trim()->toString() !== 'exists') {
$this->application_deployment_queue->addLogEntry("Dockerfile not found for service {$serviceName} at {$dockerfilePath}, skipping ARG injection.");
continue;
}
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$dockerfilePath}"),
'hidden' => true,
'save' => 'dockerfile_content_'.$serviceName,
]);
$dockerfileContent = $this->saved_outputs->get('dockerfile_content_'.$serviceName);
if (! $dockerfileContent) {
continue;
}
$dockerfile_lines = collect(str($dockerfileContent)->trim()->explode("\n"));
$fromIndices = [];
$dockerfile_lines->each(function ($line, $index) use (&$fromIndices) {
if (str($line)->trim()->startsWith('FROM')) {
$fromIndices[] = $index;
}
});
if (empty($fromIndices)) {
$this->application_deployment_queue->addLogEntry("No FROM instruction found in Dockerfile for service {$serviceName}, skipping.");
continue;
}
$isMultiStage = count($fromIndices) > 1;
$argsToAdd = collect([]);
foreach ($variables as $env) {
$argsToAdd->push("ARG {$env->key}");
}
ray($argsToAdd);
if ($argsToAdd->isEmpty()) {
$this->application_deployment_queue->addLogEntry("Service {$serviceName}: No build-time variables to add.");
continue;
}
$totalAdded = 0;
$offset = 0;
foreach ($fromIndices as $stageIndex => $fromIndex) {
$adjustedIndex = $fromIndex + $offset;
$stageStart = $adjustedIndex + 1;
$stageEnd = isset($fromIndices[$stageIndex + 1])
? $fromIndices[$stageIndex + 1] + $offset
: $dockerfile_lines->count();
$existingStageArgs = collect([]);
for ($i = $stageStart; $i < $stageEnd; $i++) {
$line = $dockerfile_lines->get($i);
if (! $line || ! str($line)->trim()->startsWith('ARG')) {
break;
}
$parts = explode(' ', trim($line), 2);
if (count($parts) >= 2) {
$argPart = $parts[1];
$keyValue = explode('=', $argPart, 2);
$existingStageArgs->push($keyValue[0]);
}
}
$stageArgsToAdd = $argsToAdd->filter(function ($arg) use ($existingStageArgs) {
$key = str($arg)->after('ARG ')->trim()->toString();
return ! $existingStageArgs->contains($key);
});
if ($stageArgsToAdd->isNotEmpty()) {
$dockerfile_lines->splice($adjustedIndex + 1, 0, $stageArgsToAdd->toArray());
$totalAdded += $stageArgsToAdd->count();
$offset += $stageArgsToAdd->count();
}
}
if ($totalAdded > 0) {
$dockerfile_base64 = base64_encode($dockerfile_lines->implode("\n"));
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}/{$dockerfilePath} > /dev/null"),
'hidden' => true,
]);
$stageInfo = $isMultiStage ? ' (multi-stage build, added to '.count($fromIndices).' stages)' : '';
$this->application_deployment_queue->addLogEntry("Added {$totalAdded} ARG declarations to Dockerfile for service {$serviceName}{$stageInfo}.");
} else {
$this->application_deployment_queue->addLogEntry("Service {$serviceName}: All required ARG declarations already exist.");
}
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
$fullDockerfilePath = "{$this->workdir}/{$dockerfilePath}";
$this->modify_dockerfile_for_secrets($fullDockerfilePath);
$this->application_deployment_queue->addLogEntry("Modified Dockerfile for service {$serviceName} to use build secrets.");
}
}
}
private function add_build_secrets_to_compose($composeFile) private function add_build_secrets_to_compose($composeFile)
{ {
// Get environment variables for secrets // Get environment variables for secrets