diff --git a/Dockerfile b/Dockerfile index 97d7b26a..6d3e5875 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ FROM node:18-slim AS production # Install dependencies only for production ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable && apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +RUN corepack enable && apt-get update && apt-get install -y curl && apt-get install -y apache2-utils && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -47,7 +47,6 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-l # Install docker RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh - # Install Nixpacks and tsx # | VERBOSE=1 VERSION=1.21.0 bash RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ diff --git a/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx new file mode 100644 index 00000000..e556650b --- /dev/null +++ b/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -0,0 +1,756 @@ +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 { HelpCircle, Settings } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +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(), + }) + .strict(); + +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(), + }) + .strict(); + +const NetworkSwarmSchema = z.array( + z + .object({ + Target: z.string().optional(), + Aliases: z.array(z.string()).optional(), + DriverOpts: z.object({}).optional(), + }) + .strict(), +); + +const LabelsSwarmSchema = z.record(z.string()); + +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(), + networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(), +}); + +type AddSwarmSettings = z.infer; + +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({ + defaultValues: { + healthCheckSwarm: null, + restartPolicySwarm: null, + placementSwarm: null, + updateConfigSwarm: null, + rollbackConfigSwarm: null, + modeSwarm: null, + labelsSwarm: null, + networkSwarm: null, + }, + resolver: zodResolver(addSwarmSettings), + }); + + useEffect(() => { + if (data) { + form.reset({ + healthCheckSwarm: data.healthCheckSwarm + ? JSON.stringify(data.healthCheckSwarm, null, 2) + : null, + restartPolicySwarm: data.restartPolicySwarm + ? JSON.stringify(data.restartPolicySwarm, null, 2) + : null, + placementSwarm: data.placementSwarm + ? JSON.stringify(data.placementSwarm, null, 2) + : null, + updateConfigSwarm: data.updateConfigSwarm + ? JSON.stringify(data.updateConfigSwarm, null, 2) + : null, + rollbackConfigSwarm: data.rollbackConfigSwarm + ? JSON.stringify(data.rollbackConfigSwarm, null, 2) + : null, + modeSwarm: data.modeSwarm + ? JSON.stringify(data.modeSwarm, null, 2) + : null, + labelsSwarm: data.labelsSwarm + ? JSON.stringify(data.labelsSwarm, null, 2) + : null, + networkSwarm: data.networkSwarm + ? JSON.stringify(data.networkSwarm, null, 2) + : null, + }); + } + }, [form, form.reset, data]); + + const onSubmit = async (data: AddSwarmSettings) => { + await mutateAsync({ + applicationId, + healthCheckSwarm: data.healthCheckSwarm, + restartPolicySwarm: data.restartPolicySwarm, + placementSwarm: data.placementSwarm, + updateConfigSwarm: data.updateConfigSwarm, + rollbackConfigSwarm: data.rollbackConfigSwarm, + modeSwarm: data.modeSwarm, + labelsSwarm: data.labelsSwarm, + networkSwarm: data.networkSwarm, + }) + .then(async () => { + toast.success("Swarm settings updated"); + refetch(); + }) + .catch(() => { + toast.error("Error to update the swarm settings"); + }); + }; + return ( + + + + + + + Swarm Settings + + Update certain settings using a json object. + + + {isError && {error?.message}} + +
+ + ( + + Health Check + + + + + Check the interface + + + + + +
+														{`{
+	Test?: string[] | undefined;
+	Interval?: number | undefined;
+	Timeout?: number | undefined;
+	StartPeriod?: number | undefined;
+	Retries?: number | undefined;
+}`}
+													
+
+
+
+
+ + +