feat: compose app

This commit is contained in:
Lorenzo Migliorero 2024-07-25 22:10:35 +02:00
parent 1f81ebd4fe
commit c681aa2e9f
No known key found for this signature in database
GPG Key ID: 9A9F1AD60C05DFE2
10 changed files with 3122 additions and 208 deletions

View File

@ -1,4 +1,3 @@
import { AddSSHKey } from "@/components/dashboard/settings/ssh-keys/add-ssh-key";
import { Button } from "@/components/ui/button";
import {
Form,
@ -20,13 +19,10 @@ import {
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { SelectSeparator } from "@radix-ui/react-select";
import { KeyRoundIcon, LockIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { flushSync } from "react-dom";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@ -53,11 +49,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } =
api.application.saveGitProdiver.useMutation();
// const { mutateAsync: generateSSHKey, isLoading: isGeneratingSSHKey } =
// api.application.generateSSHKey.useMutation();
// const { mutateAsync: removeSSHKey, isLoading: isRemovingSSHKey } =
// api.application.removeSSHKey.useMutation();
const form = useForm<GitProvider>({
defaultValues: {
branch: "",
@ -152,7 +144,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
<SelectItem value="none">None</SelectItem>
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
</SelectGroup>
<SelectSeparator />
</SelectContent>
</Select>
</FormControl>

View File

@ -1,13 +1,4 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
@ -17,11 +8,19 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { CopyIcon, LockIcon } from "lucide-react";
import { KeyRoundIcon, LockIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@ -33,6 +32,7 @@ const GitProviderSchema = z.object({
message: "Repository URL is required",
}),
branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(),
});
type GitProvider = z.infer<typeof GitProviderSchema>;
@ -43,19 +43,17 @@ interface Props {
export const SaveGitProviderCompose = ({ composeId }: Props) => {
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
const { mutateAsync, isLoading } = api.compose.update.useMutation();
const { mutateAsync: generateSSHKey, isLoading: isGeneratingSSHKey } =
api.compose.generateSSHKey.useMutation();
const { mutateAsync: removeSSHKey, isLoading: isRemovingSSHKey } =
api.compose.removeSSHKey.useMutation();
const form = useForm<GitProvider>({
defaultValues: {
branch: "",
repositoryURL: "",
composePath: "./docker-compose.yml",
sshKey: undefined,
},
resolver: zodResolver(GitProviderSchema),
});
@ -63,6 +61,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
useEffect(() => {
if (data) {
form.reset({
sshKey: data.customGitSSHKeyId || undefined,
branch: data.customGitBranch || "",
repositoryURL: data.customGitUrl || "",
composePath: data.composePath,
@ -74,6 +73,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
await mutateAsync({
customGitBranch: values.branch,
customGitUrl: values.repositoryURL,
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
composeId,
sourceType: "git",
composePath: values.composePath,
@ -94,123 +94,72 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
className="flex flex-col gap-4"
>
<div className="grid md:grid-cols-2 gap-4 ">
<div className="md:col-span-2 space-y-4">
<FormField
control={form.control}
name="repositoryURL"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row justify-between">
Repository URL
<div className="flex gap-2">
<Dialog>
<DialogTrigger className="flex flex-row gap-2">
<LockIcon className="size-4 text-muted-foreground" />?
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Private Repository</DialogTitle>
<DialogDescription>
If your repository is private is necessary to
generate SSH Keys to add to your git provider.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="relative">
<Textarea
placeholder="Please click on Generate SSH Key"
className="no-scrollbar h-64 text-muted-foreground"
disabled={!data?.customGitSSHKey}
contentEditable={false}
value={
data?.customGitSSHKey ||
"Please click on Generate SSH Key"
}
/>
<button
type="button"
className="absolute right-2 top-2"
onClick={() => {
copy(
data?.customGitSSHKey ||
"Generate a SSH Key",
);
toast.success("SSH Copied to clipboard");
}}
<div className="flex items-end col-span-2 gap-4">
<div className="grow">
<FormField
control={form.control}
name="repositoryURL"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row justify-between">
Repository URL
</FormLabel>
<FormControl>
<Input placeholder="git@bitbucket.org" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{sshKeys && sshKeys.length > 0 ? (
<FormField
control={form.control}
name="sshKey"
render={({ field }) => (
<FormItem className="basis-40">
<FormLabel className="w-full inline-flex justify-between">
SSH Key
<LockIcon className="size-4 text-muted-foreground" />
</FormLabel>
<FormControl>
<Select
key={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
<CopyIcon className="size-4" />
</button>
</div>
</div>
<DialogFooter className="flex sm:justify-between gap-3.5 flex-col sm:flex-col w-full">
<div className="flex flex-row gap-2 w-full justify-between flex-wrap">
{data?.customGitSSHKey && (
<Button
variant="destructive"
isLoading={
isGeneratingSSHKey || isRemovingSSHKey
}
className="max-sm:w-full"
onClick={async () => {
await removeSSHKey({
composeId,
})
.then(async () => {
toast.success("SSH Key Removed");
await refetch();
})
.catch(() => {
toast.error(
"Error to remove the SSH Key",
);
});
}}
type="button"
>
Remove SSH Key
</Button>
)}
<Button
isLoading={
isGeneratingSSHKey || isRemovingSSHKey
}
className="max-sm:w-full"
onClick={async () => {
await generateSSHKey({
composeId,
})
.then(async () => {
toast.success("SSH Key Generated");
await refetch();
})
.catch(() => {
toast.error(
"Error to generate the SSH Key",
);
});
}}
type="button"
>
Generate SSH Key
</Button>
</div>
<span className="text-sm text-muted-foreground">
Is recommended to remove the SSH Key if you want
to deploy a public repository.
</span>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</FormLabel>
<FormControl>
<Input placeholder="git@bitbucket.org" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{sshKey.name}
</SelectItem>
))}
<SelectItem value="none">None</SelectItem>
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
) : (
<Button
variant="secondary"
onClick={() => router.push("/dashboard/settings/ssh-keys")}
type="button"
>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
)}
</div>
<div className="space-y-4">
<FormField

View File

@ -0,0 +1,8 @@
ALTER TABLE "compose" ADD COLUMN "customGitSSHKeyId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "compose" ADD CONSTRAINT "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk" FOREIGN KEY ("customGitSSHKeyId") REFERENCES "public"."ssh-key"("sshKeyId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "compose" DROP COLUMN IF EXISTS "customGitSSHKey";

File diff suppressed because it is too large Load Diff

View File

@ -190,6 +190,13 @@
"when": 1721928706816,
"tag": "0026_yielding_king_cobra",
"breakpoints": true
},
{
"idx": 27,
"version": "6",
"when": 1721937297064,
"tag": "0027_fantastic_squadron_sinister",
"breakpoints": true
}
]
}

View File

@ -31,11 +31,6 @@ import {
removeDirectoryCode,
removeMonitoringDirectory,
} from "@/server/utils/filesystem/directory";
import {
generateSSHKey,
readSSHPublicKey,
removeSSHKey,
} from "@/server/utils/filesystem/ssh";
import {
readConfig,
removeTraefikConfig,
@ -130,7 +125,6 @@ export const applicationRouter = createTRPCRouter({
async () => await removeMonitoringDirectory(application?.appName),
async () => await removeTraefikConfig(application?.appName),
async () => await removeService(application?.appName),
async () => await removeSSHKey(application?.appName),
];
for (const operation of cleanupOperations) {
@ -240,32 +234,6 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle",
});
return true;
}),
generateSSHKey: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
try {
await generateSSHKey(application.appName);
const file = await readSSHPublicKey(application.appName);
// await updateApplication(input.applicationId, {
// customGitSSHKey: file,
// });
} catch (error) {}
return true;
}),
removeSSHKey: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
await removeSSHKey(application.appName);
// await updateApplication(input.applicationId, {
// customGitSSHKey: null,
// });
return true;
}),
markRunning: protectedProcedure

View File

@ -16,11 +16,6 @@ import { myQueue } from "@/server/queues/queueSetup";
import { createCommand } from "@/server/utils/builders/compose";
import { randomizeComposeFile } from "@/server/utils/docker/compose";
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
import {
generateSSHKey,
readSSHPublicKey,
removeSSHKey,
} from "@/server/utils/filesystem/ssh";
import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import {
@ -102,7 +97,6 @@ export const composeRouter = createTRPCRouter({
async () => await removeCompose(composeResult),
async () => await removeDeploymentsByComposeId(composeResult),
async () => await removeComposeDirectory(composeResult.appName),
async () => await removeSSHKey(composeResult.appName),
];
for (const operation of cleanupOperations) {
@ -181,38 +175,12 @@ export const composeRouter = createTRPCRouter({
const command = createCommand(compose);
return `docker ${command}`;
}),
generateSSHKey: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input }) => {
const compose = await findComposeById(input.composeId);
try {
await generateSSHKey(compose.appName);
const file = await readSSHPublicKey(compose.appName);
await updateCompose(input.composeId, {
customGitSSHKey: file,
});
} catch (error) {}
return true;
}),
refreshToken: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input }) => {
await updateCompose(input.composeId, {
refreshToken: nanoid(),
});
return true;
}),
removeSSHKey: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input }) => {
const compose = await findComposeById(input.composeId);
await removeSSHKey(compose.appName);
await updateCompose(input.composeId, {
customGitSSHKey: null,
});
return true;
}),
deployTemplate: protectedProcedure

View File

@ -1,3 +1,4 @@
import { sshKeys } from "@/server/db/schema/ssh-key";
import { generatePassword } from "@/templates/utils";
import { relations } from "drizzle-orm";
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
@ -41,7 +42,12 @@ export const compose = pgTable("compose", {
// Git
customGitUrl: text("customGitUrl"),
customGitBranch: text("customGitBranch"),
customGitSSHKey: text("customGitSSHKey"),
customGitSSHKeyId: text("customGitSSHKeyId").references(
() => sshKeys.sshKeyId,
{
onDelete: "set null",
},
),
//
command: text("command").notNull().default(""),
//
@ -62,6 +68,10 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
}),
deployments: many(deployments),
mounts: many(mounts),
customGitSSHKey: one(sshKeys, {
fields: [compose.customGitSSHKeyId],
references: [sshKeys.sshKeyId],
}),
}));
const createSchema = createInsertSchema(compose, {
@ -70,6 +80,7 @@ const createSchema = createInsertSchema(compose, {
env: z.string().optional(),
composeFile: z.string().min(1),
projectId: z.string(),
customGitSSHKeyId: z.string().optional(),
command: z.string().optional(),
composePath: z.string().min(1),
composeType: z.enum(["docker-compose", "stack"]).optional(),

View File

@ -1,4 +1,5 @@
import { applications } from "@/server/db/schema/application";
import { compose } from "@/server/db/schema/compose";
import { sshKeyCreate } from "@/server/db/validations";
import { relations } from "drizzle-orm";
import { pgTable, text, time } from "drizzle-orm/pg-core";
@ -21,6 +22,7 @@ export const sshKeys = pgTable("ssh-key", {
export const sshKeysRelations = relations(sshKeys, ({ many }) => ({
applications: many(applications),
compose: many(compose),
}));
const createSchema = createInsertSchema(

View File

@ -39,7 +39,7 @@ export const cloneGitRepository = async (
writeStream.write(
`\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`,
);
console.log(customGitSSHKeyId);
await spawnAsync(
"git",
[