Skip to content

Commit 854bd88

Browse files
authored
Merge pull request #3292 from Dokploy/3261-the-registry-password-is-always-blank-when-you-modify-any-existing-registry
feat(registry): enhance registry handling with optional password and …
2 parents 38c7e1e + acf385a commit 854bd88

File tree

3 files changed

+157
-12
lines changed

3 files changed

+157
-12
lines changed

apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ const AddRegistrySchema = z.object({
4242
username: z.string().min(1, {
4343
message: "Username is required",
4444
}),
45-
password: z.string().min(1, {
46-
message: "Password is required",
47-
}),
45+
password: z.string(),
4846
registryUrl: z
4947
.string()
5048
.optional()
@@ -75,6 +73,7 @@ const AddRegistrySchema = z.object({
7573
),
7674
imagePrefix: z.string(),
7775
serverId: z.string().optional(),
76+
isEditing: z.boolean().optional(),
7877
});
7978

8079
type AddRegistry = z.infer<typeof AddRegistrySchema>;
@@ -108,6 +107,12 @@ export const HandleRegistry = ({ registryId }: Props) => {
108107
error: testRegistryError,
109108
isError: testRegistryIsError,
110109
} = api.registry.testRegistry.useMutation();
110+
const {
111+
mutateAsync: testRegistryById,
112+
isLoading: isLoadingById,
113+
error: testRegistryByIdError,
114+
isError: testRegistryByIdIsError,
115+
} = api.registry.testRegistryById.useMutation();
111116
const form = useForm<AddRegistry>({
112117
defaultValues: {
113118
username: "",
@@ -116,8 +121,26 @@ export const HandleRegistry = ({ registryId }: Props) => {
116121
imagePrefix: "",
117122
registryName: "",
118123
serverId: "",
124+
isEditing: !!registryId,
119125
},
120-
resolver: zodResolver(AddRegistrySchema),
126+
resolver: zodResolver(
127+
AddRegistrySchema.refine(
128+
(data) => {
129+
// When creating a new registry, password is required
130+
if (
131+
!data.isEditing &&
132+
(!data.password || data.password.length === 0)
133+
) {
134+
return false;
135+
}
136+
return true;
137+
},
138+
{
139+
message: "Password is required",
140+
path: ["password"],
141+
},
142+
),
143+
),
121144
});
122145

123146
const password = form.watch("password");
@@ -138,6 +161,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
138161
registryUrl: registry.registryUrl,
139162
imagePrefix: registry.imagePrefix || "",
140163
registryName: registry.registryName,
164+
isEditing: true,
141165
});
142166
} else {
143167
form.reset({
@@ -146,21 +170,29 @@ export const HandleRegistry = ({ registryId }: Props) => {
146170
registryUrl: "",
147171
imagePrefix: "",
148172
serverId: "",
173+
isEditing: false,
149174
});
150175
}
151176
}, [form, form.reset, form.formState.isSubmitSuccessful, registry]);
152177

153178
const onSubmit = async (data: AddRegistry) => {
154-
await mutateAsync({
155-
password: data.password,
179+
const payload: any = {
156180
registryName: data.registryName,
157181
username: data.username,
158182
registryUrl: data.registryUrl || "",
159183
registryType: "cloud",
160184
imagePrefix: data.imagePrefix,
161185
serverId: data.serverId,
162186
registryId: registryId || "",
163-
})
187+
};
188+
189+
// Only include password if it's been provided (not empty)
190+
// When editing, empty password means "keep the existing password"
191+
if (data.password && data.password.length > 0) {
192+
payload.password = data.password;
193+
}
194+
195+
await mutateAsync(payload)
164196
.then(async (_data) => {
165197
await utils.registry.all.invalidate();
166198
toast.success(registryId ? "Registry updated" : "Registry added");
@@ -198,11 +230,14 @@ export const HandleRegistry = ({ registryId }: Props) => {
198230
Fill the next fields to add a external registry.
199231
</DialogDescription>
200232
</DialogHeader>
201-
{(isError || testRegistryIsError) && (
233+
{(isError || testRegistryIsError || testRegistryByIdIsError) && (
202234
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
203235
<AlertTriangle className="text-red-600 dark:text-red-400" />
204236
<span className="text-sm text-red-600 dark:text-red-400">
205-
{testRegistryError?.message || error?.message || ""}
237+
{testRegistryError?.message ||
238+
testRegistryByIdError?.message ||
239+
error?.message ||
240+
""}
206241
</span>
207242
</div>
208243
)}
@@ -253,10 +288,20 @@ export const HandleRegistry = ({ registryId }: Props) => {
253288
name="password"
254289
render={({ field }) => (
255290
<FormItem>
256-
<FormLabel>Password</FormLabel>
291+
<FormLabel>Password{registryId && " (Optional)"}</FormLabel>
292+
{registryId && (
293+
<FormDescription>
294+
Leave blank to keep existing password. Enter new
295+
password to test or update it.
296+
</FormDescription>
297+
)}
257298
<FormControl>
258299
<Input
259-
placeholder="Password"
300+
placeholder={
301+
registryId
302+
? "Leave blank to keep existing"
303+
: "Password"
304+
}
260305
autoComplete="one-time-code"
261306
{...field}
262307
type="password"
@@ -387,15 +432,45 @@ export const HandleRegistry = ({ registryId }: Props) => {
387432
<Button
388433
type="button"
389434
variant={"secondary"}
390-
isLoading={isLoading}
435+
isLoading={isLoading || isLoadingById}
391436
onClick={async () => {
437+
// When editing with empty password, use the existing password from DB
438+
if (registryId && (!password || password.length === 0)) {
439+
await testRegistryById({
440+
registryId: registryId || "",
441+
...(serverId && { serverId }),
442+
})
443+
.then((data) => {
444+
if (data) {
445+
toast.success("Registry Tested Successfully");
446+
} else {
447+
toast.error("Registry Test Failed");
448+
}
449+
})
450+
.catch(() => {
451+
toast.error("Error testing the registry");
452+
});
453+
return;
454+
}
455+
456+
// When creating, password is required
457+
if (!registryId && (!password || password.length === 0)) {
458+
form.setError("password", {
459+
type: "manual",
460+
message: "Password is required",
461+
});
462+
return;
463+
}
464+
465+
// When creating or editing with new password, validate and test with provided credentials
392466
const validationResult = AddRegistrySchema.safeParse({
393467
username,
394468
password,
395469
registryUrl,
396470
registryName: "Dokploy Registry",
397471
imagePrefix,
398472
serverId,
473+
isEditing: !!registryId,
399474
});
400475

401476
if (!validationResult.success) {

apps/dokploy/server/api/routers/registry.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
apiFindOneRegistry,
1616
apiRemoveRegistry,
1717
apiTestRegistry,
18+
apiTestRegistryById,
1819
apiUpdateRegistry,
1920
registry,
2021
} from "@/server/db/schema";
@@ -109,6 +110,67 @@ export const registryRouter = createTRPCRouter({
109110
});
110111
}
111112

113+
return true;
114+
} catch (error) {
115+
throw new TRPCError({
116+
code: "BAD_REQUEST",
117+
message:
118+
error instanceof Error
119+
? error.message
120+
: "Error testing the registry",
121+
cause: error,
122+
});
123+
}
124+
}),
125+
testRegistryById: protectedProcedure
126+
.input(apiTestRegistryById)
127+
.mutation(async ({ input, ctx }) => {
128+
try {
129+
// Get the full registry with password from database
130+
const registryData = await db.query.registry.findFirst({
131+
where: eq(registry.registryId, input.registryId ?? ""),
132+
});
133+
134+
if (!registryData) {
135+
throw new TRPCError({
136+
code: "NOT_FOUND",
137+
message: "Registry not found",
138+
});
139+
}
140+
141+
if (registryData.organizationId !== ctx.session.activeOrganizationId) {
142+
throw new TRPCError({
143+
code: "UNAUTHORIZED",
144+
message: "You are not allowed to test this registry",
145+
});
146+
}
147+
148+
const args = [
149+
"login",
150+
registryData.registryUrl,
151+
"--username",
152+
registryData.username,
153+
"--password-stdin",
154+
];
155+
156+
if (IS_CLOUD && !input.serverId) {
157+
throw new TRPCError({
158+
code: "NOT_FOUND",
159+
message: "Select a server to test the registry",
160+
});
161+
}
162+
163+
if (input.serverId && input.serverId !== "none") {
164+
await execAsyncRemote(
165+
input.serverId,
166+
`echo ${registryData.password} | docker ${args.join(" ")}`,
167+
);
168+
} else {
169+
await execFileAsync("docker", args, {
170+
input: Buffer.from(registryData.password).toString(),
171+
});
172+
}
173+
112174
return true;
113175
} catch (error) {
114176
throw new TRPCError({

packages/server/src/db/schema/registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ export const apiTestRegistry = createSchema.pick({}).extend({
8080
serverId: z.string().optional(),
8181
});
8282

83+
export const apiTestRegistryById = createSchema
84+
.pick({
85+
registryId: true,
86+
})
87+
.extend({
88+
serverId: z.string().optional(),
89+
});
90+
8391
export const apiRemoveRegistry = createSchema
8492
.pick({
8593
registryId: true,

0 commit comments

Comments
 (0)