Merge pull request #297 from lorenzomigliorero/feat/static-build-phase

feat: new publish directory flag
This commit is contained in:
Mauricio Siu 2024-08-01 02:28:37 -06:00 committed by GitHub
commit 6299385bb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 3162 additions and 11 deletions

View File

@ -40,6 +40,7 @@ const baseApp: ApplicationNested = {
placementSwarm: null, placementSwarm: null,
ports: [], ports: [],
projectId: "", projectId: "",
publishDirectory: null,
redirects: [], redirects: [],
refreshToken: "", refreshToken: "",
registry: null, registry: null,

View File

@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -43,6 +44,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}), }),
z.object({ z.object({
buildType: z.literal("nixpacks"), buildType: z.literal("nixpacks"),
publishDirectory: z.string().optional(),
}), }),
]); ]);
@ -84,6 +86,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
} else { } else {
form.reset({ form.reset({
buildType: data.buildType, buildType: data.buildType,
publishDirectory: data.publishDirectory || undefined,
}); });
} }
} }
@ -93,6 +96,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
await mutateAsync({ await mutateAsync({
applicationId, applicationId,
buildType: data.buildType, buildType: data.buildType,
publishDirectory:
data.buildType === "nixpacks" ? data.publishDirectory : null,
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null, dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
}) })
.then(async () => { .then(async () => {
@ -200,6 +205,36 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
}} }}
/> />
)} )}
{buildType === "nixpacks" && (
<FormField
control={form.control}
name="publishDirectory"
render={({ field }) => {
return (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Publish Directory</FormLabel>
<FormDescription>
Allows you to serve a single directory via NGINX after
the build phase. Useful if the final build assets
should be served as a static site.
</FormDescription>
</div>
<FormControl>
<Input
placeholder={"Publish Directory"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
)}
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">
Save Save

View File

@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "publishDirectory" text;

File diff suppressed because it is too large Load Diff

View File

@ -190,6 +190,13 @@
"when": 1721979220929, "when": 1721979220929,
"tag": "0026_known_dormammu", "tag": "0026_known_dormammu",
"breakpoints": true "breakpoints": true
},
{
"idx": 27,
"version": "6",
"when": 1722445099203,
"tag": "0027_red_lady_bullseye",
"breakpoints": true
} }
] ]
} }

View File

@ -191,6 +191,7 @@ export const applicationRouter = createTRPCRouter({
await updateApplication(input.applicationId, { await updateApplication(input.applicationId, {
buildType: input.buildType, buildType: input.buildType,
dockerfile: input.dockerfile, dockerfile: input.dockerfile,
publishDirectory: input.publishDirectory,
}); });
return true; return true;

View File

@ -157,6 +157,7 @@ export const applications = pgTable("application", {
.notNull() .notNull()
.default("idle"), .default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"), buildType: buildType("buildType").notNull().default("nixpacks"),
publishDirectory: text("publishDirectory"),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
@ -316,6 +317,7 @@ const createSchema = createInsertSchema(applications, {
"paketo_buildpacks", "paketo_buildpacks",
"nixpacks", "nixpacks",
]), ]),
publishDirectory: z.string().optional(),
owner: z.string(), owner: z.string(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(), healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(), restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
@ -353,7 +355,8 @@ export const apiSaveBuildType = createSchema
buildType: true, buildType: true,
dockerfile: true, dockerfile: true,
}) })
.required(); .required()
.merge(createSchema.pick({ publishDirectory: true }));
export const apiSaveGithubProvider = createSchema export const apiSaveGithubProvider = createSchema
.pick({ .pick({

View File

@ -9,7 +9,7 @@ export const buildCustomDocker = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { appName, env, buildArgs } = application; const { appName, env, publishDirectory, buildArgs } = application;
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
const image = `${appName}`; const image = `${appName}`;
@ -24,7 +24,15 @@ export const buildCustomDocker = async (
commandArgs.push("--build-arg", arg); commandArgs.push("--build-arg", arg);
} }
createEnvFile(dockerFilePath, env); /*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
*/
if (!publishDirectory) {
createEnvFile(dockerFilePath, env);
}
await spawnAsync( await spawnAsync(
"docker", "docker",
commandArgs, commandArgs,

View File

@ -1,18 +1,28 @@
import type { WriteStream } from "node:fs"; import type { WriteStream } from "node:fs";
import path from "node:path";
import { buildStatic } from "@/server/utils/builders/static";
import { nanoid } from "nanoid";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils"; import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
export const buildNixpacks = async ( export const buildNixpacks = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName } = application; const { env, appName, publishDirectory } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const buildAppDirectory = getBuildAppDirectory(application);
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(env); const envVariables = prepareEnvironmentVariables(env);
const writeToStream = (data: string) => {
if (writeStream.writable) {
writeStream.write(data);
}
};
try { try {
const args = ["build", buildAppDirectory, "--name", appName]; const args = ["build", buildAppDirectory, "--name", appName];
@ -20,13 +30,44 @@ export const buildNixpacks = async (
args.push("--env", env); args.push("--env", env);
} }
await spawnAsync("nixpacks", args, (data) => { if (publishDirectory) {
if (writeStream.writable) { /* No need for any start command, since we'll use nginx later on */
writeStream.write(data); args.push("--no-error-without-start");
} }
});
await spawnAsync("nixpacks", args, writeToStream);
/*
Run the container with the image created by nixpacks,
and copy the artifacts on the host filesystem.
Then, remove the container and create a static build.
*/
if (publishDirectory) {
await spawnAsync(
"docker",
["create", "--name", buildContainerId, appName],
writeToStream,
);
await spawnAsync(
"docker",
[
"cp",
`${buildContainerId}:/app/${publishDirectory}`,
path.join(buildAppDirectory, publishDirectory),
],
writeToStream,
);
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
await buildStatic(application, writeStream);
}
return true; return true;
} catch (e) { } catch (e) {
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
throw e; throw e;
} }
}; };

View File

@ -0,0 +1,38 @@
import type { WriteStream } from "node:fs";
import { buildCustomDocker } from "@/server/utils/builders/docker-file";
import type { ApplicationNested } from ".";
import { createFile } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
export const buildStatic = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { publishDirectory } = application;
const buildAppDirectory = getBuildAppDirectory(application);
try {
createFile(
buildAppDirectory,
"Dockerfile",
[
"FROM nginx:alpine",
"WORKDIR /usr/share/nginx/html/",
`COPY ${publishDirectory || "."} .`,
].join("\n"),
);
await buildCustomDocker(
{
...application,
buildType: "dockerfile",
dockerfile: "Dockerfile",
},
writeStream,
);
return true;
} catch (e) {
throw e;
}
};