mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add experimental swarm settings
This commit is contained in:
parent
5806068e2e
commit
d52f66a716
@ -0,0 +1,472 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Settings } from "lucide-react";
|
||||||
|
|
||||||
|
const HealthCheckSwarmSchema = z
|
||||||
|
.object({
|
||||||
|
Test: z.array(z.string()).optional(),
|
||||||
|
Interval: z.number().optional(),
|
||||||
|
Timeout: z.number().optional(),
|
||||||
|
StartPeriod: z.number().optional(),
|
||||||
|
Retries: z.number().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const RestartPolicySwarmSchema = z
|
||||||
|
.object({
|
||||||
|
Condition: z.string().optional(),
|
||||||
|
Delay: z.number().optional(),
|
||||||
|
MaxAttempts: z.number().optional(),
|
||||||
|
Window: z.number().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const PreferenceSchema = z
|
||||||
|
.object({
|
||||||
|
Spread: z.object({
|
||||||
|
SpreadDescriptor: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const PlatformSchema = z
|
||||||
|
.object({
|
||||||
|
Architecture: z.string(),
|
||||||
|
OS: z.string(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const PlacementSwarmSchema = z
|
||||||
|
.object({
|
||||||
|
Constraints: z.array(z.string()).optional(),
|
||||||
|
Preferences: z.array(PreferenceSchema).optional(),
|
||||||
|
MaxReplicas: z.number().optional(),
|
||||||
|
Platforms: z.array(PlatformSchema).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const UpdateConfigSwarmSchema = z.object({
|
||||||
|
Parallelism: z.number(),
|
||||||
|
Delay: z.number().optional(),
|
||||||
|
FailureAction: z.string().optional(),
|
||||||
|
Monitor: z.number().optional(),
|
||||||
|
MaxFailureRatio: z.number().optional(),
|
||||||
|
Order: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ReplicatedSchema = z
|
||||||
|
.object({
|
||||||
|
Replicas: z.number().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const ReplicatedJobSchema = z
|
||||||
|
.object({
|
||||||
|
MaxConcurrent: z.number().optional(),
|
||||||
|
TotalCompletions: z.number().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const ServiceModeSwarmSchema = z.object({
|
||||||
|
Replicated: ReplicatedSchema.optional(),
|
||||||
|
Global: z.object({}).optional(),
|
||||||
|
ReplicatedJob: ReplicatedJobSchema.optional(),
|
||||||
|
GlobalJob: z.object({}).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const LabelsSwarmSchema = z.record(z.string());
|
||||||
|
|
||||||
|
// const stringToJSONSchema = z
|
||||||
|
// .string()
|
||||||
|
// .transform((str, ctx) => {
|
||||||
|
// try {
|
||||||
|
// return JSON.parse(str);
|
||||||
|
// } catch (e) {
|
||||||
|
// ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||||
|
// return z.NEVER;
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .superRefine((data, ctx) => {
|
||||||
|
// const parseResult = HealthCheckSwarmSchema.safeParse(data);
|
||||||
|
// if (!parseResult.success) {
|
||||||
|
// for (const error of parseResult.error.issues) {
|
||||||
|
// const path = error.path.join(".");
|
||||||
|
// ctx.addIssue({
|
||||||
|
// code: z.ZodIssueCode.custom,
|
||||||
|
// message: `${path} ${error.message}`,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// // parseResult.error.errors.forEach((error) => {
|
||||||
|
// // const path = error.path.join(".");
|
||||||
|
// // ctx.addIssue({
|
||||||
|
// // code: z.ZodIssueCode.custom,
|
||||||
|
// // message: `${path} ${error.message}`,
|
||||||
|
// // });
|
||||||
|
// // });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
||||||
|
return z
|
||||||
|
.string()
|
||||||
|
.transform((str, ctx) => {
|
||||||
|
if (str === null || str === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||||
|
return z.NEVER;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(data).length === 0) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Object cannot be empty",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseResult = schema.safeParse(data);
|
||||||
|
if (!parseResult.success) {
|
||||||
|
for (const error of parseResult.error.issues) {
|
||||||
|
const path = error.path.join(".");
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `${path} ${error.message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSwarmSettings = z.object({
|
||||||
|
healthCheckSwarm: createStringToJSONSchema(HealthCheckSwarmSchema).nullable(),
|
||||||
|
restartPolicySwarm: createStringToJSONSchema(
|
||||||
|
RestartPolicySwarmSchema,
|
||||||
|
).nullable(),
|
||||||
|
placementSwarm: createStringToJSONSchema(PlacementSwarmSchema).nullable(),
|
||||||
|
updateConfigSwarm: createStringToJSONSchema(
|
||||||
|
UpdateConfigSwarmSchema,
|
||||||
|
).nullable(),
|
||||||
|
rollbackConfigSwarm: createStringToJSONSchema(
|
||||||
|
UpdateConfigSwarmSchema,
|
||||||
|
).nullable(),
|
||||||
|
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
|
||||||
|
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||||
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
|
{
|
||||||
|
applicationId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!applicationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
|
api.application.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<AddSwarmSettings>({
|
||||||
|
defaultValues: {
|
||||||
|
healthCheckSwarm: null,
|
||||||
|
restartPolicySwarm: null,
|
||||||
|
placementSwarm: null,
|
||||||
|
updateConfigSwarm: null,
|
||||||
|
rollbackConfigSwarm: null,
|
||||||
|
modeSwarm: null,
|
||||||
|
labelsSwarm: null,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(addSwarmSettings),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
console.log(data.healthCheckSwarm, null);
|
||||||
|
form.reset({
|
||||||
|
healthCheckSwarm: data.healthCheckSwarm || null,
|
||||||
|
restartPolicySwarm: data.restartPolicySwarm || null,
|
||||||
|
placementSwarm: data.placementSwarm || null,
|
||||||
|
updateConfigSwarm: data.updateConfigSwarm || null,
|
||||||
|
rollbackConfigSwarm: data.rollbackConfigSwarm || null,
|
||||||
|
modeSwarm: data.modeSwarm || null,
|
||||||
|
labelsSwarm: data.labelsSwarm || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: AddSwarmSettings) => {
|
||||||
|
console.log(data.restartPolicySwarm);
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId,
|
||||||
|
healthCheckSwarm: data.healthCheckSwarm
|
||||||
|
? JSON.stringify(data.healthCheckSwarm)
|
||||||
|
: null,
|
||||||
|
restartPolicySwarm: data.restartPolicySwarm
|
||||||
|
? JSON.stringify(data.restartPolicySwarm)
|
||||||
|
: null,
|
||||||
|
placementSwarm: data.placementSwarm
|
||||||
|
? JSON.stringify(data.placementSwarm)
|
||||||
|
: null,
|
||||||
|
updateConfigSwarm: data.updateConfigSwarm
|
||||||
|
? JSON.stringify(data.updateConfigSwarm)
|
||||||
|
: null,
|
||||||
|
rollbackConfigSwarm: data.rollbackConfigSwarm
|
||||||
|
? JSON.stringify(data.rollbackConfigSwarm)
|
||||||
|
: null,
|
||||||
|
modeSwarm: data.modeSwarm ? JSON.stringify(data.modeSwarm) : null,
|
||||||
|
labelsSwarm: data.labelsSwarm ? JSON.stringify(data.labelsSwarm) : null,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Swarm settings updated");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the swarm settings");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="secondary" className="cursor-pointer w-fit">
|
||||||
|
<Settings className="size-4 text-muted-foreground" />
|
||||||
|
Swarm Settings
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-4xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Swarm Settings</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update certain settings using a json object.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-add-permissions"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="healthCheckSwarm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative">
|
||||||
|
<FormLabel>Health Check</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
Check the interface
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className="font-mono [field-sizing:content;]"
|
||||||
|
placeholder={`
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="restartPolicySwarm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative">
|
||||||
|
<FormLabel>Restart Policy</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
{/* {path} */}
|
||||||
|
Check the interface
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className="font-mono [field-sizing:content;]"
|
||||||
|
placeholder={`
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="placementSwarm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative">
|
||||||
|
<FormLabel>Placement</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
Check the interface
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className="font-mono [field-sizing:content;]"
|
||||||
|
placeholder={`
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="updateConfigSwarm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative">
|
||||||
|
<FormLabel>Update Config</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
Check the interface
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className="font-mono [field-sizing:content;]"
|
||||||
|
placeholder={`
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="rollbackConfigSwarm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative">
|
||||||
|
<FormLabel>Rollback Config</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
Check the interface
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className="font-mono [field-sizing:content;]"
|
||||||
|
placeholder={`
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="modeSwarm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative">
|
||||||
|
<FormLabel>Mode</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
Check the interface
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className="font-mono [field-sizing:content;]"
|
||||||
|
placeholder={`
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="labelsSwarm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative">
|
||||||
|
<FormLabel>Labels</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
Check the interface
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className="font-mono [field-sizing:content;]"
|
||||||
|
placeholder={`
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-add-permissions"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
@ -33,6 +33,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
|
import { AddSwarmSettings } from "./modify-swarm-settings";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@ -102,6 +103,7 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
Add the registry and the replicas of the application
|
Add the registry and the replicas of the application
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
<AddSwarmSettings applicationId={applicationId} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@ -186,6 +188,7 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button isLoading={isLoading} type="submit" className="w-fit">
|
<Button isLoading={isLoading} type="submit" className="w-fit">
|
||||||
Save
|
Save
|
||||||
|
@ -20,7 +20,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -99,7 +99,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -94,7 +94,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
@ -20,7 +20,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -89,7 +89,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Pencil, CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { Pencil, CheckIcon, ChevronsUpDown, PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -115,7 +115,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
@ -8,7 +8,7 @@ export const ProjectLayout = ({ children }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="bg-radial relative flex flex-col bg-background pt-6"
|
className="bg-radial relative flex flex-col bg-background"
|
||||||
id="app-container"
|
id="app-container"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
|
@ -147,7 +147,6 @@ const FormMessage = React.forwardRef<
|
|||||||
>(({ className, children, ...props }, ref) => {
|
>(({ className, children, ...props }, ref) => {
|
||||||
const { error, formMessageId } = useFormField();
|
const { error, formMessageId } = useFormField();
|
||||||
const body = error ? String(error?.message) : children;
|
const body = error ? String(error?.message) : children;
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
7
drizzle/0012_chubby_umar.sql
Normal file
7
drizzle/0012_chubby_umar.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "labelsSwarm" json;
|
2386
drizzle/meta/0012_snapshot.json
Normal file
2386
drizzle/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -85,6 +85,13 @@
|
|||||||
"when": 1715574230599,
|
"when": 1715574230599,
|
||||||
"tag": "0011_petite_calypso",
|
"tag": "0011_petite_calypso",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1716015716708,
|
||||||
|
"tag": "0012_chubby_umar",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -10,7 +10,14 @@ import { projects } from "./project";
|
|||||||
import { security } from "./security";
|
import { security } from "./security";
|
||||||
import { applicationStatus } from "./shared";
|
import { applicationStatus } from "./shared";
|
||||||
import { ports } from "./port";
|
import { ports } from "./port";
|
||||||
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
import {
|
||||||
|
boolean,
|
||||||
|
integer,
|
||||||
|
json,
|
||||||
|
pgEnum,
|
||||||
|
pgTable,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
import { registry } from "./registry";
|
import { registry } from "./registry";
|
||||||
|
|
||||||
@ -23,6 +30,58 @@ export const buildType = pgEnum("buildType", [
|
|||||||
"nixpacks",
|
"nixpacks",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
interface HealthCheckSwarm {
|
||||||
|
Test?: string[] | undefined;
|
||||||
|
Interval?: number | undefined;
|
||||||
|
Timeout?: number | undefined;
|
||||||
|
StartPeriod?: number | undefined;
|
||||||
|
Retries?: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RestartPolicySwarm {
|
||||||
|
Condition?: string | undefined;
|
||||||
|
Delay?: number | undefined;
|
||||||
|
MaxAttempts?: number | undefined;
|
||||||
|
Window?: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlacementSwarm {
|
||||||
|
Constraints?: string[] | undefined;
|
||||||
|
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
|
||||||
|
MaxReplicas?: number | undefined;
|
||||||
|
Platforms?:
|
||||||
|
| Array<{
|
||||||
|
Architecture: string;
|
||||||
|
OS: string;
|
||||||
|
}>
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateConfigSwarm {
|
||||||
|
Parallelism: number;
|
||||||
|
Delay?: number | undefined;
|
||||||
|
FailureAction?: string | undefined;
|
||||||
|
Monitor?: number | undefined;
|
||||||
|
MaxFailureRatio?: number | undefined;
|
||||||
|
Order: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceModeSwarm {
|
||||||
|
Replicated?: { Replicas?: number | undefined } | undefined;
|
||||||
|
Global?: {} | undefined;
|
||||||
|
ReplicatedJob?:
|
||||||
|
| {
|
||||||
|
MaxConcurrent?: number | undefined;
|
||||||
|
TotalCompletions?: number | undefined;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
GlobalJob?: {} | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LabelsSwarm {
|
||||||
|
[name: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const applications = pgTable("application", {
|
export const applications = pgTable("application", {
|
||||||
applicationId: text("applicationId")
|
applicationId: text("applicationId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@ -61,6 +120,15 @@ export const applications = pgTable("application", {
|
|||||||
customGitBuildPath: text("customGitBuildPath"),
|
customGitBuildPath: text("customGitBuildPath"),
|
||||||
customGitSSHKey: text("customGitSSHKey"),
|
customGitSSHKey: text("customGitSSHKey"),
|
||||||
dockerfile: text("dockerfile"),
|
dockerfile: text("dockerfile"),
|
||||||
|
// Docker swarm json
|
||||||
|
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
|
||||||
|
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
|
||||||
|
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
|
||||||
|
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||||
|
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||||
|
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||||
|
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||||
|
//
|
||||||
replicas: integer("replicas").default(1).notNull(),
|
replicas: integer("replicas").default(1).notNull(),
|
||||||
applicationStatus: applicationStatus("applicationStatus")
|
applicationStatus: applicationStatus("applicationStatus")
|
||||||
.notNull()
|
.notNull()
|
||||||
|
@ -5,6 +5,7 @@ import type { CreateServiceOptions } from "dockerode";
|
|||||||
import {
|
import {
|
||||||
calculateResources,
|
calculateResources,
|
||||||
generateBindMounts,
|
generateBindMounts,
|
||||||
|
generateConfigContainer,
|
||||||
generateFileMounts,
|
generateFileMounts,
|
||||||
generateVolumeMounts,
|
generateVolumeMounts,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
@ -72,7 +73,6 @@ export const mechanizeDockerContainer = async (
|
|||||||
cpuReservation,
|
cpuReservation,
|
||||||
command,
|
command,
|
||||||
ports,
|
ports,
|
||||||
replicas,
|
|
||||||
} = application;
|
} = application;
|
||||||
|
|
||||||
const resources = calculateResources({
|
const resources = calculateResources({
|
||||||
@ -83,13 +83,29 @@ export const mechanizeDockerContainer = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
const volumesMount = generateVolumeMounts(mounts);
|
||||||
|
|
||||||
|
const {
|
||||||
|
HealthCheck,
|
||||||
|
RestartPolicy,
|
||||||
|
Placement,
|
||||||
|
Labels,
|
||||||
|
Mode,
|
||||||
|
RollbackConfig,
|
||||||
|
UpdateConfig,
|
||||||
|
} = generateConfigContainer(application);
|
||||||
|
|
||||||
const bindsMount = generateBindMounts(mounts);
|
const bindsMount = generateBindMounts(mounts);
|
||||||
const filesMount = generateFileMounts(appName, mounts);
|
const filesMount = generateFileMounts(appName, mounts);
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
const envVariables = prepareEnvironmentVariables(env);
|
||||||
|
|
||||||
const registry = application.registry;
|
const registry = application.registry;
|
||||||
|
|
||||||
let image = sourceType === "docker" ? dockerImage! : `${appName}:latest`;
|
console.log(Labels);
|
||||||
|
|
||||||
|
let image =
|
||||||
|
sourceType === "docker"
|
||||||
|
? dockerImage || "ERROR-NO-IMAGE-PROVIDED"
|
||||||
|
: `${appName}:latest`;
|
||||||
|
|
||||||
if (registry) {
|
if (registry) {
|
||||||
image = `${registry.registryUrl}/${appName}`;
|
image = `${registry.registryUrl}/${appName}`;
|
||||||
@ -108,6 +124,7 @@ export const mechanizeDockerContainer = async (
|
|||||||
Name: appName,
|
Name: appName,
|
||||||
TaskTemplate: {
|
TaskTemplate: {
|
||||||
ContainerSpec: {
|
ContainerSpec: {
|
||||||
|
HealthCheck,
|
||||||
Image: image,
|
Image: image,
|
||||||
Env: envVariables,
|
Env: envVariables,
|
||||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||||
@ -117,20 +134,18 @@ export const mechanizeDockerContainer = async (
|
|||||||
Args: ["-c", command],
|
Args: ["-c", command],
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
Labels,
|
||||||
},
|
},
|
||||||
Networks: [{ Target: "dokploy-network" }],
|
Networks: [{ Target: "dokploy-network" }],
|
||||||
RestartPolicy: {
|
RestartPolicy,
|
||||||
Condition: "on-failure",
|
Placement,
|
||||||
},
|
|
||||||
Resources: {
|
Resources: {
|
||||||
...resources,
|
...resources,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mode: {
|
Mode,
|
||||||
Replicated: {
|
RollbackConfig,
|
||||||
Replicas: replicas,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
EndpointSpec: {
|
EndpointSpec: {
|
||||||
Ports: ports.map((port) => ({
|
Ports: ports.map((port) => ({
|
||||||
Protocol: port.protocol,
|
Protocol: port.protocol,
|
||||||
@ -138,10 +153,7 @@ export const mechanizeDockerContainer = async (
|
|||||||
PublishedPort: port.publishedPort,
|
PublishedPort: port.publishedPort,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
UpdateConfig: {
|
UpdateConfig,
|
||||||
Parallelism: 1,
|
|
||||||
Order: "start-first",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -156,7 +168,7 @@ export const mechanizeDockerContainer = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
await docker.createService(settings);
|
await docker.createService(settings);
|
||||||
}
|
}
|
||||||
// await cleanUpUnusedImages();
|
|
||||||
};
|
};
|
||||||
|
@ -122,10 +122,10 @@ export const cleanUpInactiveContainers = async () => {
|
|||||||
|
|
||||||
for (const container of inactiveContainers) {
|
for (const container of inactiveContainers) {
|
||||||
await docker.getContainer(container.Id).remove({ force: true });
|
await docker.getContainer(container.Id).remove({ force: true });
|
||||||
console.log(`Contenedor eliminado: ${container.Id}`);
|
console.log(`Cleaning up inactive container: ${container.Id}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al limpiar contenedores inactivos:", error);
|
console.error("Error cleaning up inactive containers:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -199,6 +199,88 @@ export const calculateResources = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateConfigContainer = (application: ApplicationNested) => {
|
||||||
|
const { replicas, mounts } = application;
|
||||||
|
|
||||||
|
const healthCheckSwarm = JSON.parse(
|
||||||
|
(application.healthCheckSwarm as string) || "{}",
|
||||||
|
);
|
||||||
|
const restartPolicySwarm = JSON.parse(
|
||||||
|
(application.restartPolicySwarm as string) || "{}",
|
||||||
|
);
|
||||||
|
const placementSwarm = JSON.parse(
|
||||||
|
(application.placementSwarm as string) || "{}",
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateConfigSwarm = JSON.parse(
|
||||||
|
(application.updateConfigSwarm as unknown as string) || "{}",
|
||||||
|
);
|
||||||
|
|
||||||
|
const rollbackConfigSwarm = JSON.parse(
|
||||||
|
(application.rollbackConfigSwarm as unknown as string) || "{}",
|
||||||
|
);
|
||||||
|
const modeSwarm = JSON.parse((application.modeSwarm as string) || "{}");
|
||||||
|
|
||||||
|
const labelsSwarm = JSON.parse(
|
||||||
|
(application.labelsSwarm as unknown as string) || "{}",
|
||||||
|
);
|
||||||
|
|
||||||
|
const haveMounts = mounts.length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(healthCheckSwarm && {
|
||||||
|
HealthCheck: healthCheckSwarm,
|
||||||
|
}),
|
||||||
|
...(restartPolicySwarm
|
||||||
|
? {
|
||||||
|
RestartPolicy: restartPolicySwarm,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
// if no restartPolicySwarm provided use default
|
||||||
|
RestartPolicy: {
|
||||||
|
Condition: "on-failure",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(placementSwarm
|
||||||
|
? {
|
||||||
|
Placement: placementSwarm,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
// if app have mounts keep manager as constraint
|
||||||
|
Placement: {
|
||||||
|
Constraints: haveMounts ? ["node.role==manager"] : [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(labelsSwarm && {
|
||||||
|
Labels: labelsSwarm,
|
||||||
|
}),
|
||||||
|
...(modeSwarm
|
||||||
|
? {
|
||||||
|
Mode: modeSwarm,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
// use replicas value if no modeSwarm provided
|
||||||
|
Mode: {
|
||||||
|
Replicated: {
|
||||||
|
Replicas: replicas,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(rollbackConfigSwarm && {
|
||||||
|
RollbackConfig: rollbackConfigSwarm,
|
||||||
|
}),
|
||||||
|
...(updateConfigSwarm
|
||||||
|
? { UpdateConfig: updateConfigSwarm }
|
||||||
|
: {
|
||||||
|
// default config if no updateConfigSwarm provided
|
||||||
|
UpdateConfig: {
|
||||||
|
Parallelism: 1,
|
||||||
|
Order: "start-first",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => {
|
export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => {
|
||||||
if (!mounts || mounts.length === 0) {
|
if (!mounts || mounts.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
Loading…
Reference in New Issue
Block a user