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/LICENSE.MD b/LICENSE.MD index 9c53a3bc..9031c94b 100644 --- a/LICENSE.MD +++ b/LICENSE.MD @@ -1,5 +1,7 @@ # License +## Core License (Apache License 2.0) + Copyright 2024 Mauricio Siu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,11 +15,12 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -## Appendix +## Additional Terms for Specific Features -In the event of a conflict, the provisions in this appendix shall take precedence over those in the Apache License. +The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: -- **Modification Distribution:** Any modifications to the software must be distributed freely. -- **Future Paid Features:** Any future paid features of Dokploy cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. +- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version. +- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. +- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service. For further inquiries or permissions, please contact us directly. diff --git a/README-ru.md b/README-ru.md index a63e76f0..5f07d38d 100644 --- a/README-ru.md +++ b/README-ru.md @@ -46,4 +46,4 @@ curl -sSL https://dokploy.com/install.sh | sh - Centos 9 ## πŸ“„ ДокумСнтация -Для ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΠΉ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ посСтитС docs.dokploy.com/docs. +Для ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΠΉ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ посСтитС [docs.dokploy.com/docs](https://docs.dokploy.com). 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;
+}`}
+													
+
+
+
+
+ + +