Merge pull request #249 from lorenzomigliorero/feat/buildargs

feat: buildargs
This commit is contained in:
Mauricio Siu
2024-07-22 21:19:14 -06:00
committed by GitHub
11 changed files with 3101 additions and 119 deletions

1
.gitignore vendored
View File

@@ -52,6 +52,7 @@ yarn-error.log*
# otros
/.data
/.main
.vscode
*.lockb
*.rdb

View File

@@ -1,30 +1,16 @@
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Toggle } from "@/components/ui/toggle";
import { Card, CardContent } from "@/components/ui/card";
import { Form } from "@/components/ui/form";
import { Secrets } from "@/components/ui/secrets";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addEnvironmentSchema = z.object({
environment: z.string(),
env: z.string(),
buildArgs: z.string(),
});
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
@@ -34,7 +20,6 @@ interface Props {
}
export const ShowEnvironment = ({ applicationId }: Props) => {
const [isEnvVisible, setIsEnvVisible] = useState(true);
const { mutateAsync, isLoading } =
api.application.saveEnvironment.useMutation();
@@ -46,24 +31,19 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
enabled: !!applicationId,
},
);
const form = useForm<EnvironmentSchema>({
defaultValues: {
environment: "",
env: data?.env || "",
buildArgs: data?.buildArgs || "",
},
resolver: zodResolver(addEnvironmentSchema),
});
useEffect(() => {
if (data) {
form.reset({
environment: data.env || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: EnvironmentSchema) => {
mutateAsync({
env: data.environment,
env: data.env,
buildArgs: data.buildArgs,
applicationId,
})
.then(async () => {
@@ -74,94 +54,50 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
toast.error("Error to add environment");
});
};
useEffect(() => {
if (isEnvVisible) {
if (data?.env) {
const maskedLines = data.env
.split("\n")
.map((line) => "*".repeat(line.length))
.join("\n");
form.reset({
environment: maskedLines,
});
} else {
form.reset({
environment: "",
});
}
} else {
form.reset({
environment: data?.env || "",
});
}
}, [form.reset, data, form, isEnvVisible]);
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader className="flex flex-row w-full items-center justify-between">
<div>
<CardTitle className="text-xl">Environment Settings</CardTitle>
<CardDescription>
You can add environment variables to your resource.
</CardDescription>
</div>
<Toggle
aria-label="Toggle bold"
pressed={isEnvVisible}
onPressedChange={setIsEnvVisible}
>
{isEnvVisible ? (
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
) : (
<EyeIcon className="h-4 w-4 text-muted-foreground" />
)}
</Toggle>
</CardHeader>
<CardContent>
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="w-full space-y-4"
>
<FormField
control={form.control}
name="environment"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<CodeEditor
language="properties"
disabled={isEnvVisible}
placeholder={`NODE_ENV=production
PORT=3000
`}
className="h-96 font-mono"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row justify-end">
<Button
disabled={isEnvVisible}
isLoading={isLoading}
className="w-fit"
type="submit"
>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex w-full flex-col gap-5 "
>
<Card className="bg-background">
<Secrets
name="env"
title="Environment Settings"
description="You can add environment variables to your resource."
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
/>
{data?.buildType === "dockerfile" && (
<Secrets
name="buildArgs"
title="Build-time Variables"
description={
<span>
Available only at build-time. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/guide/build-args/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="NPM_TOKEN=xyz"
/>
)}
<CardContent>
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">
Save
</Button>
</div>
</CardContent>
</Card>
</form>
</Form>
);
};

78
components/ui/secrets.tsx Normal file
View File

@@ -0,0 +1,78 @@
import { CodeEditor } from "@/components/shared/code-editor";
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Toggle } from "@/components/ui/toggle";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type CSSProperties, type ReactNode, useState } from "react";
import { useFormContext } from "react-hook-form";
interface Props {
name: string;
title: string;
description: ReactNode;
placeholder: string;
}
export const Secrets = (props: Props) => {
const [isVisible, setIsVisible] = useState(true);
const form = useFormContext<Record<string, string>>();
return (
<>
<CardHeader className="flex flex-row w-full items-center justify-between">
<div>
<CardTitle className="text-xl">{props.title}</CardTitle>
<CardDescription>{props.description}</CardDescription>
</div>
<Toggle
aria-label="Toggle bold"
pressed={isVisible}
onPressedChange={setIsVisible}
>
{isVisible ? (
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
) : (
<EyeIcon className="h-4 w-4 text-muted-foreground" />
)}
</Toggle>
</CardHeader>
<CardContent className="w-full space-y-4">
<FormField
control={form.control}
name={props.name}
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<CodeEditor
style={
{
WebkitTextSecurity: isVisible ? "disc" : null,
} as CSSProperties
}
language="properties"
disabled={isVisible}
placeholder={props.placeholder}
className="h-96 font-mono"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</>
);
};

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -176,6 +176,13 @@
"when": 1721603595092,
"tag": "0024_dapper_supernaut",
"breakpoints": true
},
{
"idx": 25,
"version": "6",
"when": 1721633853118,
"tag": "0025_lying_mephisto",
"breakpoints": true
}
]
}

View File

@@ -17,7 +17,7 @@
"migration:run": "tsx -r dotenv/config migration.ts",
"migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts",
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
"db:push": "drizzle-kit --config ./server/db/drizzle.config.ts",
"db:push": "drizzle-kit push --config ./server/db/drizzle.config.ts",
"db:truncate": "tsx -r dotenv/config ./server/db/reset.ts",
"db:studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true",

View File

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

View File

@@ -107,6 +107,7 @@ export const applications = pgTable("application", {
.unique(),
description: text("description"),
env: text("env"),
buildArgs: text("buildArgs"),
memoryReservation: integer("memoryReservation"),
memoryLimit: integer("memoryLimit"),
cpuReservation: integer("cpuReservation"),
@@ -275,6 +276,7 @@ const createSchema = createInsertSchema(applications, {
applicationId: z.string(),
autoDeploy: z.boolean(),
env: z.string().optional(),
buildArgs: z.string().optional(),
name: z.string().min(1),
description: z.string().optional(),
memoryReservation: z.number().optional(),
@@ -375,6 +377,7 @@ export const apiSaveEnvironmentVariables = createSchema
.pick({
applicationId: true,
env: true,
buildArgs: true,
})
.required();

View File

@@ -1,5 +1,6 @@
import type { WriteStream } from "node:fs";
import { docker } from "@/server/constants";
import { prepareBuildArgs } from "@/server/utils/docker/utils";
import * as tar from "tar-fs";
import type { ApplicationNested } from ".";
import { getBuildAppDirectory } from "../filesystem/directory";
@@ -9,7 +10,7 @@ export const buildCustomDocker = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { appName, env } = application;
const { appName, env, buildArgs } = application;
const dockerFilePath = getBuildAppDirectory(application);
try {
const image = `${appName}`;
@@ -21,6 +22,7 @@ export const buildCustomDocker = async (
const stream = await docker.buildImage(tarStream, {
t: image,
buildargs: prepareBuildArgs(buildArgs),
dockerfile: dockerFilePath.substring(dockerFilePath.lastIndexOf("/") + 1),
});

View File

@@ -161,6 +161,21 @@ export const removeService = async (appName: string) => {
export const prepareEnvironmentVariables = (env: string | null) =>
Object.entries(parse(env ?? "")).map(([key, value]) => `${key}=${value}`);
export const prepareBuildArgs = (input: string | null) => {
const pairs = (input ?? "").split("\n");
const jsonObject: Record<string, string> = {};
for (const pair of pairs) {
const [key, value] = pair.split("=");
if (key && value) {
jsonObject[key] = value;
}
}
return jsonObject;
};
export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => {
if (!mounts || mounts.length === 0) {
return [];