From 017bdd2778c76c52ece0992a212a2b4b40e05217 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 23 Oct 2024 00:54:40 -0600 Subject: [PATCH] refactor(dokploy): add flag to prevent run commands when is cloud --- .../registry/add-self-docker-registry.tsx | 181 ------------------ .../cluster/registry/show-registry.tsx | 4 - .../settings/destination/add-destination.tsx | 135 ++++++++++--- .../destination/update-destination.tsx | 134 ++++++++++--- .../dokploy/server/api/routers/destination.ts | 22 ++- apps/dokploy/server/api/routers/registry.ts | 40 +--- apps/dokploy/server/api/routers/settings.ts | 2 +- packages/server/src/db/schema/destination.ts | 10 +- packages/server/src/index.ts | 2 - packages/server/src/services/registry.ts | 36 ++-- packages/server/src/setup/registry-setup.ts | 91 --------- packages/server/src/utils/providers/git.ts | 2 - packages/server/src/utils/traefik/registry.ts | 75 -------- 13 files changed, 278 insertions(+), 456 deletions(-) delete mode 100644 apps/dokploy/components/dashboard/settings/cluster/registry/add-self-docker-registry.tsx delete mode 100644 packages/server/src/setup/registry-setup.ts delete mode 100644 packages/server/src/utils/traefik/registry.ts diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/add-self-docker-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/add-self-docker-registry.tsx deleted file mode 100644 index fe5a0ca9..00000000 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/add-self-docker-registry.tsx +++ /dev/null @@ -1,181 +0,0 @@ -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 { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, Container } from "lucide-react"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const AddRegistrySchema = z.object({ - username: z - .string() - .min(1, { - message: "Username is required", - }) - .regex(/^[a-zA-Z0-9]+$/, { - message: "Username can only contain letters and numbers", - }), - password: z.string().min(1, { - message: "Password is required", - }), - registryUrl: z.string().min(1, { - message: "Registry URL is required", - }), -}); - -type AddRegistry = z.infer; - -export const AddSelfHostedRegistry = () => { - const utils = api.useUtils(); - const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, error, isError, isLoading } = - api.registry.enableSelfHostedRegistry.useMutation(); - const router = useRouter(); - const form = useForm({ - defaultValues: { - username: "", - password: "", - registryUrl: "", - }, - resolver: zodResolver(AddRegistrySchema), - }); - - useEffect(() => { - form.reset({ - registryUrl: "", - username: "", - password: "", - }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); - - const onSubmit = async (data: AddRegistry) => { - await mutateAsync({ - registryUrl: data.registryUrl, - username: data.username, - password: data.password, - }) - .then(async (data) => { - await utils.registry.all.invalidate(); - toast.success("Self Hosted Registry Created"); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error to create a self hosted registry"); - }); - }; - - return ( - - - - - - - Add a self hosted registry - - Fill the next fields to add a self hosted registry. - - - {isError && ( -
- - - {error?.message} - -
- )} -
- -
- ( - - Username - - - - - - - )} - /> -
-
- ( - - Password - - - - - - - )} - /> -
-
- ( - - Registry URL - - - - - Point a DNS record to the VPS IP address. - - - - - )} - /> -
- - - -
- -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx index 94c82c48..0522301f 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx @@ -8,7 +8,6 @@ import { import { api } from "@/utils/api"; import { Server } from "lucide-react"; import { AddRegistry } from "./add-docker-registry"; -import { AddSelfHostedRegistry } from "./add-self-docker-registry"; import { DeleteRegistry } from "./delete-registry"; import { UpdateDockerRegistry } from "./update-docker-registry"; @@ -31,8 +30,6 @@ export const ShowRegistry = () => {
{data && data?.length > 0 && ( <> - {!haveSelfHostedRegistry && } - )} @@ -47,7 +44,6 @@ export const ShowRegistry = () => {
-
diff --git a/apps/dokploy/components/dashboard/settings/destination/add-destination.tsx b/apps/dokploy/components/dashboard/settings/destination/add-destination.tsx index e8b42daf..b8718d3b 100644 --- a/apps/dokploy/components/dashboard/settings/destination/add-destination.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/add-destination.tsx @@ -18,6 +18,16 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect } from "react"; @@ -32,12 +42,15 @@ const addDestination = z.object({ bucket: z.string(), region: z.string(), endpoint: z.string(), + serverId: z.string().optional(), }); type AddDestination = z.infer; export const AddDestination = () => { const utils = api.useUtils(); + const { data: servers } = api.server.withSSHKey.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { mutateAsync, isError, error, isLoading } = api.destination.create.useMutation(); @@ -189,30 +202,106 @@ export const AddDestination = () => { /> - - + + ) : ( + + .then(async () => { + toast.success("Connection Success"); + }) + .catch(() => { + toast.error("Error to connect the provider"); + }); + }} + > + Test connection + + )} + + + ) : ( + + .then(async () => { + toast.success("Connection Success"); + }) + .catch(() => { + toast.error("Error to connect the provider"); + }); + }} + > + Test connection + + )} + diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index 7abc474d..e960b278 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -12,9 +12,10 @@ import { destinations, } from "@/server/db/schema"; import { + IS_CLOUD, createDestintation, execAsync, - findAdmin, + execAsyncRemote, findDestinationById, removeDestinationById, updateDestinationById, @@ -53,11 +54,26 @@ export const destinationRouter = createTRPCRouter({ ]; const rcloneDestination = `:s3:${bucket}`; const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; - await execAsync(rcloneCommand); + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Server not found", + }); + } + + if (IS_CLOUD) { + await execAsyncRemote(input.serverId || "", rcloneCommand); + } else { + await execAsync(rcloneCommand); + } } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Error to connect to bucket", + message: + error instanceof Error + ? error?.message + : "Error to connect to bucket", cause: error, }); } diff --git a/apps/dokploy/server/api/routers/registry.ts b/apps/dokploy/server/api/routers/registry.ts index 4b3f4628..8ca4a8a0 100644 --- a/apps/dokploy/server/api/routers/registry.ts +++ b/apps/dokploy/server/api/routers/registry.ts @@ -1,6 +1,5 @@ import { apiCreateRegistry, - apiEnableSelfHostedRegistry, apiFindOneRegistry, apiRemoveRegistry, apiTestRegistry, @@ -13,8 +12,6 @@ import { execAsyncRemote, findAllRegistryByAdminId, findRegistryById, - initializeRegistry, - manageRegistry, removeRegistry, updateRegistry, } from "@dokploy/server"; @@ -84,6 +81,13 @@ export const registryRouter = createTRPCRouter({ try { const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Select a server to test the registry", + }); + } + if (input.serverId && input.serverId !== "none") { await execAsyncRemote(input.serverId, loginCommand); } else { @@ -96,34 +100,4 @@ export const registryRouter = createTRPCRouter({ return false; } }), - - enableSelfHostedRegistry: adminProcedure - .input(apiEnableSelfHostedRegistry) - .mutation(async ({ input, ctx }) => { - if (IS_CLOUD) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "Self Hosted Registry is not available in the cloud version", - }); - } - const selfHostedRegistry = await createRegistry( - { - ...input, - registryName: "Self Hosted Registry", - registryType: "selfHosted", - registryUrl: - process.env.NODE_ENV === "production" - ? input.registryUrl - : "dokploy-registry.docker.localhost", - imagePrefix: null, - serverId: undefined, - }, - ctx.user.adminId, - ); - - await manageRegistry(selfHostedRegistry); - await initializeRegistry(input.username, input.password); - - return selfHostedRegistry; - }), }); diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index ca1db29d..485e8c73 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -510,7 +510,7 @@ export const settingsRouter = createTRPCRouter({ if (input?.serverId) { const result = await execAsyncRemote(input.serverId, command); stdout = result.stdout; - } else { + } else if (!IS_CLOUD) { const result = await execAsync( "docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik", ); diff --git a/packages/server/src/db/schema/destination.ts b/packages/server/src/db/schema/destination.ts index 962303f7..bd9c7762 100644 --- a/packages/server/src/db/schema/destination.ts +++ b/packages/server/src/db/schema/destination.ts @@ -53,7 +53,10 @@ export const apiCreateDestination = createSchema endpoint: true, secretAccessKey: true, }) - .required(); + .required() + .extend({ + serverId: z.string().optional(), + }); export const apiFindOneDestination = createSchema .pick({ @@ -77,4 +80,7 @@ export const apiUpdateDestination = createSchema secretAccessKey: true, destinationId: true, }) - .required(); + .required() + .extend({ + serverId: z.string().optional(), + }); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 06f2bc87..abddc405 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -37,7 +37,6 @@ export * from "./services/application"; export * from "./setup/config-paths"; export * from "./setup/postgres-setup"; export * from "./setup/redis-setup"; -export * from "./setup/registry-setup"; export * from "./setup/server-setup"; export * from "./setup/setup"; export * from "./setup/traefik-setup"; @@ -97,7 +96,6 @@ export * from "./utils/traefik/domain"; export * from "./utils/traefik/file-types"; export * from "./utils/traefik/middleware"; export * from "./utils/traefik/redirect"; -export * from "./utils/traefik/registry"; export * from "./utils/traefik/security"; export * from "./utils/traefik/types"; export * from "./utils/traefik/web-server"; diff --git a/packages/server/src/services/registry.ts b/packages/server/src/services/registry.ts index d0b599e1..f85542b4 100644 --- a/packages/server/src/services/registry.ts +++ b/packages/server/src/services/registry.ts @@ -1,14 +1,9 @@ import { db } from "@/server/db"; import { type apiCreateRegistry, registry } from "@/server/db/schema"; -import { initializeRegistry } from "@/server/setup/registry-setup"; -import { removeService } from "@/server/utils/docker/utils"; import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync"; -import { - manageRegistry, - removeSelfHostedRegistry, -} from "@/server/utils/traefik/registry"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { IS_CLOUD } from "../constants"; export type Registry = typeof registry.$inferSelect; @@ -32,6 +27,13 @@ export const createRegistry = async ( message: "Error input: Inserting registry", }); } + + if (IS_CLOUD && !input.serverId && input.serverId !== "none") { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Select a server to add the registry", + }); + } const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; if (input.serverId && input.serverId !== "none") { await execAsyncRemote(input.serverId, loginCommand); @@ -58,13 +60,10 @@ export const removeRegistry = async (registryId: string) => { }); } - if (response.registryType === "selfHosted") { - await removeSelfHostedRegistry(); - await removeService("dokploy-registry"); + if (!IS_CLOUD) { + await execAsync(`docker logout ${response.registryUrl}`); } - await execAsync(`docker logout ${response.registryUrl}`); - return response; } catch (error) { throw new TRPCError({ @@ -89,12 +88,19 @@ export const updateRegistry = async ( .returning() .then((res) => res[0]); - if (response?.registryType === "selfHosted") { - await manageRegistry(response); - await initializeRegistry(response.username, response.password); - } const loginCommand = `echo ${response?.password} | docker login ${response?.registryUrl} --username ${response?.username} --password-stdin`; + if ( + IS_CLOUD && + !registryData?.serverId && + registryData?.serverId !== "none" + ) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Select a server to add the registry", + }); + } + if (registryData?.serverId && registryData?.serverId !== "none") { await execAsyncRemote(registryData.serverId, loginCommand); } else if (response?.registryType === "cloud") { diff --git a/packages/server/src/setup/registry-setup.ts b/packages/server/src/setup/registry-setup.ts deleted file mode 100644 index 3c4cc566..00000000 --- a/packages/server/src/setup/registry-setup.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { CreateServiceOptions } from "dockerode"; -import { docker, paths } from "../constants"; -import { generatePassword } from "../templates/utils"; -import { pullImage } from "../utils/docker/utils"; -import { execAsync } from "../utils/process/execAsync"; - -export const initializeRegistry = async ( - username: string, - password: string, -) => { - const { REGISTRY_PATH } = paths(); - const imageName = "registry:2.8.3"; - const containerName = "dokploy-registry"; - await generateRegistryPassword(username, password); - const randomPass = generatePassword(); - const settings: CreateServiceOptions = { - Name: containerName, - TaskTemplate: { - ContainerSpec: { - Image: imageName, - Env: [ - "REGISTRY_STORAGE_DELETE_ENABLED=true", - "REGISTRY_AUTH=htpasswd", - "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", - "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd", - `REGISTRY_HTTP_SECRET=${randomPass}`, - ], - Mounts: [ - { - Type: "bind", - Source: `${REGISTRY_PATH}/htpasswd`, - Target: "/auth/htpasswd", - ReadOnly: true, - }, - { - Type: "volume", - Source: "registry-data", - Target: "/var/lib/registry", - ReadOnly: false, - }, - ], - }, - Networks: [{ Target: "dokploy-network" }], - Placement: { - Constraints: ["node.role==manager"], - }, - }, - Mode: { - Replicated: { - Replicas: 1, - }, - }, - EndpointSpec: { - Ports: [ - { - TargetPort: 5000, - PublishedPort: 5000, - Protocol: "tcp", - PublishMode: "host", - }, - ], - }, - }; - try { - await pullImage(imageName); - - const service = docker.getService(containerName); - const inspect = await service.inspect(); - await service.update({ - version: Number.parseInt(inspect.Version.Index), - ...settings, - }); - console.log("Registry Started ✅"); - } catch (error) { - await docker.createService(settings); - console.log("Registry Not Found: Starting ✅"); - } -}; - -const generateRegistryPassword = async (username: string, password: string) => { - try { - const { REGISTRY_PATH } = paths(); - const command = `htpasswd -nbB ${username} "${password}" > ${REGISTRY_PATH}/htpasswd`; - const result = await execAsync(command); - console.log("Password generated ✅"); - return result.stdout.trim(); - } catch (error) { - console.error("Error generating password:", error); - return null; - } -}; diff --git a/packages/server/src/utils/providers/git.ts b/packages/server/src/utils/providers/git.ts index 8622879f..c504d65a 100644 --- a/packages/server/src/utils/providers/git.ts +++ b/packages/server/src/utils/providers/git.ts @@ -55,8 +55,6 @@ export const cloneGitRepository = async ( await addHostToKnownHosts(customGitUrl); } await recreateDirectory(outputPath); - // const command = `GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}" git clone --branch ${customGitBranch} --depth 1 ${customGitUrl} ${gitCopyPath} --progress`; - // const { stdout, stderr } = await execAsync(command); writeStream.write( `\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`, ); diff --git a/packages/server/src/utils/traefik/registry.ts b/packages/server/src/utils/traefik/registry.ts deleted file mode 100644 index e857dd2d..00000000 --- a/packages/server/src/utils/traefik/registry.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { paths } from "@/server/constants"; -import type { Registry } from "@/server/services/registry"; -import { dump, load } from "js-yaml"; -import { removeDirectoryIfExistsContent } from "../filesystem/directory"; -import type { FileConfig, HttpRouter } from "./file-types"; - -export const manageRegistry = async (registry: Registry) => { - const { REGISTRY_PATH } = paths(); - if (!existsSync(REGISTRY_PATH)) { - mkdirSync(REGISTRY_PATH, { recursive: true }); - } - - const appName = "dokploy-registry"; - const config: FileConfig = loadOrCreateConfig(); - - const serviceName = `${appName}-service`; - const routerName = `${appName}-router`; - - config.http = config.http || { routers: {}, services: {} }; - config.http.routers = config.http.routers || {}; - config.http.services = config.http.services || {}; - - config.http.routers[routerName] = await createRegistryRouterConfig(registry); - - config.http.services[serviceName] = { - loadBalancer: { - servers: [{ url: `http://${appName}:5000` }], - passHostHeader: true, - }, - }; - - const yamlConfig = dump(config); - const configFile = join(REGISTRY_PATH, "registry.yml"); - writeFileSync(configFile, yamlConfig); -}; - -export const removeSelfHostedRegistry = async () => { - const { REGISTRY_PATH } = paths(); - await removeDirectoryIfExistsContent(REGISTRY_PATH); -}; - -const createRegistryRouterConfig = async (registry: Registry) => { - const { registryUrl } = registry; - const routerConfig: HttpRouter = { - rule: `Host(\`${registryUrl}\`)`, - service: "dokploy-registry-service", - middlewares: ["redirect-to-https"], - entryPoints: [ - "web", - ...(process.env.NODE_ENV === "production" ? ["websecure"] : []), - ], - ...(process.env.NODE_ENV === "production" - ? { - tls: { certResolver: "letsencrypt" }, - } - : {}), - }; - - return routerConfig; -}; - -const loadOrCreateConfig = (): FileConfig => { - const { REGISTRY_PATH } = paths(); - const configPath = join(REGISTRY_PATH, "registry.yml"); - if (existsSync(configPath)) { - const yamlStr = readFileSync(configPath, "utf8"); - const parsedConfig = (load(yamlStr) as FileConfig) || { - http: { routers: {}, services: {} }, - }; - return parsedConfig; - } - return { http: { routers: {}, services: {} } }; -};