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 (
+
+ );
+};
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: {} } };
+};