mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: compose app
This commit is contained in:
parent
1f81ebd4fe
commit
c681aa2e9f
@ -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>
|
||||
|
@ -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
|
||||
|
8
drizzle/0027_fantastic_squadron_sinister.sql
Normal file
8
drizzle/0027_fantastic_squadron_sinister.sql
Normal 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";
|
3010
drizzle/meta/0027_snapshot.json
Normal file
3010
drizzle/meta/0027_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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(
|
||||
|
@ -39,7 +39,7 @@ export const cloneGitRepository = async (
|
||||
writeStream.write(
|
||||
`\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`,
|
||||
);
|
||||
console.log(customGitSSHKeyId);
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
|
Loading…
Reference in New Issue
Block a user