fix(deployment): enhance Dockerfile modification for build-time variables and secrets during deployment in case of docker compose buildpack
This commit is contained in:
@@ -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
|
||||||
|
Reference in New Issue
Block a user