Skip to content

Commit 48be854

Browse files
authored
Merge pull request #3215 from Dokploy/3198-bug-docker-swarm-deployment-fails-due-to-duplicate-username-in-image-tag
test(upload): add unit tests for getRegistryTag function
2 parents c233ddb + ee411ac commit 48be854

File tree

2 files changed

+241
-3
lines changed

2 files changed

+241
-3
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import type { Registry } from "@dokploy/server";
2+
import { getRegistryTag } from "@dokploy/server";
3+
import { describe, expect, it } from "vitest";
4+
5+
describe("getRegistryTag", () => {
6+
// Helper to create a mock registry
7+
const createMockRegistry = (overrides: Partial<Registry> = {}): Registry => {
8+
return {
9+
registryId: "test-registry-id",
10+
registryName: "Test Registry",
11+
username: "myuser",
12+
password: "test-password",
13+
registryUrl: "docker.io",
14+
registryType: "cloud",
15+
imagePrefix: null,
16+
createdAt: new Date().toISOString(),
17+
organizationId: "test-org-id",
18+
...overrides,
19+
};
20+
};
21+
22+
describe("with username (no imagePrefix)", () => {
23+
it("should handle simple image name without tag", () => {
24+
const registry = createMockRegistry({ username: "myuser" });
25+
const result = getRegistryTag(registry, "nginx");
26+
expect(result).toBe("docker.io/myuser/nginx");
27+
});
28+
29+
it("should handle image name with tag", () => {
30+
const registry = createMockRegistry({ username: "myuser" });
31+
const result = getRegistryTag(registry, "nginx:latest");
32+
expect(result).toBe("docker.io/myuser/nginx:latest");
33+
});
34+
35+
it("should handle image name with username already present (no duplication)", () => {
36+
const registry = createMockRegistry({ username: "myuser" });
37+
const result = getRegistryTag(registry, "myuser/myprivaterepo");
38+
// Should not duplicate username
39+
expect(result).toBe("docker.io/myuser/myprivaterepo");
40+
});
41+
42+
it("should handle image name with username and tag already present", () => {
43+
const registry = createMockRegistry({ username: "myuser" });
44+
const result = getRegistryTag(registry, "myuser/myprivaterepo:latest");
45+
// Should not duplicate username
46+
expect(result).toBe("docker.io/myuser/myprivaterepo:latest");
47+
});
48+
49+
it("should handle complex image name with username", () => {
50+
const registry = createMockRegistry({ username: "siumauricio" });
51+
const result = getRegistryTag(
52+
registry,
53+
"siumauricio/app-parse-multi-byte-port-e32uh7",
54+
);
55+
// Should not duplicate username
56+
expect(result).toBe(
57+
"docker.io/siumauricio/app-parse-multi-byte-port-e32uh7",
58+
);
59+
});
60+
61+
it("should handle image name with different username (should not duplicate)", () => {
62+
const registry = createMockRegistry({ username: "myuser" });
63+
const result = getRegistryTag(registry, "otheruser/myprivaterepo");
64+
expect(result).toBe("docker.io/myuser/myprivaterepo");
65+
});
66+
67+
it("should handle image name with full registry URL (no username)", () => {
68+
const registry = createMockRegistry({ username: "myuser" });
69+
const result = getRegistryTag(registry, "docker.io/nginx");
70+
// Should add username since imageName doesn't have one
71+
expect(result).toBe("docker.io/myuser/nginx");
72+
});
73+
74+
it("should handle image name with custom registry URL and username", () => {
75+
const registry = createMockRegistry({ username: "myuser" });
76+
const result = getRegistryTag(registry, "ghcr.io/myuser/repo");
77+
// Should not duplicate username even if registry URL is different
78+
expect(result).toBe("docker.io/myuser/repo");
79+
});
80+
81+
it("should handle image name with custom registry URL (different username)", () => {
82+
const registry = createMockRegistry({ username: "myuser" });
83+
const result = getRegistryTag(registry, "ghcr.io/otheruser/repo");
84+
// Should use registry username, not the one in imageName
85+
expect(result).toBe("docker.io/myuser/repo");
86+
});
87+
});
88+
89+
describe("with imagePrefix", () => {
90+
it("should use imagePrefix instead of username", () => {
91+
const registry = createMockRegistry({
92+
username: "myuser",
93+
imagePrefix: "myorg",
94+
});
95+
const result = getRegistryTag(registry, "nginx");
96+
expect(result).toBe("docker.io/myorg/nginx");
97+
});
98+
99+
it("should use imagePrefix with image tag", () => {
100+
const registry = createMockRegistry({
101+
username: "myuser",
102+
imagePrefix: "myorg",
103+
});
104+
const result = getRegistryTag(registry, "nginx:latest");
105+
expect(result).toBe("docker.io/myorg/nginx:latest");
106+
});
107+
108+
it("should handle imagePrefix with username already in image name", () => {
109+
const registry = createMockRegistry({
110+
username: "myuser",
111+
imagePrefix: "myorg",
112+
});
113+
const result = getRegistryTag(registry, "myuser/myprivaterepo");
114+
expect(result).toBe("docker.io/myorg/myprivaterepo");
115+
});
116+
117+
it("should handle imagePrefix matching image name prefix", () => {
118+
const registry = createMockRegistry({
119+
username: "myuser",
120+
imagePrefix: "myorg",
121+
});
122+
const result = getRegistryTag(registry, "myorg/myprivaterepo");
123+
// Should not duplicate prefix
124+
expect(result).toBe("docker.io/myorg/myprivaterepo");
125+
});
126+
});
127+
128+
describe("without registryUrl", () => {
129+
it("should work without registryUrl", () => {
130+
const registry = createMockRegistry({
131+
username: "myuser",
132+
registryUrl: "",
133+
});
134+
const result = getRegistryTag(registry, "nginx");
135+
expect(result).toBe("myuser/nginx");
136+
});
137+
138+
it("should work without registryUrl with imagePrefix", () => {
139+
const registry = createMockRegistry({
140+
username: "myuser",
141+
imagePrefix: "myorg",
142+
registryUrl: "",
143+
});
144+
const result = getRegistryTag(registry, "nginx");
145+
expect(result).toBe("myorg/nginx");
146+
});
147+
148+
it("should handle username already present without registryUrl", () => {
149+
const registry = createMockRegistry({
150+
username: "myuser",
151+
registryUrl: "",
152+
});
153+
const result = getRegistryTag(registry, "myuser/myprivaterepo");
154+
// Should not duplicate username
155+
expect(result).toBe("myuser/myprivaterepo");
156+
});
157+
});
158+
159+
describe("with custom registryUrl", () => {
160+
it("should handle custom registry URL", () => {
161+
const registry = createMockRegistry({
162+
username: "myuser",
163+
registryUrl: "ghcr.io",
164+
});
165+
const result = getRegistryTag(registry, "nginx");
166+
expect(result).toBe("ghcr.io/myuser/nginx");
167+
});
168+
169+
it("should handle custom registry URL with imagePrefix", () => {
170+
const registry = createMockRegistry({
171+
username: "myuser",
172+
imagePrefix: "myorg",
173+
registryUrl: "ghcr.io",
174+
});
175+
const result = getRegistryTag(registry, "nginx");
176+
expect(result).toBe("ghcr.io/myorg/nginx");
177+
});
178+
179+
it("should handle custom registry URL with username already present", () => {
180+
const registry = createMockRegistry({
181+
username: "myuser",
182+
registryUrl: "ghcr.io",
183+
});
184+
const result = getRegistryTag(registry, "myuser/myprivaterepo");
185+
// Should not duplicate username
186+
expect(result).toBe("ghcr.io/myuser/myprivaterepo");
187+
});
188+
});
189+
190+
describe("edge cases", () => {
191+
it("should handle empty image name", () => {
192+
const registry = createMockRegistry({ username: "myuser" });
193+
const result = getRegistryTag(registry, "");
194+
expect(result).toBe("docker.io/myuser/");
195+
});
196+
197+
it("should handle image name with multiple slashes", () => {
198+
const registry = createMockRegistry({ username: "myuser" });
199+
const result = getRegistryTag(registry, "org/suborg/repo");
200+
expect(result).toBe("docker.io/myuser/repo");
201+
});
202+
203+
it("should handle image name with username at different position", () => {
204+
const registry = createMockRegistry({ username: "myuser" });
205+
const result = getRegistryTag(registry, "org/myuser/repo");
206+
expect(result).toBe("docker.io/myuser/repo");
207+
});
208+
});
209+
});

packages/server/src/utils/cluster/upload.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,40 @@ export const uploadImageRemoteCommand = async (
7474
throw error;
7575
}
7676
};
77+
/**
78+
* Extract the repository name from imageName by taking the last part after '/'
79+
* Examples:
80+
* - "nginx" -> "nginx"
81+
* - "nginx:latest" -> "nginx:latest"
82+
* - "myuser/myrepo" -> "myrepo"
83+
* - "myuser/myrepo:tag" -> "myrepo:tag"
84+
* - "docker.io/myuser/myrepo" -> "myrepo"
85+
*/
86+
const extractRepositoryName = (imageName: string): string => {
87+
const lastSlashIndex = imageName.lastIndexOf("/");
88+
89+
// If no '/', return the imageName as is
90+
if (lastSlashIndex === -1) {
91+
return imageName;
92+
}
93+
94+
// Extract everything after the last '/'
95+
return imageName.substring(lastSlashIndex + 1);
96+
};
97+
7798
export const getRegistryTag = (registry: Registry, imageName: string) => {
7899
const { registryUrl, imagePrefix, username } = registry;
79-
return imagePrefix
80-
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
81-
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
100+
101+
// Extract the repository name (last part after '/')
102+
const repositoryName = extractRepositoryName(imageName);
103+
104+
// Build the final tag using registry's username/prefix
105+
const targetPrefix = imagePrefix || username;
106+
const finalRegistry = registryUrl || "";
107+
108+
return finalRegistry
109+
? `${finalRegistry}/${targetPrefix}/${repositoryName}`
110+
: `${targetPrefix}/${repositoryName}`;
82111
};
83112

84113
const getRegistryCommands = (

0 commit comments

Comments
 (0)