mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: agroupate utilities
This commit is contained in:
@@ -15,7 +15,8 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||||
import { RandomizeCompose } from "./randomize-compose";
|
import { RandomizeCompose } from "./randomize-compose";
|
||||||
import { RandomizeDeployable } from "./randomize-deployable";
|
import { RandomizeDeployable } from "./isolated-deployment";
|
||||||
|
import { ShowUtilities } from "./show-utilities";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
@@ -126,8 +127,7 @@ services:
|
|||||||
</Form>
|
</Form>
|
||||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
||||||
<RandomizeCompose composeId={composeId} />
|
<ShowUtilities composeId={composeId} />
|
||||||
<RandomizeDeployable composeId={composeId} />
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
isolatedDeployment: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const IsolatedDeployment = ({ composeId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [compose, setCompose] = useState<string>("");
|
||||||
|
const { mutateAsync, error, isError } =
|
||||||
|
api.compose.isolatedDeployment.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
||||||
|
|
||||||
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
|
{ composeId },
|
||||||
|
{ enabled: !!composeId },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
isolatedDeployment: false,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
randomizeCompose();
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
isolatedDeployment: data?.isolatedDeployment || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: Schema) => {
|
||||||
|
await updateCompose({
|
||||||
|
composeId,
|
||||||
|
isolatedDeployment: formData?.isolatedDeployment || false,
|
||||||
|
})
|
||||||
|
.then(async (data) => {
|
||||||
|
randomizeCompose();
|
||||||
|
refetch();
|
||||||
|
toast.success("Compose updated");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating the compose");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const randomizeCompose = async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
suffix: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(async (data) => {
|
||||||
|
await utils.project.all.invalidate();
|
||||||
|
setCompose(data);
|
||||||
|
toast.success("Compose Isolated");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error isolating the compose");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Isolate Deployment</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Use this option to isolate the deployment of this compose file.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||||
|
<span>
|
||||||
|
This feature creates an isolated environment for your deployment by
|
||||||
|
adding unique prefixes to all resources. It establishes a dedicated
|
||||||
|
network based on your compose file's name, ensuring your services run
|
||||||
|
in isolation. This prevents conflicts when running multiple instances
|
||||||
|
of the same template or services with identical names.
|
||||||
|
</span>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">
|
||||||
|
Resources that will be isolated:
|
||||||
|
</h4>
|
||||||
|
<ul className="list-disc list-inside">
|
||||||
|
<li>Docker volumes</li>
|
||||||
|
<li>Docker networks</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
id="hook-form-add-project"
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
{isError && (
|
||||||
|
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||||
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{error?.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
||||||
|
<div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isolatedDeployment"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Enable Randomize ({data?.appName})</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Enable randomize to the compose file.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||||
|
<Button
|
||||||
|
form="hook-form-add-project"
|
||||||
|
type="submit"
|
||||||
|
className="lg:w-fit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Label>Preview</Label>
|
||||||
|
<pre>
|
||||||
|
<CodeEditor
|
||||||
|
value={compose || ""}
|
||||||
|
language="yaml"
|
||||||
|
readOnly
|
||||||
|
height="50rem"
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -20,11 +16,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
|
||||||
InputOTP,
|
|
||||||
InputOTPGroup,
|
|
||||||
InputOTPSlot,
|
|
||||||
} from "@/components/ui/input-otp";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -70,6 +61,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
const suffix = form.watch("suffix");
|
const suffix = form.watch("suffix");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
randomizeCompose();
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
suffix: data?.suffix || "",
|
suffix: data?.suffix || "",
|
||||||
@@ -110,126 +102,117 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<div className="w-full">
|
||||||
<DialogTrigger asChild onClick={() => randomizeCompose()}>
|
<DialogHeader>
|
||||||
<Button className="max-lg:w-full" variant="outline">
|
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
|
||||||
<Dices className="h-4 w-4" />
|
<DialogDescription>
|
||||||
Randomize Compose
|
Use this in case you want to deploy the same compose file and you have
|
||||||
</Button>
|
conflicts with some property like volumes, networks, etc.
|
||||||
</DialogTrigger>
|
</DialogDescription>
|
||||||
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
</DialogHeader>
|
||||||
<DialogHeader>
|
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||||
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
|
<span>
|
||||||
<DialogDescription>
|
This will randomize the compose file and will add a suffix to the
|
||||||
Use this in case you want to deploy the same compose file and you
|
property to avoid conflicts
|
||||||
have conflicts with some property like volumes, networks, etc.
|
</span>
|
||||||
</DialogDescription>
|
<ul className="list-disc list-inside">
|
||||||
</DialogHeader>
|
<li>volumes</li>
|
||||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
<li>networks</li>
|
||||||
<span>
|
<li>services</li>
|
||||||
This will randomize the compose file and will add a suffix to the
|
<li>configs</li>
|
||||||
property to avoid conflicts
|
<li>secrets</li>
|
||||||
</span>
|
</ul>
|
||||||
<ul className="list-disc list-inside">
|
<AlertBlock type="info">
|
||||||
<li>volumes</li>
|
When you activate this option, we will include a env `COMPOSE_PREFIX`
|
||||||
<li>networks</li>
|
variable to the compose file so you can use it in your compose file.
|
||||||
<li>services</li>
|
</AlertBlock>
|
||||||
<li>configs</li>
|
</div>
|
||||||
<li>secrets</li>
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
</ul>
|
<Form {...form}>
|
||||||
<AlertBlock type="info">
|
<form
|
||||||
When you activate this option, we will include a env
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
`COMPOSE_PREFIX` variable to the compose file so you can use it in
|
id="hook-form-add-project"
|
||||||
your compose file.
|
className="grid w-full gap-4"
|
||||||
</AlertBlock>
|
>
|
||||||
</div>
|
{isError && (
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||||
<Form {...form}>
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
<form
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
{error?.message}
|
||||||
id="hook-form-add-project"
|
</span>
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
|
||||||
<div>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="suffix"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col justify-center max-sm:items-center w-full">
|
|
||||||
<FormLabel>Suffix</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter a suffix (Optional, example: prod)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="randomize"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Apply Randomize</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Apply randomize to the compose file.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
|
||||||
<Button
|
|
||||||
form="hook-form-add-project"
|
|
||||||
type="submit"
|
|
||||||
className="lg:w-fit"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={async () => {
|
|
||||||
await randomizeCompose();
|
|
||||||
}}
|
|
||||||
className="lg:w-fit"
|
|
||||||
>
|
|
||||||
Random
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<pre>
|
)}
|
||||||
<CodeEditor
|
|
||||||
value={compose || ""}
|
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
||||||
language="yaml"
|
<div>
|
||||||
readOnly
|
<FormField
|
||||||
height="50rem"
|
control={form.control}
|
||||||
|
name="suffix"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col justify-center max-sm:items-center w-full mt-4">
|
||||||
|
<FormLabel>Suffix</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a suffix (Optional, example: prod)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</pre>
|
<FormField
|
||||||
</form>
|
control={form.control}
|
||||||
</Form>
|
name="randomize"
|
||||||
</DialogContent>
|
render={({ field }) => (
|
||||||
</Dialog>
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Apply Randomize</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Apply randomize to the compose file.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||||
|
<Button
|
||||||
|
form="hook-form-add-project"
|
||||||
|
type="submit"
|
||||||
|
className="lg:w-fit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await randomizeCompose();
|
||||||
|
}}
|
||||||
|
className="lg:w-fit"
|
||||||
|
>
|
||||||
|
Random
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre>
|
||||||
|
<CodeEditor
|
||||||
|
value={compose || ""}
|
||||||
|
language="yaml"
|
||||||
|
readOnly
|
||||||
|
height="50rem"
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { AlertTriangle, Dices } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
deployable: z.boolean().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Schema = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
export const RandomizeDeployable = ({ composeId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [compose, setCompose] = useState<string>("");
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { mutateAsync, error, isError } =
|
|
||||||
api.compose.randomizeDeployableCompose.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
|
||||||
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
|
||||||
{ composeId },
|
|
||||||
{ enabled: !!composeId },
|
|
||||||
);
|
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
|
||||||
defaultValues: {
|
|
||||||
deployable: false,
|
|
||||||
},
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
deployable: data?.deployable || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (formData: Schema) => {
|
|
||||||
await updateCompose({
|
|
||||||
composeId,
|
|
||||||
deployable: formData?.deployable || false,
|
|
||||||
})
|
|
||||||
.then(async (data) => {
|
|
||||||
randomizeCompose();
|
|
||||||
refetch();
|
|
||||||
toast.success("Compose updated");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error randomizing the compose");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const randomizeCompose = async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
composeId,
|
|
||||||
suffix: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(async (data) => {
|
|
||||||
await utils.project.all.invalidate();
|
|
||||||
setCompose(data);
|
|
||||||
toast.success("Compose randomized");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error randomizing the compose");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild onClick={() => randomizeCompose()}>
|
|
||||||
<Button className="max-lg:w-full" variant="outline">
|
|
||||||
<Dices className="h-4 w-4" />
|
|
||||||
Randomize Deployable
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Randomize Deployable (Experimental)</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Use this in case you want to deploy the same compose file twice.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
|
||||||
<span>
|
|
||||||
This will randomize the compose file and will add a suffix to the
|
|
||||||
property to avoid conflicts
|
|
||||||
</span>
|
|
||||||
<ul className="list-disc list-inside">
|
|
||||||
<li>volumes</li>
|
|
||||||
<li>networks</li>
|
|
||||||
</ul>
|
|
||||||
<AlertBlock type="info">
|
|
||||||
When you activate this option, we will include a env
|
|
||||||
`DOKPLOY_SUFFIX` variable to the compose file so you can use it in
|
|
||||||
your compose file, also we don't include the{" "}
|
|
||||||
<code>dokploy-network</code> to any of the services by default.
|
|
||||||
</AlertBlock>
|
|
||||||
</div>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
id="hook-form-add-project"
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
|
||||||
<div>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="deployable"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Apply Randomize ({data?.appName})</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Apply randomize to the compose file.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
|
||||||
<Button
|
|
||||||
form="hook-form-add-project"
|
|
||||||
type="submit"
|
|
||||||
className="lg:w-fit"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<pre>
|
|
||||||
<CodeEditor
|
|
||||||
value={compose || ""}
|
|
||||||
language="yaml"
|
|
||||||
readOnly
|
|
||||||
height="50rem"
|
|
||||||
/>
|
|
||||||
</pre>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { IsolatedDeployment } from "./isolated-deployment";
|
||||||
|
import { RandomizeCompose } from "./randomize-compose";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowUtilities = ({ composeId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost">Show Utilities</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Utilities </DialogTitle>
|
||||||
|
<DialogDescription>Modify the application data</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Tabs defaultValue="isolated">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="isolated">Isolated Deployment</TabsTrigger>
|
||||||
|
<TabsTrigger value="randomize">Randomize Compose</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="randomize" className="pt-5">
|
||||||
|
<RandomizeCompose composeId={composeId} />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="isolated" className="pt-5">
|
||||||
|
<IsolatedDeployment composeId={composeId} />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
apps/dokploy/drizzle/0065_daily_zaladane.sql
Normal file
1
apps/dokploy/drizzle/0065_daily_zaladane.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "compose" RENAME COLUMN "deployable" TO "isolatedDeployment";
|
||||||
4485
apps/dokploy/drizzle/meta/0065_snapshot.json
Normal file
4485
apps/dokploy/drizzle/meta/0065_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -456,6 +456,13 @@
|
|||||||
"when": 1738564387043,
|
"when": 1738564387043,
|
||||||
"tag": "0064_previous_agent_brand",
|
"tag": "0064_previous_agent_brand",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 65,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739087857244,
|
||||||
|
"tag": "0065_daily_zaladane",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
apiCreateCompose,
|
apiCreateCompose,
|
||||||
apiCreateComposeByTemplate,
|
apiCreateComposeByTemplate,
|
||||||
apiDeleteCompose,
|
apiDeleteCompose,
|
||||||
apiFetchServices,
|
apiFetchServices,
|
||||||
apiFindCompose,
|
apiFindCompose,
|
||||||
apiRandomizeCompose,
|
apiRandomizeCompose,
|
||||||
apiUpdateCompose,
|
apiUpdateCompose,
|
||||||
compose,
|
compose,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
import {
|
import {
|
||||||
generatePassword,
|
generatePassword,
|
||||||
loadTemplateModule,
|
loadTemplateModule,
|
||||||
readTemplateComposeFile,
|
readTemplateComposeFile,
|
||||||
} from "@/templates/utils";
|
} from "@/templates/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -28,443 +28,443 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
|
|||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
addDomainToCompose,
|
addDomainToCompose,
|
||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
cloneCompose,
|
cloneCompose,
|
||||||
cloneComposeRemote,
|
cloneComposeRemote,
|
||||||
createCommand,
|
createCommand,
|
||||||
createCompose,
|
createCompose,
|
||||||
createComposeByTemplate,
|
createComposeByTemplate,
|
||||||
createDomain,
|
createDomain,
|
||||||
createMount,
|
createMount,
|
||||||
findAdminById,
|
findAdminById,
|
||||||
findComposeById,
|
findComposeById,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findServerById,
|
findServerById,
|
||||||
loadServices,
|
loadServices,
|
||||||
randomizeComposeFile,
|
randomizeComposeFile,
|
||||||
randomizeDeployableComposeFile,
|
randomizeIsolatedDeploymentComposeFile,
|
||||||
removeCompose,
|
removeCompose,
|
||||||
removeComposeDirectory,
|
removeComposeDirectory,
|
||||||
removeDeploymentsByComposeId,
|
removeDeploymentsByComposeId,
|
||||||
startCompose,
|
startCompose,
|
||||||
stopCompose,
|
stopCompose,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
|
||||||
export const composeRouter = createTRPCRouter({
|
export const composeRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateCompose)
|
.input(apiCreateCompose)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You need to use a server to create a compose",
|
message: "You need to use a server to create a compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.adminId !== ctx.user.adminId) {
|
if (project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newService = await createCompose(input);
|
const newService = await createCompose(input);
|
||||||
|
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await addNewService(ctx.user.authId, newService.composeId);
|
await addNewService(ctx.user.authId, newService.composeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newService;
|
return newService;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.composeId, "access");
|
await checkServiceAccess(ctx.user.authId, input.composeId, "access");
|
||||||
}
|
}
|
||||||
|
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this compose",
|
message: "You are not authorized to access this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return compose;
|
return compose;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateCompose)
|
.input(apiUpdateCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to update this compose",
|
message: "You are not authorized to update this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return updateCompose(input.composeId, input);
|
return updateCompose(input.composeId, input);
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiDeleteCompose)
|
.input(apiDeleteCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
|
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
|
||||||
}
|
}
|
||||||
const composeResult = await findComposeById(input.composeId);
|
const composeResult = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (composeResult.project.adminId !== ctx.user.adminId) {
|
if (composeResult.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to delete this compose",
|
message: "You are not authorized to delete this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
4;
|
4;
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.delete(compose)
|
.delete(compose)
|
||||||
.where(eq(compose.composeId, input.composeId))
|
.where(eq(compose.composeId, input.composeId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
async () => await removeCompose(composeResult, input.deleteVolumes),
|
async () => await removeCompose(composeResult, input.deleteVolumes),
|
||||||
async () => await removeDeploymentsByComposeId(composeResult),
|
async () => await removeDeploymentsByComposeId(composeResult),
|
||||||
async () => await removeComposeDirectory(composeResult.appName),
|
async () => await removeComposeDirectory(composeResult.appName),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const operation of cleanupOperations) {
|
for (const operation of cleanupOperations) {
|
||||||
try {
|
try {
|
||||||
await operation();
|
await operation();
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result[0];
|
return result[0];
|
||||||
}),
|
}),
|
||||||
cleanQueues: protectedProcedure
|
cleanQueues: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to clean this compose",
|
message: "You are not authorized to clean this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await cleanQueuesByCompose(input.composeId);
|
await cleanQueuesByCompose(input.composeId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
loadServices: protectedProcedure
|
loadServices: protectedProcedure
|
||||||
.input(apiFetchServices)
|
.input(apiFetchServices)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to load this compose",
|
message: "You are not authorized to load this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await loadServices(input.composeId, input.type);
|
return await loadServices(input.composeId, input.type);
|
||||||
}),
|
}),
|
||||||
fetchSourceType: protectedProcedure
|
fetchSourceType: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to fetch this compose",
|
message: "You are not authorized to fetch this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await cloneComposeRemote(compose);
|
await cloneComposeRemote(compose);
|
||||||
} else {
|
} else {
|
||||||
await cloneCompose(compose);
|
await cloneCompose(compose);
|
||||||
}
|
}
|
||||||
return compose.sourceType;
|
return compose.sourceType;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error fetching source type",
|
message: "Error fetching source type",
|
||||||
cause: err,
|
cause: err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
randomizeCompose: protectedProcedure
|
randomizeCompose: protectedProcedure
|
||||||
.input(apiRandomizeCompose)
|
.input(apiRandomizeCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to randomize this compose",
|
message: "You are not authorized to randomize this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await randomizeComposeFile(input.composeId, input.suffix);
|
return await randomizeComposeFile(input.composeId, input.suffix);
|
||||||
}),
|
}),
|
||||||
randomizeDeployableCompose: protectedProcedure
|
isolatedDeployment: protectedProcedure
|
||||||
.input(apiRandomizeCompose)
|
.input(apiRandomizeCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to randomize this compose",
|
message: "You are not authorized to randomize this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await randomizeDeployableComposeFile(
|
return await randomizeIsolatedDeploymentComposeFile(
|
||||||
input.composeId,
|
input.composeId,
|
||||||
input.suffix,
|
input.suffix
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
getConvertedCompose: protectedProcedure
|
getConvertedCompose: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to get this compose",
|
message: "You are not authorized to get this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const domains = await findDomainsByComposeId(input.composeId);
|
const domains = await findDomainsByComposeId(input.composeId);
|
||||||
const composeFile = await addDomainToCompose(compose, domains);
|
const composeFile = await addDomainToCompose(compose, domains);
|
||||||
return dump(composeFile, {
|
return dump(composeFile, {
|
||||||
lineWidth: 1000,
|
lineWidth: 1000,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this compose",
|
message: "You are not authorized to deploy this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: "Manual deployment",
|
titleLog: "Manual deployment",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
server: !!compose.serverId,
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_CLOUD && compose.serverId) {
|
if (IS_CLOUD && compose.serverId) {
|
||||||
jobData.serverId = compose.serverId;
|
jobData.serverId = compose.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
{ ...jobData },
|
{ ...jobData },
|
||||||
{
|
{
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to redeploy this compose",
|
message: "You are not authorized to redeploy this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: "Rebuild deployment",
|
titleLog: "Rebuild deployment",
|
||||||
type: "redeploy",
|
type: "redeploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
server: !!compose.serverId,
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
if (IS_CLOUD && compose.serverId) {
|
if (IS_CLOUD && compose.serverId) {
|
||||||
jobData.serverId = compose.serverId;
|
jobData.serverId = compose.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
{ ...jobData },
|
{ ...jobData },
|
||||||
{
|
{
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this compose",
|
message: "You are not authorized to stop this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await stopCompose(input.composeId);
|
await stopCompose(input.composeId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
start: protectedProcedure
|
start: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this compose",
|
message: "You are not authorized to stop this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await startCompose(input.composeId);
|
await startCompose(input.composeId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
getDefaultCommand: protectedProcedure
|
getDefaultCommand: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to get this compose",
|
message: "You are not authorized to get this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const command = createCommand(compose);
|
const command = createCommand(compose);
|
||||||
return `docker ${command}`;
|
return `docker ${command}`;
|
||||||
}),
|
}),
|
||||||
refreshToken: protectedProcedure
|
refreshToken: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (compose.project.adminId !== ctx.user.adminId) {
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to refresh this compose",
|
message: "You are not authorized to refresh this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await updateCompose(input.composeId, {
|
await updateCompose(input.composeId, {
|
||||||
refreshToken: nanoid(),
|
refreshToken: nanoid(),
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
deployTemplate: protectedProcedure
|
deployTemplate: protectedProcedure
|
||||||
.input(apiCreateComposeByTemplate)
|
.input(apiCreateComposeByTemplate)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You need to use a server to create a compose",
|
message: "You need to use a server to create a compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const composeFile = await readTemplateComposeFile(input.id);
|
const composeFile = await readTemplateComposeFile(input.id);
|
||||||
|
|
||||||
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
||||||
|
|
||||||
const admin = await findAdminById(ctx.user.adminId);
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
let serverIp = admin.serverIp || "127.0.0.1";
|
let serverIp = admin.serverIp || "127.0.0.1";
|
||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
if (input.serverId) {
|
if (input.serverId) {
|
||||||
const server = await findServerById(input.serverId);
|
const server = await findServerById(input.serverId);
|
||||||
serverIp = server.ipAddress;
|
serverIp = server.ipAddress;
|
||||||
} else if (process.env.NODE_ENV === "development") {
|
} else if (process.env.NODE_ENV === "development") {
|
||||||
serverIp = "127.0.0.1";
|
serverIp = "127.0.0.1";
|
||||||
}
|
}
|
||||||
const projectName = slugify(`${project.name} ${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
const { envs, mounts, domains } = generate({
|
const { envs, mounts, domains } = generate({
|
||||||
serverIp: serverIp || "",
|
serverIp: serverIp || "",
|
||||||
projectName: projectName,
|
projectName: projectName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const compose = await createComposeByTemplate({
|
const compose = await createComposeByTemplate({
|
||||||
...input,
|
...input,
|
||||||
composeFile: composeFile,
|
composeFile: composeFile,
|
||||||
env: envs?.join("\n"),
|
env: envs?.join("\n"),
|
||||||
serverId: input.serverId,
|
serverId: input.serverId,
|
||||||
name: input.id,
|
name: input.id,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
appName: `${projectName}-${generatePassword(6)}`,
|
appName: `${projectName}-${generatePassword(6)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await addNewService(ctx.user.authId, compose.composeId);
|
await addNewService(ctx.user.authId, compose.composeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounts && mounts?.length > 0) {
|
if (mounts && mounts?.length > 0) {
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
await createMount({
|
await createMount({
|
||||||
filePath: mount.filePath,
|
filePath: mount.filePath,
|
||||||
mountPath: "",
|
mountPath: "",
|
||||||
content: mount.content,
|
content: mount.content,
|
||||||
serviceId: compose.composeId,
|
serviceId: compose.composeId,
|
||||||
serviceType: "compose",
|
serviceType: "compose",
|
||||||
type: "file",
|
type: "file",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domains && domains?.length > 0) {
|
if (domains && domains?.length > 0) {
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
await createDomain({
|
await createDomain({
|
||||||
...domain,
|
...domain,
|
||||||
domainType: "compose",
|
domainType: "compose",
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
composeId: compose.composeId,
|
composeId: compose.composeId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
templates: protectedProcedure.query(async () => {
|
templates: protectedProcedure.query(async () => {
|
||||||
const templatesData = templates.map((t) => ({
|
const templatesData = templates.map((t) => ({
|
||||||
name: t.name,
|
name: t.name,
|
||||||
description: t.description,
|
description: t.description,
|
||||||
id: t.id,
|
id: t.id,
|
||||||
links: t.links,
|
links: t.links,
|
||||||
tags: t.tags,
|
tags: t.tags,
|
||||||
logo: t.logo,
|
logo: t.logo,
|
||||||
version: t.version,
|
version: t.version,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return templatesData;
|
return templatesData;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getTags: protectedProcedure.query(async ({ input }) => {
|
getTags: protectedProcedure.query(async ({ input }) => {
|
||||||
const allTags = templates.flatMap((template) => template.tags);
|
const allTags = templates.flatMap((template) => template.tags);
|
||||||
const uniqueTags = _.uniq(allTags);
|
const uniqueTags = _.uniq(allTags);
|
||||||
return uniqueTags;
|
return uniqueTags;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,170 +16,170 @@ import { sshKeys } from "./ssh-key";
|
|||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
|
||||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||||
"git",
|
"git",
|
||||||
"github",
|
"github",
|
||||||
"gitlab",
|
"gitlab",
|
||||||
"bitbucket",
|
"bitbucket",
|
||||||
"raw",
|
"raw",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const composeType = pgEnum("composeType", ["docker-compose", "stack"]);
|
export const composeType = pgEnum("composeType", ["docker-compose", "stack"]);
|
||||||
|
|
||||||
export const compose = pgTable("compose", {
|
export const compose = pgTable("compose", {
|
||||||
composeId: text("composeId")
|
composeId: text("composeId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => nanoid()),
|
.$defaultFn(() => nanoid()),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
appName: text("appName")
|
appName: text("appName")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => generateAppName("compose")),
|
.$defaultFn(() => generateAppName("compose")),
|
||||||
description: text("description"),
|
description: text("description"),
|
||||||
env: text("env"),
|
env: text("env"),
|
||||||
composeFile: text("composeFile").notNull().default(""),
|
composeFile: text("composeFile").notNull().default(""),
|
||||||
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
||||||
sourceType: sourceTypeCompose("sourceType").notNull().default("github"),
|
sourceType: sourceTypeCompose("sourceType").notNull().default("github"),
|
||||||
composeType: composeType("composeType").notNull().default("docker-compose"),
|
composeType: composeType("composeType").notNull().default("docker-compose"),
|
||||||
// Github
|
// Github
|
||||||
repository: text("repository"),
|
repository: text("repository"),
|
||||||
owner: text("owner"),
|
owner: text("owner"),
|
||||||
branch: text("branch"),
|
branch: text("branch"),
|
||||||
autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
|
autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
|
||||||
// Gitlab
|
// Gitlab
|
||||||
gitlabProjectId: integer("gitlabProjectId"),
|
gitlabProjectId: integer("gitlabProjectId"),
|
||||||
gitlabRepository: text("gitlabRepository"),
|
gitlabRepository: text("gitlabRepository"),
|
||||||
gitlabOwner: text("gitlabOwner"),
|
gitlabOwner: text("gitlabOwner"),
|
||||||
gitlabBranch: text("gitlabBranch"),
|
gitlabBranch: text("gitlabBranch"),
|
||||||
gitlabPathNamespace: text("gitlabPathNamespace"),
|
gitlabPathNamespace: text("gitlabPathNamespace"),
|
||||||
// Bitbucket
|
// Bitbucket
|
||||||
bitbucketRepository: text("bitbucketRepository"),
|
bitbucketRepository: text("bitbucketRepository"),
|
||||||
bitbucketOwner: text("bitbucketOwner"),
|
bitbucketOwner: text("bitbucketOwner"),
|
||||||
bitbucketBranch: text("bitbucketBranch"),
|
bitbucketBranch: text("bitbucketBranch"),
|
||||||
// Git
|
// Git
|
||||||
customGitUrl: text("customGitUrl"),
|
customGitUrl: text("customGitUrl"),
|
||||||
customGitBranch: text("customGitBranch"),
|
customGitBranch: text("customGitBranch"),
|
||||||
customGitSSHKeyId: text("customGitSSHKeyId").references(
|
customGitSSHKeyId: text("customGitSSHKeyId").references(
|
||||||
() => sshKeys.sshKeyId,
|
() => sshKeys.sshKeyId,
|
||||||
{
|
{
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
},
|
}
|
||||||
),
|
),
|
||||||
command: text("command").notNull().default(""),
|
command: text("command").notNull().default(""),
|
||||||
//
|
//
|
||||||
composePath: text("composePath").notNull().default("./docker-compose.yml"),
|
composePath: text("composePath").notNull().default("./docker-compose.yml"),
|
||||||
suffix: text("suffix").notNull().default(""),
|
suffix: text("suffix").notNull().default(""),
|
||||||
randomize: boolean("randomize").notNull().default(false),
|
randomize: boolean("randomize").notNull().default(false),
|
||||||
deployable: boolean("deployable").notNull().default(false),
|
isolatedDeployment: boolean("isolatedDeployment").notNull().default(false),
|
||||||
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
||||||
projectId: text("projectId")
|
projectId: text("projectId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
|
|
||||||
githubId: text("githubId").references(() => github.githubId, {
|
githubId: text("githubId").references(() => github.githubId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
|
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const composeRelations = relations(compose, ({ one, many }) => ({
|
export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||||
project: one(projects, {
|
project: one(projects, {
|
||||||
fields: [compose.projectId],
|
fields: [compose.projectId],
|
||||||
references: [projects.projectId],
|
references: [projects.projectId],
|
||||||
}),
|
}),
|
||||||
deployments: many(deployments),
|
deployments: many(deployments),
|
||||||
mounts: many(mounts),
|
mounts: many(mounts),
|
||||||
customGitSSHKey: one(sshKeys, {
|
customGitSSHKey: one(sshKeys, {
|
||||||
fields: [compose.customGitSSHKeyId],
|
fields: [compose.customGitSSHKeyId],
|
||||||
references: [sshKeys.sshKeyId],
|
references: [sshKeys.sshKeyId],
|
||||||
}),
|
}),
|
||||||
domains: many(domains),
|
domains: many(domains),
|
||||||
github: one(github, {
|
github: one(github, {
|
||||||
fields: [compose.githubId],
|
fields: [compose.githubId],
|
||||||
references: [github.githubId],
|
references: [github.githubId],
|
||||||
}),
|
}),
|
||||||
gitlab: one(gitlab, {
|
gitlab: one(gitlab, {
|
||||||
fields: [compose.gitlabId],
|
fields: [compose.gitlabId],
|
||||||
references: [gitlab.gitlabId],
|
references: [gitlab.gitlabId],
|
||||||
}),
|
}),
|
||||||
bitbucket: one(bitbucket, {
|
bitbucket: one(bitbucket, {
|
||||||
fields: [compose.bitbucketId],
|
fields: [compose.bitbucketId],
|
||||||
references: [bitbucket.bitbucketId],
|
references: [bitbucket.bitbucketId],
|
||||||
}),
|
}),
|
||||||
server: one(server, {
|
server: one(server, {
|
||||||
fields: [compose.serverId],
|
fields: [compose.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(compose, {
|
const createSchema = createInsertSchema(compose, {
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
env: z.string().optional(),
|
env: z.string().optional(),
|
||||||
composeFile: z.string().min(1),
|
composeFile: z.string().min(1),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
customGitSSHKeyId: z.string().optional(),
|
customGitSSHKeyId: z.string().optional(),
|
||||||
command: z.string().optional(),
|
command: z.string().optional(),
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateCompose = createSchema.pick({
|
export const apiCreateCompose = createSchema.pick({
|
||||||
name: true,
|
name: true,
|
||||||
description: true,
|
description: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
composeType: true,
|
composeType: true,
|
||||||
appName: true,
|
appName: true,
|
||||||
serverId: true,
|
serverId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateComposeByTemplate = createSchema
|
export const apiCreateComposeByTemplate = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
projectId: true,
|
projectId: true,
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiFindCompose = z.object({
|
export const apiFindCompose = z.object({
|
||||||
composeId: z.string().min(1),
|
composeId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiDeleteCompose = z.object({
|
export const apiDeleteCompose = z.object({
|
||||||
composeId: z.string().min(1),
|
composeId: z.string().min(1),
|
||||||
deleteVolumes: z.boolean(),
|
deleteVolumes: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiFetchServices = z.object({
|
export const apiFetchServices = z.object({
|
||||||
composeId: z.string().min(1),
|
composeId: z.string().min(1),
|
||||||
type: z.enum(["fetch", "cache"]).optional().default("cache"),
|
type: z.enum(["fetch", "cache"]).optional().default("cache"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiUpdateCompose = createSchema
|
export const apiUpdateCompose = createSchema
|
||||||
.partial()
|
.partial()
|
||||||
.extend({
|
.extend({
|
||||||
composeId: z.string(),
|
composeId: z.string(),
|
||||||
composeFile: z.string().optional(),
|
composeFile: z.string().optional(),
|
||||||
command: z.string().optional(),
|
command: z.string().optional(),
|
||||||
})
|
})
|
||||||
.omit({ serverId: true });
|
.omit({ serverId: true });
|
||||||
|
|
||||||
export const apiRandomizeCompose = createSchema
|
export const apiRandomizeCompose = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
composeId: true,
|
composeId: true,
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
suffix: z.string().optional(),
|
suffix: z.string().optional(),
|
||||||
composeId: z.string().min(1),
|
composeId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,117 +1,117 @@
|
|||||||
import {
|
import {
|
||||||
createWriteStream,
|
createWriteStream,
|
||||||
existsSync,
|
existsSync,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
import {
|
import {
|
||||||
writeDomainsToCompose,
|
writeDomainsToCompose,
|
||||||
writeDomainsToComposeRemote,
|
writeDomainsToComposeRemote,
|
||||||
} from "../docker/domain";
|
} from "../docker/domain";
|
||||||
import {
|
import {
|
||||||
encodeBase64,
|
encodeBase64,
|
||||||
getEnviromentVariablesObject,
|
getEnviromentVariablesObject,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
import { spawnAsync } from "../process/spawnAsync";
|
||||||
|
|
||||||
export type ComposeNested = InferResultType<
|
export type ComposeNested = InferResultType<
|
||||||
"compose",
|
"compose",
|
||||||
{ project: true; mounts: true; domains: true }
|
{ project: true; mounts: true; domains: true }
|
||||||
>;
|
>;
|
||||||
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||||
const { sourceType, appName, mounts, composeType, domains } = compose;
|
const { sourceType, appName, mounts, composeType, domains } = compose;
|
||||||
try {
|
try {
|
||||||
const { COMPOSE_PATH } = paths();
|
const { COMPOSE_PATH } = paths();
|
||||||
const command = createCommand(compose);
|
const command = createCommand(compose);
|
||||||
await writeDomainsToCompose(compose, domains);
|
await writeDomainsToCompose(compose, domains);
|
||||||
createEnvFile(compose);
|
createEnvFile(compose);
|
||||||
|
|
||||||
if (compose.deployable) {
|
if (compose.isolatedDeployment) {
|
||||||
await execAsync(
|
await execAsync(
|
||||||
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}`,
|
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const logContent = `
|
const logContent = `
|
||||||
App Name: ${appName}
|
App Name: ${appName}
|
||||||
Build Compose 🐳
|
Build Compose 🐳
|
||||||
Detected: ${mounts.length} mounts 📂
|
Detected: ${mounts.length} mounts 📂
|
||||||
Command: docker ${command}
|
Command: docker ${command}
|
||||||
Source Type: docker ${sourceType} ✅
|
Source Type: docker ${sourceType} ✅
|
||||||
Compose Type: ${composeType} ✅`;
|
Compose Type: ${composeType} ✅`;
|
||||||
const logBox = boxen(logContent, {
|
const logBox = boxen(logContent, {
|
||||||
padding: {
|
padding: {
|
||||||
left: 1,
|
left: 1,
|
||||||
right: 1,
|
right: 1,
|
||||||
bottom: 1,
|
bottom: 1,
|
||||||
},
|
},
|
||||||
width: 80,
|
width: 80,
|
||||||
borderStyle: "double",
|
borderStyle: "double",
|
||||||
});
|
});
|
||||||
writeStream.write(`\n${logBox}\n`);
|
writeStream.write(`\n${logBox}\n`);
|
||||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||||
|
|
||||||
await spawnAsync(
|
await spawnAsync(
|
||||||
"docker",
|
"docker",
|
||||||
[...command.split(" ")],
|
[...command.split(" ")],
|
||||||
(data) => {
|
(data) => {
|
||||||
if (writeStream.writable) {
|
if (writeStream.writable) {
|
||||||
writeStream.write(data.toString());
|
writeStream.write(data.toString());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
PATH: process.env.PATH,
|
PATH: process.env.PATH,
|
||||||
...(composeType === "stack" && {
|
...(composeType === "stack" && {
|
||||||
...getEnviromentVariablesObject(compose.env, compose.project.env),
|
...getEnviromentVariablesObject(compose.env, compose.project.env),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (compose.deployable) {
|
if (compose.isolatedDeployment) {
|
||||||
await execAsync(
|
await execAsync(
|
||||||
`docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1`,
|
`docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeStream.write("Docker Compose Deployed: ✅");
|
writeStream.write("Docker Compose Deployed: ✅");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeStream.write(`Error ❌ ${(error as Error).message}`);
|
writeStream.write(`Error ❌ ${(error as Error).message}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
writeStream.end();
|
writeStream.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBuildComposeCommand = async (
|
export const getBuildComposeCommand = async (
|
||||||
compose: ComposeNested,
|
compose: ComposeNested,
|
||||||
logPath: string,
|
logPath: string
|
||||||
) => {
|
) => {
|
||||||
const { COMPOSE_PATH } = paths(true);
|
const { COMPOSE_PATH } = paths(true);
|
||||||
const { sourceType, appName, mounts, composeType, domains, composePath } =
|
const { sourceType, appName, mounts, composeType, domains, composePath } =
|
||||||
compose;
|
compose;
|
||||||
const command = createCommand(compose);
|
const command = createCommand(compose);
|
||||||
const envCommand = getCreateEnvFileCommand(compose);
|
const envCommand = getCreateEnvFileCommand(compose);
|
||||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||||
const exportEnvCommand = getExportEnvCommand(compose);
|
const exportEnvCommand = getExportEnvCommand(compose);
|
||||||
|
|
||||||
const newCompose = await writeDomainsToComposeRemote(
|
const newCompose = await writeDomainsToComposeRemote(
|
||||||
compose,
|
compose,
|
||||||
domains,
|
domains,
|
||||||
logPath,
|
logPath
|
||||||
);
|
);
|
||||||
const logContent = `
|
const logContent = `
|
||||||
App Name: ${appName}
|
App Name: ${appName}
|
||||||
Build Compose 🐳
|
Build Compose 🐳
|
||||||
Detected: ${mounts.length} mounts 📂
|
Detected: ${mounts.length} mounts 📂
|
||||||
@@ -119,17 +119,17 @@ Command: docker ${command}
|
|||||||
Source Type: docker ${sourceType} ✅
|
Source Type: docker ${sourceType} ✅
|
||||||
Compose Type: ${composeType} ✅`;
|
Compose Type: ${composeType} ✅`;
|
||||||
|
|
||||||
const logBox = boxen(logContent, {
|
const logBox = boxen(logContent, {
|
||||||
padding: {
|
padding: {
|
||||||
left: 1,
|
left: 1,
|
||||||
right: 1,
|
right: 1,
|
||||||
bottom: 1,
|
bottom: 1,
|
||||||
},
|
},
|
||||||
width: 80,
|
width: 80,
|
||||||
borderStyle: "double",
|
borderStyle: "double",
|
||||||
});
|
});
|
||||||
|
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
set -e
|
set -e
|
||||||
{
|
{
|
||||||
echo "${logBox}" >> "${logPath}"
|
echo "${logBox}" >> "${logPath}"
|
||||||
@@ -141,9 +141,9 @@ Compose Type: ${composeType} ✅`;
|
|||||||
cd "${projectPath}";
|
cd "${projectPath}";
|
||||||
|
|
||||||
${exportEnvCommand}
|
${exportEnvCommand}
|
||||||
${compose.deployable ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
||||||
docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; }
|
docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; }
|
||||||
${compose.deployable ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
||||||
|
|
||||||
echo "Docker Compose Deployed: ✅" >> "${logPath}"
|
echo "Docker Compose Deployed: ✅" >> "${logPath}"
|
||||||
} || {
|
} || {
|
||||||
@@ -152,106 +152,106 @@ Compose Type: ${composeType} ✅`;
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return await execAsyncRemote(compose.serverId, bashCommand);
|
return await execAsyncRemote(compose.serverId, bashCommand);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeCommand = (command: string) => {
|
const sanitizeCommand = (command: string) => {
|
||||||
const sanitizedCommand = command.trim();
|
const sanitizedCommand = command.trim();
|
||||||
|
|
||||||
const parts = sanitizedCommand.split(/\s+/);
|
const parts = sanitizedCommand.split(/\s+/);
|
||||||
|
|
||||||
const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1"));
|
const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1"));
|
||||||
|
|
||||||
return restCommand.join(" ");
|
return restCommand.join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createCommand = (compose: ComposeNested) => {
|
export const createCommand = (compose: ComposeNested) => {
|
||||||
const { composeType, appName, sourceType } = compose;
|
const { composeType, appName, sourceType } = compose;
|
||||||
if (compose.command) {
|
if (compose.command) {
|
||||||
return `${sanitizeCommand(compose.command)}`;
|
return `${sanitizeCommand(compose.command)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const path =
|
const path =
|
||||||
sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
||||||
let command = "";
|
let command = "";
|
||||||
|
|
||||||
if (composeType === "docker-compose") {
|
if (composeType === "docker-compose") {
|
||||||
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
||||||
} else if (composeType === "stack") {
|
} else if (composeType === "stack") {
|
||||||
command = `stack deploy -c ${path} ${appName} --prune`;
|
command = `stack deploy -c ${path} ${appName} --prune`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createEnvFile = (compose: ComposeNested) => {
|
const createEnvFile = (compose: ComposeNested) => {
|
||||||
const { COMPOSE_PATH } = paths();
|
const { COMPOSE_PATH } = paths();
|
||||||
const { env, composePath, appName } = compose;
|
const { env, composePath, appName } = compose;
|
||||||
const composeFilePath =
|
const composeFilePath =
|
||||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
join(COMPOSE_PATH, appName, "code", composePath) ||
|
||||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||||
|
|
||||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||||
let envContent = env || "";
|
let envContent = env || "";
|
||||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (compose.randomize) {
|
if (compose.randomize) {
|
||||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envFileContent = prepareEnvironmentVariables(
|
const envFileContent = prepareEnvironmentVariables(
|
||||||
envContent,
|
envContent,
|
||||||
compose.project.env,
|
compose.project.env
|
||||||
).join("\n");
|
).join("\n");
|
||||||
|
|
||||||
if (!existsSync(dirname(envFilePath))) {
|
if (!existsSync(dirname(envFilePath))) {
|
||||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||||
}
|
}
|
||||||
writeFileSync(envFilePath, envFileContent);
|
writeFileSync(envFilePath, envFileContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||||
const { COMPOSE_PATH } = paths(true);
|
const { COMPOSE_PATH } = paths(true);
|
||||||
const { env, composePath, appName } = compose;
|
const { env, composePath, appName } = compose;
|
||||||
const composeFilePath =
|
const composeFilePath =
|
||||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
join(COMPOSE_PATH, appName, "code", composePath) ||
|
||||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||||
|
|
||||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||||
|
|
||||||
let envContent = env || "";
|
let envContent = env || "";
|
||||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (compose.randomize) {
|
if (compose.randomize) {
|
||||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envFileContent = prepareEnvironmentVariables(
|
const envFileContent = prepareEnvironmentVariables(
|
||||||
envContent,
|
envContent,
|
||||||
compose.project.env,
|
compose.project.env
|
||||||
).join("\n");
|
).join("\n");
|
||||||
|
|
||||||
const encodedContent = encodeBase64(envFileContent);
|
const encodedContent = encodeBase64(envFileContent);
|
||||||
return `
|
return `
|
||||||
touch ${envFilePath};
|
touch ${envFilePath};
|
||||||
echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExportEnvCommand = (compose: ComposeNested) => {
|
const getExportEnvCommand = (compose: ComposeNested) => {
|
||||||
if (compose.composeType !== "stack") return "";
|
if (compose.composeType !== "stack") return "";
|
||||||
|
|
||||||
const envVars = getEnviromentVariablesObject(
|
const envVars = getEnviromentVariablesObject(
|
||||||
compose.env,
|
compose.env,
|
||||||
compose.project.env,
|
compose.project.env
|
||||||
);
|
);
|
||||||
const exports = Object.entries(envVars)
|
const exports = Object.entries(envVars)
|
||||||
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
|
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
return exports ? `\n# Export environment variables\n${exports}\n` : "";
|
return exports ? `\n# Export environment variables\n${exports}\n` : "";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,41 +6,41 @@ import { addSuffixToAllVolumes } from "./compose/volume";
|
|||||||
import type { ComposeSpecification } from "./types";
|
import type { ComposeSpecification } from "./types";
|
||||||
|
|
||||||
export const addAppNameToPreventCollision = (
|
export const addAppNameToPreventCollision = (
|
||||||
composeData: ComposeSpecification,
|
composeData: ComposeSpecification,
|
||||||
appName: string,
|
appName: string
|
||||||
): ComposeSpecification => {
|
): ComposeSpecification => {
|
||||||
let updatedComposeData = { ...composeData };
|
let updatedComposeData = { ...composeData };
|
||||||
|
|
||||||
updatedComposeData = addAppNameToAllServiceNames(updatedComposeData, appName);
|
updatedComposeData = addAppNameToAllServiceNames(updatedComposeData, appName);
|
||||||
updatedComposeData = addSuffixToAllVolumes(updatedComposeData, appName);
|
updatedComposeData = addSuffixToAllVolumes(updatedComposeData, appName);
|
||||||
return updatedComposeData;
|
return updatedComposeData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const randomizeDeployableComposeFile = async (
|
export const randomizeIsolatedDeploymentComposeFile = async (
|
||||||
composeId: string,
|
composeId: string,
|
||||||
suffix?: string,
|
suffix?: string
|
||||||
) => {
|
) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
const composeFile = compose.composeFile;
|
const composeFile = compose.composeFile;
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = load(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const randomSuffix = suffix || compose.appName || generateRandomHash();
|
const randomSuffix = suffix || compose.appName || generateRandomHash();
|
||||||
|
|
||||||
const newComposeFile = addAppNameToPreventCollision(
|
const newComposeFile = addAppNameToPreventCollision(
|
||||||
composeData,
|
composeData,
|
||||||
randomSuffix,
|
randomSuffix
|
||||||
);
|
);
|
||||||
|
|
||||||
return dump(newComposeFile);
|
return dump(newComposeFile);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const randomizeDeployableSpecificationFile = (
|
export const randomizeDeployableSpecificationFile = (
|
||||||
composeSpec: ComposeSpecification,
|
composeSpec: ComposeSpecification,
|
||||||
suffix?: string,
|
suffix?: string
|
||||||
) => {
|
) => {
|
||||||
if (!suffix) {
|
if (!suffix) {
|
||||||
return composeSpec;
|
return composeSpec;
|
||||||
}
|
}
|
||||||
const newComposeFile = addAppNameToPreventCollision(composeSpec, suffix);
|
const newComposeFile = addAppNameToPreventCollision(composeSpec, suffix);
|
||||||
return newComposeFile;
|
return newComposeFile;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,346 +7,346 @@ import type { Domain } from "@dokploy/server/services/domain";
|
|||||||
import { dump, load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
import {
|
import {
|
||||||
cloneRawBitbucketRepository,
|
cloneRawBitbucketRepository,
|
||||||
cloneRawBitbucketRepositoryRemote,
|
cloneRawBitbucketRepositoryRemote,
|
||||||
} from "../providers/bitbucket";
|
} from "../providers/bitbucket";
|
||||||
import {
|
import {
|
||||||
cloneGitRawRepository,
|
cloneGitRawRepository,
|
||||||
cloneRawGitRepositoryRemote,
|
cloneRawGitRepositoryRemote,
|
||||||
} from "../providers/git";
|
} from "../providers/git";
|
||||||
import {
|
import {
|
||||||
cloneRawGithubRepository,
|
cloneRawGithubRepository,
|
||||||
cloneRawGithubRepositoryRemote,
|
cloneRawGithubRepositoryRemote,
|
||||||
} from "../providers/github";
|
} from "../providers/github";
|
||||||
import {
|
import {
|
||||||
cloneRawGitlabRepository,
|
cloneRawGitlabRepository,
|
||||||
cloneRawGitlabRepositoryRemote,
|
cloneRawGitlabRepositoryRemote,
|
||||||
} from "../providers/gitlab";
|
} from "../providers/gitlab";
|
||||||
import {
|
import {
|
||||||
createComposeFileRaw,
|
createComposeFileRaw,
|
||||||
createComposeFileRawRemote,
|
createComposeFileRawRemote,
|
||||||
} from "../providers/raw";
|
} from "../providers/raw";
|
||||||
import { randomizeDeployableSpecificationFile } from "./collision";
|
import { randomizeDeployableSpecificationFile } from "./collision";
|
||||||
import { randomizeSpecificationFile } from "./compose";
|
import { randomizeSpecificationFile } from "./compose";
|
||||||
import type {
|
import type {
|
||||||
ComposeSpecification,
|
ComposeSpecification,
|
||||||
DefinitionsService,
|
DefinitionsService,
|
||||||
PropertiesNetworks,
|
PropertiesNetworks,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { encodeBase64 } from "./utils";
|
import { encodeBase64 } from "./utils";
|
||||||
|
|
||||||
export const cloneCompose = async (compose: Compose) => {
|
export const cloneCompose = async (compose: Compose) => {
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
await cloneRawGithubRepository(compose);
|
await cloneRawGithubRepository(compose);
|
||||||
} else if (compose.sourceType === "gitlab") {
|
} else if (compose.sourceType === "gitlab") {
|
||||||
await cloneRawGitlabRepository(compose);
|
await cloneRawGitlabRepository(compose);
|
||||||
} else if (compose.sourceType === "bitbucket") {
|
} else if (compose.sourceType === "bitbucket") {
|
||||||
await cloneRawBitbucketRepository(compose);
|
await cloneRawBitbucketRepository(compose);
|
||||||
} else if (compose.sourceType === "git") {
|
} else if (compose.sourceType === "git") {
|
||||||
await cloneGitRawRepository(compose);
|
await cloneGitRawRepository(compose);
|
||||||
} else if (compose.sourceType === "raw") {
|
} else if (compose.sourceType === "raw") {
|
||||||
await createComposeFileRaw(compose);
|
await createComposeFileRaw(compose);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cloneComposeRemote = async (compose: Compose) => {
|
export const cloneComposeRemote = async (compose: Compose) => {
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
await cloneRawGithubRepositoryRemote(compose);
|
await cloneRawGithubRepositoryRemote(compose);
|
||||||
} else if (compose.sourceType === "gitlab") {
|
} else if (compose.sourceType === "gitlab") {
|
||||||
await cloneRawGitlabRepositoryRemote(compose);
|
await cloneRawGitlabRepositoryRemote(compose);
|
||||||
} else if (compose.sourceType === "bitbucket") {
|
} else if (compose.sourceType === "bitbucket") {
|
||||||
await cloneRawBitbucketRepositoryRemote(compose);
|
await cloneRawBitbucketRepositoryRemote(compose);
|
||||||
} else if (compose.sourceType === "git") {
|
} else if (compose.sourceType === "git") {
|
||||||
await cloneRawGitRepositoryRemote(compose);
|
await cloneRawGitRepositoryRemote(compose);
|
||||||
} else if (compose.sourceType === "raw") {
|
} else if (compose.sourceType === "raw") {
|
||||||
await createComposeFileRawRemote(compose);
|
await createComposeFileRawRemote(compose);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getComposePath = (compose: Compose) => {
|
export const getComposePath = (compose: Compose) => {
|
||||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||||
const { appName, sourceType, composePath } = compose;
|
const { appName, sourceType, composePath } = compose;
|
||||||
let path = "";
|
let path = "";
|
||||||
|
|
||||||
if (sourceType === "raw") {
|
if (sourceType === "raw") {
|
||||||
path = "docker-compose.yml";
|
path = "docker-compose.yml";
|
||||||
} else {
|
} else {
|
||||||
path = composePath;
|
path = composePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return join(COMPOSE_PATH, appName, "code", path);
|
return join(COMPOSE_PATH, appName, "code", path);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadDockerCompose = async (
|
export const loadDockerCompose = async (
|
||||||
compose: Compose,
|
compose: Compose
|
||||||
): Promise<ComposeSpecification | null> => {
|
): Promise<ComposeSpecification | null> => {
|
||||||
const path = getComposePath(compose);
|
const path = getComposePath(compose);
|
||||||
|
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
const yamlStr = readFileSync(path, "utf8");
|
const yamlStr = readFileSync(path, "utf8");
|
||||||
const parsedConfig = load(yamlStr) as ComposeSpecification;
|
const parsedConfig = load(yamlStr) as ComposeSpecification;
|
||||||
return parsedConfig;
|
return parsedConfig;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadDockerComposeRemote = async (
|
export const loadDockerComposeRemote = async (
|
||||||
compose: Compose,
|
compose: Compose
|
||||||
): Promise<ComposeSpecification | null> => {
|
): Promise<ComposeSpecification | null> => {
|
||||||
const path = getComposePath(compose);
|
const path = getComposePath(compose);
|
||||||
try {
|
try {
|
||||||
if (!compose.serverId) {
|
if (!compose.serverId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { stdout, stderr } = await execAsyncRemote(
|
const { stdout, stderr } = await execAsyncRemote(
|
||||||
compose.serverId,
|
compose.serverId,
|
||||||
`cat ${path}`,
|
`cat ${path}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!stdout) return null;
|
if (!stdout) return null;
|
||||||
const parsedConfig = load(stdout) as ComposeSpecification;
|
const parsedConfig = load(stdout) as ComposeSpecification;
|
||||||
return parsedConfig;
|
return parsedConfig;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readComposeFile = async (compose: Compose) => {
|
export const readComposeFile = async (compose: Compose) => {
|
||||||
const path = getComposePath(compose);
|
const path = getComposePath(compose);
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
const yamlStr = readFileSync(path, "utf8");
|
const yamlStr = readFileSync(path, "utf8");
|
||||||
return yamlStr;
|
return yamlStr;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writeDomainsToCompose = async (
|
export const writeDomainsToCompose = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
domains: Domain[],
|
domains: Domain[]
|
||||||
) => {
|
) => {
|
||||||
if (!domains.length) {
|
if (!domains.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const composeConverted = await addDomainToCompose(compose, domains);
|
const composeConverted = await addDomainToCompose(compose, domains);
|
||||||
|
|
||||||
const path = getComposePath(compose);
|
const path = getComposePath(compose);
|
||||||
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
||||||
try {
|
try {
|
||||||
await writeFile(path, composeString, "utf8");
|
await writeFile(path, composeString, "utf8");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writeDomainsToComposeRemote = async (
|
export const writeDomainsToComposeRemote = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
domains: Domain[],
|
domains: Domain[],
|
||||||
logPath: string,
|
logPath: string
|
||||||
) => {
|
) => {
|
||||||
if (!domains.length) {
|
if (!domains.length) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const composeConverted = await addDomainToCompose(compose, domains);
|
const composeConverted = await addDomainToCompose(compose, domains);
|
||||||
const path = getComposePath(compose);
|
const path = getComposePath(compose);
|
||||||
|
|
||||||
if (!composeConverted) {
|
if (!composeConverted) {
|
||||||
return `
|
return `
|
||||||
echo "❌ Error: Compose file not found" >> ${logPath};
|
echo "❌ Error: Compose file not found" >> ${logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
||||||
const encodedContent = encodeBase64(composeString);
|
const encodedContent = encodeBase64(composeString);
|
||||||
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath};
|
return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit
|
// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit
|
||||||
export const addDomainToCompose = async (
|
export const addDomainToCompose = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
domains: Domain[],
|
domains: Domain[]
|
||||||
) => {
|
) => {
|
||||||
const { appName } = compose;
|
const { appName } = compose;
|
||||||
|
|
||||||
let result: ComposeSpecification | null;
|
let result: ComposeSpecification | null;
|
||||||
|
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor
|
result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor
|
||||||
} else {
|
} else {
|
||||||
result = await loadDockerCompose(compose);
|
result = await loadDockerCompose(compose);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result || domains.length === 0) {
|
if (!result || domains.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (compose.deployable) {
|
if (compose.isolatedDeployment) {
|
||||||
const randomized = randomizeDeployableSpecificationFile(
|
const randomized = randomizeDeployableSpecificationFile(
|
||||||
result,
|
result,
|
||||||
compose.suffix || compose.appName,
|
compose.suffix || compose.appName
|
||||||
);
|
);
|
||||||
result = randomized;
|
result = randomized;
|
||||||
} else if (compose.randomize) {
|
} else if (compose.randomize) {
|
||||||
const randomized = randomizeSpecificationFile(result, compose.suffix);
|
const randomized = randomizeSpecificationFile(result, compose.suffix);
|
||||||
result = randomized;
|
result = randomized;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
const { serviceName, https } = domain;
|
const { serviceName, https } = domain;
|
||||||
if (!serviceName) {
|
if (!serviceName) {
|
||||||
throw new Error("Service name not found");
|
throw new Error("Service name not found");
|
||||||
}
|
}
|
||||||
if (!result?.services?.[serviceName]) {
|
if (!result?.services?.[serviceName]) {
|
||||||
throw new Error(`The service ${serviceName} not found in the compose`);
|
throw new Error(`The service ${serviceName} not found in the compose`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const httpLabels = await createDomainLabels(appName, domain, "web");
|
const httpLabels = await createDomainLabels(appName, domain, "web");
|
||||||
if (https) {
|
if (https) {
|
||||||
const httpsLabels = await createDomainLabels(
|
const httpsLabels = await createDomainLabels(
|
||||||
appName,
|
appName,
|
||||||
domain,
|
domain,
|
||||||
"websecure",
|
"websecure"
|
||||||
);
|
);
|
||||||
httpLabels.push(...httpsLabels);
|
httpLabels.push(...httpsLabels);
|
||||||
}
|
}
|
||||||
|
|
||||||
let labels: DefinitionsService["labels"] = [];
|
let labels: DefinitionsService["labels"] = [];
|
||||||
if (compose.composeType === "docker-compose") {
|
if (compose.composeType === "docker-compose") {
|
||||||
if (!result.services[serviceName].labels) {
|
if (!result.services[serviceName].labels) {
|
||||||
result.services[serviceName].labels = [];
|
result.services[serviceName].labels = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
labels = result.services[serviceName].labels;
|
labels = result.services[serviceName].labels;
|
||||||
} else {
|
} else {
|
||||||
// Stack Case
|
// Stack Case
|
||||||
if (!result.services[serviceName].deploy) {
|
if (!result.services[serviceName].deploy) {
|
||||||
result.services[serviceName].deploy = {};
|
result.services[serviceName].deploy = {};
|
||||||
}
|
}
|
||||||
if (!result.services[serviceName].deploy.labels) {
|
if (!result.services[serviceName].deploy.labels) {
|
||||||
result.services[serviceName].deploy.labels = [];
|
result.services[serviceName].deploy.labels = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
labels = result.services[serviceName].deploy.labels;
|
labels = result.services[serviceName].deploy.labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(labels)) {
|
if (Array.isArray(labels)) {
|
||||||
if (!labels.includes("traefik.enable=true")) {
|
if (!labels.includes("traefik.enable=true")) {
|
||||||
labels.push("traefik.enable=true");
|
labels.push("traefik.enable=true");
|
||||||
}
|
}
|
||||||
labels.push(...httpLabels);
|
labels.push(...httpLabels);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!compose.deployable) {
|
if (!compose.isolatedDeployment) {
|
||||||
// Add the dokploy-network to the service
|
// Add the dokploy-network to the service
|
||||||
result.services[serviceName].networks = addDokployNetworkToService(
|
result.services[serviceName].networks = addDokployNetworkToService(
|
||||||
result.services[serviceName].networks,
|
result.services[serviceName].networks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add dokploy-network to the root of the compose file
|
// Add dokploy-network to the root of the compose file
|
||||||
if (!compose.deployable) {
|
if (!compose.isolatedDeployment) {
|
||||||
result.networks = addDokployNetworkToRoot(result.networks);
|
result.networks = addDokployNetworkToRoot(result.networks);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writeComposeFile = async (
|
export const writeComposeFile = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
composeSpec: ComposeSpecification,
|
composeSpec: ComposeSpecification
|
||||||
) => {
|
) => {
|
||||||
const path = getComposePath(compose);
|
const path = getComposePath(compose);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const composeFile = dump(composeSpec, {
|
const composeFile = dump(composeSpec, {
|
||||||
lineWidth: 1000,
|
lineWidth: 1000,
|
||||||
});
|
});
|
||||||
fs.writeFileSync(path, composeFile, "utf8");
|
fs.writeFileSync(path, composeFile, "utf8");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error saving the YAML config file:", e);
|
console.error("Error saving the YAML config file:", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDomainLabels = async (
|
export const createDomainLabels = async (
|
||||||
appName: string,
|
appName: string,
|
||||||
domain: Domain,
|
domain: Domain,
|
||||||
entrypoint: "web" | "websecure",
|
entrypoint: "web" | "websecure"
|
||||||
) => {
|
) => {
|
||||||
const { host, port, https, uniqueConfigKey, certificateType, path } = domain;
|
const { host, port, https, uniqueConfigKey, certificateType, path } = domain;
|
||||||
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
|
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
|
||||||
const labels = [
|
const labels = [
|
||||||
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)${path && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)${path && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||||
`traefik.http.routers.${routerName}.entrypoints=${entrypoint}`,
|
`traefik.http.routers.${routerName}.entrypoints=${entrypoint}`,
|
||||||
`traefik.http.services.${routerName}.loadbalancer.server.port=${port}`,
|
`traefik.http.services.${routerName}.loadbalancer.server.port=${port}`,
|
||||||
`traefik.http.routers.${routerName}.service=${routerName}`,
|
`traefik.http.routers.${routerName}.service=${routerName}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (entrypoint === "web" && https) {
|
if (entrypoint === "web" && https) {
|
||||||
labels.push(
|
labels.push(
|
||||||
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`,
|
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entrypoint === "websecure") {
|
if (entrypoint === "websecure") {
|
||||||
if (certificateType === "letsencrypt") {
|
if (certificateType === "letsencrypt") {
|
||||||
labels.push(
|
labels.push(
|
||||||
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`,
|
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return labels;
|
return labels;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addDokployNetworkToService = (
|
export const addDokployNetworkToService = (
|
||||||
networkService: DefinitionsService["networks"],
|
networkService: DefinitionsService["networks"]
|
||||||
) => {
|
) => {
|
||||||
let networks = networkService;
|
let networks = networkService;
|
||||||
const network = "dokploy-network";
|
const network = "dokploy-network";
|
||||||
if (!networks) {
|
if (!networks) {
|
||||||
networks = [];
|
networks = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(networks)) {
|
if (Array.isArray(networks)) {
|
||||||
if (!networks.includes(network)) {
|
if (!networks.includes(network)) {
|
||||||
networks.push(network);
|
networks.push(network);
|
||||||
}
|
}
|
||||||
} else if (networks && typeof networks === "object") {
|
} else if (networks && typeof networks === "object") {
|
||||||
if (!(network in networks)) {
|
if (!(network in networks)) {
|
||||||
networks[network] = {};
|
networks[network] = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return networks;
|
return networks;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addDokployNetworkToRoot = (
|
export const addDokployNetworkToRoot = (
|
||||||
networkRoot: PropertiesNetworks | undefined,
|
networkRoot: PropertiesNetworks | undefined
|
||||||
) => {
|
) => {
|
||||||
let networks = networkRoot;
|
let networks = networkRoot;
|
||||||
const network = "dokploy-network";
|
const network = "dokploy-network";
|
||||||
|
|
||||||
if (!networks) {
|
if (!networks) {
|
||||||
networks = {};
|
networks = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (networks[network] || !networks[network]) {
|
if (networks[network] || !networks[network]) {
|
||||||
networks[network] = {
|
networks[network] = {
|
||||||
external: true,
|
external: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return networks;
|
return networks;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user