From 08517d6f3651a741e5e8b18ac7471b7240278438 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 14 May 2024 03:09:07 -0600 Subject: [PATCH] feat: add update registry and fix the docker url markup --- Dockerfile | 3 +- .../cluster/registry/add-docker-registry.tsx | 22 +- .../cluster/registry/show-registry.tsx | 6 +- .../registry/update-docker-registry.tsx | 275 ++++++++++++++++++ server/api/routers/registry.ts | 13 +- server/api/services/registry.ts | 21 +- server/db/schema/registry.ts | 21 +- server/setup/registry-setup.ts | 4 +- server/utils/builders/index.ts | 15 +- server/utils/cluster/upload.ts | 26 +- server/utils/traefik/registry.ts | 27 +- 11 files changed, 366 insertions(+), 67 deletions(-) create mode 100644 components/dashboard/settings/cluster/registry/update-docker-registry.tsx diff --git a/Dockerfile b/Dockerfile index 97d7b26a7..6d3e58758 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/settings/cluster/registry/add-docker-registry.tsx b/components/dashboard/settings/cluster/registry/add-docker-registry.tsx index 8e7f1dd23..04ac32707 100644 --- a/components/dashboard/settings/cluster/registry/add-docker-registry.tsx +++ b/components/dashboard/settings/cluster/registry/add-docker-registry.tsx @@ -217,10 +217,6 @@ export const AddRegistry = () => { variant={"secondary"} isLoading={isLoading} onClick={async () => { - if (!form.formState.isValid) { - toast.error("Please fill all the fields"); - return; - } await testRegistry({ username: username, password: password, @@ -228,13 +224,17 @@ export const AddRegistry = () => { registryName: registryName, registryType: "cloud", imagePrefix: imagePrefix, - }).then((data) => { - if (data) { - toast.success("Registry Tested Successfully"); - } else { - toast.error("Registry Test Failed"); - } - }); + }) + .then((data) => { + if (data) { + toast.success("Registry Tested Successfully"); + } else { + toast.error("Registry Test Failed"); + } + }) + .catch(() => { + toast.error("Error to test the registry"); + }); }} > Test Registry diff --git a/components/dashboard/settings/cluster/registry/show-registry.tsx b/components/dashboard/settings/cluster/registry/show-registry.tsx index c02c6ae6a..30ed18fe9 100644 --- a/components/dashboard/settings/cluster/registry/show-registry.tsx +++ b/components/dashboard/settings/cluster/registry/show-registry.tsx @@ -10,6 +10,7 @@ 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"; export const ShowRegistry = () => { const { data } = api.registry.all.useQuery(); @@ -49,8 +50,6 @@ export const ShowRegistry = () => { - - {/* */} ) : (
@@ -63,12 +62,11 @@ export const ShowRegistry = () => { {index + 1}. {registry.registryName}
+
))} - -
{/* */}
)} diff --git a/components/dashboard/settings/cluster/registry/update-docker-registry.tsx b/components/dashboard/settings/cluster/registry/update-docker-registry.tsx new file mode 100644 index 000000000..c84c019ac --- /dev/null +++ b/components/dashboard/settings/cluster/registry/update-docker-registry.tsx @@ -0,0 +1,275 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle, PenBoxIcon } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const updateRegistry = z.object({ + registryName: z.string().min(1, { + message: "Registry name is required", + }), + username: z.string().min(1, { + message: "Username is required", + }), + password: z.string(), + registryUrl: z.string().min(1, { + message: "Registry URL is required", + }), + imagePrefix: z.string(), +}); + +type UpdateRegistry = z.infer; + +interface Props { + registryId: string; +} + +export const UpdateDockerRegistry = ({ registryId }: Props) => { + const utils = api.useUtils(); + const { mutateAsync: testRegistry, isLoading } = + api.registry.testRegistry.useMutation(); + const { data, refetch } = api.registry.one.useQuery( + { + registryId, + }, + { + enabled: !!registryId, + }, + ); + + const isCloud = data?.registryType === "cloud"; + const { mutateAsync, isError, error } = api.registry.update.useMutation(); + + const form = useForm({ + defaultValues: { + imagePrefix: "", + registryName: "", + username: "", + password: "", + registryUrl: "", + }, + resolver: zodResolver(updateRegistry), + }); + + const password = form.watch("password"); + const username = form.watch("username"); + const registryUrl = form.watch("registryUrl"); + const registryName = form.watch("registryName"); + const imagePrefix = form.watch("imagePrefix"); + + useEffect(() => { + if (data) { + form.reset({ + imagePrefix: data.imagePrefix || "", + registryName: data.registryName || "", + username: data.username || "", + password: "", + registryUrl: data.registryUrl || "", + }); + } + }, [form, form.reset, data]); + + const onSubmit = async (data: UpdateRegistry) => { + await mutateAsync({ + registryId, + ...(data.password ? { password: data.password } : {}), + registryName: data.registryName, + username: data.username, + registryUrl: data.registryUrl, + imagePrefix: data.imagePrefix, + }) + .then(async (data) => { + toast.success("Registry Updated"); + await refetch(); + await utils.registry.all.invalidate(); + }) + .catch(() => { + toast.error("Error to update the registry"); + }); + }; + return ( + + + + + + + Registry + Update the registry information + + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+ +
+
+ ( + + Registry Name + + + + + + + )} + /> + + ( + + Username + + + + + + + )} + /> + + ( + + Password + + + + + + + )} + /> + {isCloud && ( + ( + + Image Prefix + + + + + + + )} + /> + )} + + ( + + Registry URL + + + + + + + )} + /> +
+
+
+ + + {isCloud && ( + + )} + + + + +
+
+ ); +}; diff --git a/server/api/routers/registry.ts b/server/api/routers/registry.ts index 4779b9488..63ffa213c 100644 --- a/server/api/routers/registry.ts +++ b/server/api/routers/registry.ts @@ -3,6 +3,7 @@ import { apiEnableSelfHostedRegistry, apiFindOneRegistry, apiRemoveRegistry, + apiTestRegistry, apiUpdateRegistry, } from "@/server/db/schema"; import { @@ -10,7 +11,7 @@ import { findAllRegistry, findRegistryById, removeRegistry, - updaterRegistry, + updateRegistry, } from "../services/registry"; import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; import { TRPCError } from "@trpc/server"; @@ -33,7 +34,7 @@ export const registryRouter = createTRPCRouter({ .input(apiUpdateRegistry) .mutation(async ({ input }) => { const { registryId, ...rest } = input; - const application = await updaterRegistry(registryId, { + const application = await updateRegistry(registryId, { ...rest, }); @@ -49,11 +50,11 @@ export const registryRouter = createTRPCRouter({ all: protectedProcedure.query(async () => { return await findAllRegistry(); }), - findOne: adminProcedure.input(apiFindOneRegistry).query(async ({ input }) => { + one: adminProcedure.input(apiFindOneRegistry).query(async ({ input }) => { return await findRegistryById(input.registryId); }), testRegistry: protectedProcedure - .input(apiCreateRegistry) + .input(apiTestRegistry) .mutation(async ({ input }) => { try { const result = await docker.checkAuth({ @@ -76,6 +77,10 @@ export const registryRouter = createTRPCRouter({ ...input, registryName: "Self Hosted Registry", registryType: "selfHosted", + registryUrl: + process.env.NODE_ENV === "production" + ? input.registryUrl + : "dokploy-registry.docker.localhost", imagePrefix: null, }); diff --git a/server/api/services/registry.ts b/server/api/services/registry.ts index 149927d5f..48077d055 100644 --- a/server/api/services/registry.ts +++ b/server/api/services/registry.ts @@ -3,8 +3,12 @@ import { TRPCError } from "@trpc/server"; import { db } from "@/server/db"; import { eq } from "drizzle-orm"; import { findAdmin } from "./admin"; -import { removeSelfHostedRegistry } from "@/server/utils/traefik/registry"; +import { + manageRegistry, + removeSelfHostedRegistry, +} from "@/server/utils/traefik/registry"; import { removeService } from "@/server/utils/docker/utils"; +import { initializeRegistry } from "@/server/setup/registry-setup"; export type Registry = typeof registry.$inferSelect; @@ -59,7 +63,7 @@ export const removeRegistry = async (registryId: string) => { } }; -export const updaterRegistry = async ( +export const updateRegistry = async ( registryId: string, registryData: Partial, ) => { @@ -70,9 +74,15 @@ export const updaterRegistry = async ( ...registryData, }) .where(eq(registry.registryId, registryId)) - .returning(); + .returning() + .then((res) => res[0]); - return response[0]; + if (response?.registryType === "selfHosted") { + await manageRegistry(response); + await initializeRegistry(response.username, response.password); + } + + return response; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -84,6 +94,9 @@ export const updaterRegistry = async ( export const findRegistryById = async (registryId: string) => { const registryResponse = await db.query.registry.findFirst({ where: eq(registry.registryId, registryId), + columns: { + password: false, + }, }); if (!registryResponse) { throw new TRPCError({ diff --git a/server/db/schema/registry.ts b/server/db/schema/registry.ts index 9a536c7c2..7921c5aa4 100644 --- a/server/db/schema/registry.ts +++ b/server/db/schema/registry.ts @@ -64,6 +64,15 @@ export const apiCreateRegistry = createSchema }) .required(); +export const apiTestRegistry = createSchema.pick({}).extend({ + registryName: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + registryUrl: z.string(), + registryType: z.enum(["selfHosted", "cloud"]), + imagePrefix: z.string().nullable().optional(), +}); + export const apiRemoveRegistry = createSchema .pick({ registryId: true, @@ -76,15 +85,9 @@ export const apiFindOneRegistry = createSchema }) .required(); -export const apiUpdateRegistry = createSchema - .pick({ - password: true, - registryName: true, - username: true, - registryUrl: true, - registryId: true, - }) - .required(); +export const apiUpdateRegistry = createSchema.partial().extend({ + registryId: z.string().min(1), +}); export const apiEnableSelfHostedRegistry = createSchema .pick({ diff --git a/server/setup/registry-setup.ts b/server/setup/registry-setup.ts index 9756ff8c7..94d592f7f 100644 --- a/server/setup/registry-setup.ts +++ b/server/setup/registry-setup.ts @@ -10,7 +10,7 @@ export const initializeRegistry = async ( ) => { const imageName = "registry:2.8.3"; const containerName = "dokploy-registry"; - await generatePassword(username, password); + await generateRegistryPassword(username, password); const randomPass = await generateRandomPassword(); const settings: CreateServiceOptions = { Name: containerName, @@ -76,7 +76,7 @@ export const initializeRegistry = async ( } }; -const generatePassword = async (username: string, password: string) => { +const generateRegistryPassword = async (username: string, password: string) => { try { const command = `htpasswd -nbB ${username} "${password}" > ${REGISTRY_PATH}/htpasswd`; const result = await execAsync(command); diff --git a/server/utils/builders/index.ts b/server/utils/builders/index.ts index d2611e2b2..715a1fe34 100644 --- a/server/utils/builders/index.ts +++ b/server/utils/builders/index.ts @@ -89,12 +89,15 @@ export const mechanizeDockerContainer = async ( const registry = application.registry; - const image = - sourceType === "docker" - ? dockerImage! - : registry - ? `${registry.registryUrl}/${appName}` - : `${appName}:latest`; + let image = sourceType === "docker" ? dockerImage! : `${appName}:latest`; + + if (registry) { + image = `${registry.registryUrl}/${appName}`; + + if (registry.imagePrefix) { + image = `${registry.registryUrl}/${registry.imagePrefix}/${appName}`; + } + } const settings: CreateServiceOptions = { authconfig: { diff --git a/server/utils/cluster/upload.ts b/server/utils/cluster/upload.ts index d033434d7..234fdbd54 100644 --- a/server/utils/cluster/upload.ts +++ b/server/utils/cluster/upload.ts @@ -1,5 +1,4 @@ import type { ApplicationNested } from "../builders"; -import { execAsync } from "../process/execAsync"; import { spawnAsync } from "../process/spawnAsync"; import type { WriteStream } from "node:fs"; @@ -13,25 +12,20 @@ export const uploadImage = async ( throw new Error("Registry not found"); } - const { registryUrl, imagePrefix } = registry; + const { registryUrl, imagePrefix, registryType } = registry; const { appName } = application; const imageName = `${appName}:latest`; - let finalURL = registryUrl; + const finalURL = + registryType === "selfHosted" + ? process.env.NODE_ENV === "development" + ? "localhost:5000" + : registryUrl + : registryUrl; - let registryTag = `${registryUrl}/${imageName}`; - - if (imagePrefix) { - registryTag = `${registryUrl}/${imagePrefix}/${imageName}`; - } - - // registry.digitalocean.com// - // index.docker.io/siumauricio/app-parse-multi-byte-port-e32uh7:latest - if (registry.registryType === "selfHosted") { - finalURL = - process.env.NODE_ENV === "development" ? "localhost:5000" : registryUrl; - registryTag = `${finalURL}/${imageName}`; - } + const registryTag = imagePrefix + ? `${registryUrl}/${imagePrefix}/${imageName}` + : `${finalURL}/${imageName}`; try { console.log(finalURL, registryTag); diff --git a/server/utils/traefik/registry.ts b/server/utils/traefik/registry.ts index bab6c9876..6f2960ea2 100644 --- a/server/utils/traefik/registry.ts +++ b/server/utils/traefik/registry.ts @@ -1,18 +1,19 @@ -import { loadOrCreateConfig } from "./application"; import type { FileConfig, HttpRouter } from "./file-types"; import type { Registry } from "@/server/api/services/registry"; import { removeDirectoryIfExistsContent } from "../filesystem/directory"; import { REGISTRY_PATH } from "@/server/constants"; -import { dump } from "js-yaml"; +import { dump, load } from "js-yaml"; import { join } from "node:path"; -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; export const manageRegistry = async (registry: Registry) => { if (!existsSync(REGISTRY_PATH)) { mkdirSync(REGISTRY_PATH, { recursive: true }); } + const appName = "dokploy-registry"; - const config: FileConfig = loadOrCreateConfig(appName); + const config: FileConfig = loadOrCreateConfig(); + const serviceName = `${appName}-service`; const routerName = `${appName}-router`; @@ -40,12 +41,8 @@ export const removeSelfHostedRegistry = async () => { const createRegistryRouterConfig = async (registry: Registry) => { const { registryUrl } = registry; - const url = - process.env.NODE_ENV === "production" - ? registryUrl - : "dokploy-registry.docker.localhost"; const routerConfig: HttpRouter = { - rule: `Host(\`${url}\`)`, + rule: `Host(\`${registryUrl}\`)`, service: "dokploy-registry-service", ...(process.env.NODE_ENV === "production" ? { @@ -65,3 +62,15 @@ const createRegistryRouterConfig = async (registry: Registry) => { return routerConfig; }; + +const loadOrCreateConfig = (): FileConfig => { + 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: {} } }; +};