Compare commits

...

1 Commits

Author SHA1 Message Date
Mauricio Siu
c7f44f65bc feat(applications): add support for Docker build secrets
- Implement build secrets functionality for Dockerfile builds
- Add new `buildSecrets` field to application schema
- Update UI and backend to handle build-time secrets
- Modify Docker build process to support secret injection during build
2025-03-08 19:06:14 -06:00
8 changed files with 5257 additions and 28 deletions

View File

@@ -11,6 +11,7 @@ import { z } from "zod";
const addEnvironmentSchema = z.object({ const addEnvironmentSchema = z.object({
env: z.string(), env: z.string(),
buildArgs: z.string(), buildArgs: z.string(),
buildSecrets: z.record(z.string(), z.string()),
}); });
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>; type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
@@ -36,6 +37,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
defaultValues: { defaultValues: {
env: data?.env || "", env: data?.env || "",
buildArgs: data?.buildArgs || "", buildArgs: data?.buildArgs || "",
buildSecrets: data?.buildSecrets || {},
}, },
resolver: zodResolver(addEnvironmentSchema), resolver: zodResolver(addEnvironmentSchema),
}); });
@@ -44,6 +46,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
mutateAsync({ mutateAsync({
env: data.env, env: data.env,
buildArgs: data.buildArgs, buildArgs: data.buildArgs,
buildSecrets: data.buildSecrets,
applicationId, applicationId,
}) })
.then(async () => { .then(async () => {
@@ -69,25 +72,63 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")} placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
/> />
{data?.buildType === "dockerfile" && ( {data?.buildType === "dockerfile" && (
<Secrets <>
name="buildArgs" <Secrets
title="Build-time Variables" name="buildArgs"
description={ title="Build-time Variables"
<span> description={
Available only at build-time. See documentation&nbsp; <span>
<a Available only at build-time. See documentation&nbsp;
className="text-primary" <a
href="https://docs.docker.com/build/guide/build-args/" className="text-primary"
target="_blank" href="https://docs.docker.com/build/guide/build-args/"
rel="noopener noreferrer" target="_blank"
> rel="noopener noreferrer"
here >
</a> here
. </a>
</span> .
} </span>
placeholder="NPM_TOKEN=xyz" }
/> placeholder="NPM_TOKEN=xyz"
/>
<Secrets
name="buildSecrets"
title="Build Secrets"
description={
<span>
Secrets available only during build-time and not in the
final image. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/building/secrets/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="API_TOKEN=xyz"
transformValue={(value) => {
// Convert the string format to object
const lines = value.split("\n").filter((line) => line.trim());
return Object.fromEntries(
lines.map((line) => {
const [key, ...valueParts] = line.split("=");
return [key.trim(), valueParts.join("=").trim()];
}),
);
}}
formatValue={(value) => {
// Convert the object back to string format
return Object.entries(value as Record<string, string>)
.map(([key, val]) => `${key}=${val}`)
.join("\n");
}}
/>
</>
)} )}
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit"> <Button isLoading={isLoading} className="w-fit" type="submit">

View File

@@ -0,0 +1,2 @@
ALTER TABLE "application" ADD COLUMN "buildSecrets" text;--> statement-breakpoint
ALTER TABLE "user_temp" DROP COLUMN "enableLogRotation";

File diff suppressed because it is too large Load Diff

View File

@@ -505,6 +505,13 @@
"when": 1741460060541, "when": 1741460060541,
"tag": "0071_flaky_black_queen", "tag": "0071_flaky_black_queen",
"breakpoints": true "breakpoints": true
},
{
"idx": 72,
"version": "7",
"when": 1741481694393,
"tag": "0072_milky_lyja",
"breakpoints": true
} }
] ]
} }

View File

@@ -298,6 +298,7 @@ export const applicationRouter = createTRPCRouter({
await updateApplication(input.applicationId, { await updateApplication(input.applicationId, {
env: input.env, env: input.env,
buildArgs: input.buildArgs, buildArgs: input.buildArgs,
buildSecrets: input.buildSecrets,
}); });
return true; return true;
}), }),

View File

@@ -129,6 +129,7 @@ export const applications = pgTable("application", {
false, false,
), ),
buildArgs: text("buildArgs"), buildArgs: text("buildArgs"),
buildSecrets: json("buildSecrets").$type<Record<string, string>>(),
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"), memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"), cpuReservation: text("cpuReservation"),
@@ -353,6 +354,7 @@ const createSchema = createInsertSchema(applications, {
autoDeploy: z.boolean(), autoDeploy: z.boolean(),
env: z.string().optional(), env: z.string().optional(),
buildArgs: z.string().optional(), buildArgs: z.string().optional(),
buildSecrets: z.record(z.string(), z.string()).optional(),
name: z.string().min(1), name: z.string().min(1),
description: z.string().optional(), description: z.string().optional(),
memoryReservation: z.string().optional(), memoryReservation: z.string().optional(),
@@ -499,11 +501,12 @@ export const apiSaveGitProvider = createSchema
}), }),
); );
export const apiSaveEnvironmentVariables = createSchema export const apiSaveEnvironmentVariables = z
.pick({ .object({
applicationId: true, applicationId: z.string(),
env: true, env: z.string().optional(),
buildArgs: true, buildArgs: z.string().optional(),
buildSecrets: z.record(z.string(), z.string()).optional(),
}) })
.required(); .required();

View File

@@ -149,6 +149,7 @@ table application {
previewLimit integer [default: 3] previewLimit integer [default: 3]
isPreviewDeploymentsActive boolean [default: false] isPreviewDeploymentsActive boolean [default: false]
buildArgs text buildArgs text
buildSecrets json
memoryReservation text memoryReservation text
memoryLimit text memoryLimit text
cpuReservation text cpuReservation text

View File

@@ -12,8 +12,14 @@ export const buildCustomDocker = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } = const {
application; appName,
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
} = application;
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
const image = `${appName}`; const image = `${appName}`;
@@ -25,6 +31,10 @@ export const buildCustomDocker = async (
application.project.env, application.project.env,
); );
const secrets = buildSecrets
? Object.entries(buildSecrets).map(([key, value]) => `${key}=${value}`)
: [];
const dockerContextPath = getDockerContextPath(application); const dockerContextPath = getDockerContextPath(application);
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."]; const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
@@ -36,6 +46,12 @@ export const buildCustomDocker = async (
for (const arg of args) { for (const arg of args) {
commandArgs.push("--build-arg", arg); commandArgs.push("--build-arg", arg);
} }
for (const secret of secrets) {
const [key] = secret.split("=");
commandArgs.push("--secret", `id=${key},env=${key}`);
}
/* /*
Do not generate an environment file when publishDirectory is specified, Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed. as it could be publicly exposed.
@@ -54,6 +70,10 @@ export const buildCustomDocker = async (
}, },
{ {
cwd: dockerContextPath || defaultContextPath, cwd: dockerContextPath || defaultContextPath,
env: {
...process.env,
...Object.fromEntries(secrets.map((s) => s.split("="))),
},
}, },
); );
} catch (error) { } catch (error) {
@@ -65,8 +85,14 @@ export const getDockerCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } = const {
application; appName,
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
} = application;
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
@@ -79,6 +105,10 @@ export const getDockerCommand = (
application.project.env, application.project.env,
); );
const secrets = buildSecrets
? Object.entries(buildSecrets).map(([key, value]) => `${key}=${value}`)
: [];
const dockerContextPath = const dockerContextPath =
getDockerContextPath(application) || defaultContextPath; getDockerContextPath(application) || defaultContextPath;
@@ -92,6 +122,11 @@ export const getDockerCommand = (
commandArgs.push("--build-arg", arg); commandArgs.push("--build-arg", arg);
} }
for (const secret of secrets) {
const [key] = secret.split("=");
commandArgs.push("--secret", `id=${key},env=${key}`);
}
/* /*
Do not generate an environment file when publishDirectory is specified, Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed. as it could be publicly exposed.
@@ -105,6 +140,14 @@ export const getDockerCommand = (
); );
} }
// Export secrets as environment variables
if (secrets.length > 0) {
command += "\n# Export build secrets\n";
for (const secret of secrets) {
command += `export ${secret}\n`;
}
}
command += ` command += `
echo "Building ${appName}" >> ${logPath}; echo "Building ${appName}" >> ${logPath};
cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || { cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {