feat: add experimental swarm settings

This commit is contained in:
Mauricio Siu 2024-05-18 03:07:51 -06:00
parent 5806068e2e
commit d52f66a716
14 changed files with 3064 additions and 28 deletions

View File

@ -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>
);
};

View File

@ -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

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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;
} }

View 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;

File diff suppressed because it is too large Load Diff

View File

@ -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
} }
] ]
} }

View File

@ -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()

View File

@ -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();
}; };

View File

@ -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 [];