@fenwave/agent 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +11 -0
- package/Dockerfile +12 -0
- package/LICENSE +29 -0
- package/README.md +434 -0
- package/auth.js +276 -0
- package/cli-commands.js +1185 -0
- package/containerManager.js +385 -0
- package/convert-to-esm.sh +62 -0
- package/docker-actions/apps.js +3256 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +346 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +188 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +696 -0
- package/helper-functions.js +193 -0
- package/index.html +60 -0
- package/index.js +988 -0
- package/package.json +49 -0
- package/setup/setupWizard.js +499 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +174 -0
- package/store/deviceCredentialStore.js +107 -0
- package/store/npmTokenStore.js +65 -0
- package/store/registryStore.js +329 -0
- package/store/setupState.js +147 -0
- package/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +54 -0
- package/utils/errorHandler.js +327 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/websocket-server.js +364 -0
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
import { docker } from "./containers.js";
|
|
2
|
+
import { formatSize, formatCreatedTime } from "../helper-functions.js";
|
|
3
|
+
import registryStore from "../store/registryStore.js";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { ECRClient, GetAuthorizationTokenCommand } from "@aws-sdk/client-ecr";
|
|
7
|
+
|
|
8
|
+
// Function to verify Docker Hub push success
|
|
9
|
+
async function verifyDockerHubPush(
|
|
10
|
+
username,
|
|
11
|
+
repoName,
|
|
12
|
+
tag,
|
|
13
|
+
authUsername,
|
|
14
|
+
authPassword
|
|
15
|
+
) {
|
|
16
|
+
try {
|
|
17
|
+
// Docker Hub API endpoint to check if repository/tag exists
|
|
18
|
+
const apiUrl = `https://hub.docker.com/v2/repositories/${username}/${repoName}/tags/${tag}/`;
|
|
19
|
+
|
|
20
|
+
const response = await axios.get(apiUrl, {
|
|
21
|
+
auth: {
|
|
22
|
+
username: authUsername,
|
|
23
|
+
password: authPassword,
|
|
24
|
+
},
|
|
25
|
+
timeout: 10000,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return response.status === 200;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// If 404, the tag doesn't exist (push failed)
|
|
31
|
+
// If 401/403, authentication issue
|
|
32
|
+
// If other error, network/API issue
|
|
33
|
+
console.log(
|
|
34
|
+
chalk.red(
|
|
35
|
+
`❌ Docker Hub verification failed: ${
|
|
36
|
+
error.response?.status || error.message
|
|
37
|
+
}`
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Function to get ECR authentication token
|
|
45
|
+
async function getEcrAuth(registryUrl, credentials) {
|
|
46
|
+
try {
|
|
47
|
+
// Validate that we have the required credentials
|
|
48
|
+
if (!credentials) {
|
|
49
|
+
throw new Error("ECR credentials not provided");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!credentials.accessKeyId || !credentials.secretAccessKey) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"ECR credentials are missing accessKeyId or secretAccessKey"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Extract region from ECR URL
|
|
59
|
+
const urlParts = registryUrl.replace(/^https?:\/\//, "").split(".");
|
|
60
|
+
const region = urlParts[3]; // Extract region from ECR URL format
|
|
61
|
+
|
|
62
|
+
if (!region) {
|
|
63
|
+
throw new Error("Unable to extract region from ECR URL");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Use the provided credentials or fall back to the region from URL if available
|
|
67
|
+
const ecrCredentials = {
|
|
68
|
+
accessKeyId: credentials?.accessKeyId,
|
|
69
|
+
secretAccessKey: credentials?.secretAccessKey,
|
|
70
|
+
sessionToken: credentials?.sessionToken,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Remove undefined values
|
|
74
|
+
Object.keys(ecrCredentials).forEach((key) => {
|
|
75
|
+
if (ecrCredentials[key] === undefined) {
|
|
76
|
+
delete ecrCredentials[key];
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const client = new ECRClient({
|
|
81
|
+
region: credentials?.region || region,
|
|
82
|
+
credentials: ecrCredentials,
|
|
83
|
+
});
|
|
84
|
+
const command = new GetAuthorizationTokenCommand({});
|
|
85
|
+
const response = await client.send(command);
|
|
86
|
+
|
|
87
|
+
const authData = response.authorizationData?.[0];
|
|
88
|
+
if (!authData) {
|
|
89
|
+
throw new Error("No authorization data received from ECR");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Decode base64 token
|
|
93
|
+
const token = Buffer.from(authData.authorizationToken, "base64").toString();
|
|
94
|
+
const [username, password] = token.split(":");
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
username,
|
|
98
|
+
password,
|
|
99
|
+
serveraddress: authData.proxyEndpoint, // e.g., https://123456789012.dkr.ecr.us-east-1.amazonaws.com
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Helper function to generate meaningful error messages based on registry type and error
|
|
107
|
+
function getRegistrySpecificErrorMessage(error, registry, imageName) {
|
|
108
|
+
const errorMessage = error.message || error.toString();
|
|
109
|
+
|
|
110
|
+
// Check for common error patterns
|
|
111
|
+
const isNotFound =
|
|
112
|
+
errorMessage.includes("404") ||
|
|
113
|
+
errorMessage.includes("not found") ||
|
|
114
|
+
errorMessage.includes("manifest unknown") ||
|
|
115
|
+
// Unauthorized can also mean "not found" when pulling from wrong registry
|
|
116
|
+
(errorMessage.includes("unauthorized") &&
|
|
117
|
+
errorMessage.includes("Not Authorized"));
|
|
118
|
+
|
|
119
|
+
const isAuthError =
|
|
120
|
+
errorMessage.includes("no basic auth credentials") ||
|
|
121
|
+
(errorMessage.includes("authentication") &&
|
|
122
|
+
!errorMessage.includes("Not Authorized")) ||
|
|
123
|
+
errorMessage.includes("access denied");
|
|
124
|
+
|
|
125
|
+
if (isAuthError) {
|
|
126
|
+
switch (registry?.type) {
|
|
127
|
+
case "ecr":
|
|
128
|
+
return `ECR authentication failed. Please check your AWS credentials and permissions: ${errorMessage}`;
|
|
129
|
+
case "docker-hub":
|
|
130
|
+
return `Docker Hub authentication failed. Please check your username and password: ${errorMessage}`;
|
|
131
|
+
case "gcr":
|
|
132
|
+
return `GCR authentication failed. Please check your service account credentials: ${errorMessage}`;
|
|
133
|
+
case "acr":
|
|
134
|
+
return `ACR authentication failed. Please check your credentials: ${errorMessage}`;
|
|
135
|
+
default:
|
|
136
|
+
return `Registry authentication failed. Please check your credentials: ${errorMessage}`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isNotFound) {
|
|
141
|
+
switch (registry?.type) {
|
|
142
|
+
case "ecr":
|
|
143
|
+
return `Image "${imageName}" not found in "${registry.name}" registry. Please verify the image name and tag exist.`;
|
|
144
|
+
case "docker-hub":
|
|
145
|
+
return `Image "${imageName}" not found in "${registry.name}" registry. Please verify the repository name and tag exist.`;
|
|
146
|
+
case "gcr":
|
|
147
|
+
return `Image "${imageName}" not found in "${registry.name}" registry. Please verify the image path and tag exist.`;
|
|
148
|
+
case "acr":
|
|
149
|
+
return `Image "${imageName}" not found in "${registry.name}" registry. Please verify the repository and tag exist.`;
|
|
150
|
+
case "custom":
|
|
151
|
+
return `Image "${imageName}" not found in "${registry.name}" registry. Please verify the image exists.`;
|
|
152
|
+
default:
|
|
153
|
+
return `Image "${imageName}" not found in registry "${
|
|
154
|
+
registry?.name || "selected registry"
|
|
155
|
+
}". Please verify the image name and tag exist.`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// For other errors, return a generic message with the original error
|
|
160
|
+
return `Failed to pull image "${imageName}" from ${
|
|
161
|
+
registry?.name || "registry"
|
|
162
|
+
}: ${errorMessage}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function handleImageAction(ws, action, payload) {
|
|
166
|
+
switch (action) {
|
|
167
|
+
case "fetchImages":
|
|
168
|
+
return await handleFetchImages(ws, payload);
|
|
169
|
+
case "pullImage":
|
|
170
|
+
return await handlePullImage(ws, payload);
|
|
171
|
+
case "deleteImage":
|
|
172
|
+
return await handleDeleteImage(ws, payload);
|
|
173
|
+
case "pushImage":
|
|
174
|
+
return await handlePushImage(ws, payload);
|
|
175
|
+
case "tagImage":
|
|
176
|
+
return await handleTagImage(ws, payload);
|
|
177
|
+
case "inspectImage":
|
|
178
|
+
return await handleInspectImage(ws, payload);
|
|
179
|
+
default:
|
|
180
|
+
throw new Error(`Unknown image action: ${action}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function handleFetchImages(ws, payload = {}) {
|
|
185
|
+
try {
|
|
186
|
+
const images = await docker.listImages();
|
|
187
|
+
const formattedImages = [];
|
|
188
|
+
|
|
189
|
+
images.forEach((image) => {
|
|
190
|
+
if (image.RepoTags && image.RepoTags.length > 0) {
|
|
191
|
+
// Create a separate entry for each tag
|
|
192
|
+
image.RepoTags.forEach((repoTag) => {
|
|
193
|
+
if (repoTag !== "<none>:<none>") {
|
|
194
|
+
const parts = repoTag.split(":");
|
|
195
|
+
const name = parts[0];
|
|
196
|
+
const tag = parts.length > 1 ? parts[1] : "latest";
|
|
197
|
+
|
|
198
|
+
formattedImages.push({
|
|
199
|
+
id: `${image.Id}-${name}:${tag}`, // Create unique ID for each tag
|
|
200
|
+
dockerId: image.Id, // Keep original Docker image ID
|
|
201
|
+
name,
|
|
202
|
+
tag,
|
|
203
|
+
size: formatSize(image.Size),
|
|
204
|
+
created: formatCreatedTime(image.Created),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
// Handle images without tags
|
|
210
|
+
formattedImages.push({
|
|
211
|
+
id: `${image.Id}-<none>:<none>`, // Create unique ID
|
|
212
|
+
dockerId: image.Id, // Keep original Docker image ID
|
|
213
|
+
name: "<none>",
|
|
214
|
+
tag: "<none>",
|
|
215
|
+
size: formatSize(image.Size),
|
|
216
|
+
created: formatCreatedTime(image.Created),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
ws.send(
|
|
222
|
+
JSON.stringify({
|
|
223
|
+
type: "images",
|
|
224
|
+
images: formattedImages,
|
|
225
|
+
requestId: payload.requestId,
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error("Error fetching images:", error);
|
|
230
|
+
ws.send(
|
|
231
|
+
JSON.stringify({
|
|
232
|
+
type: "error",
|
|
233
|
+
error: "Failed to fetch images: " + error.message,
|
|
234
|
+
requestId: payload.requestId,
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function handlePullImage(ws, payload) {
|
|
241
|
+
try {
|
|
242
|
+
const { imageTag, registryId, requestId } = payload;
|
|
243
|
+
|
|
244
|
+
// Get the registry details if registryId is provided
|
|
245
|
+
let registry = null;
|
|
246
|
+
if (registryId && registryId.trim() !== "") {
|
|
247
|
+
registry = await registryStore.getRegistryWithCredentials(registryId);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Split image tag into name and tag
|
|
251
|
+
const [name, tag = "latest"] = imageTag.split(":");
|
|
252
|
+
|
|
253
|
+
// Check if the image already exists locally
|
|
254
|
+
const existingImages = await docker.listImages();
|
|
255
|
+
const existingImage = existingImages.find(
|
|
256
|
+
(image) => image.RepoTags && image.RepoTags.includes(`${name}:${tag}`)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (existingImage) {
|
|
260
|
+
console.log(`⚠️ Image ${name}:${tag} already exists locally`);
|
|
261
|
+
ws.send(
|
|
262
|
+
JSON.stringify({
|
|
263
|
+
type: "pullWarning",
|
|
264
|
+
imageTag: `${name}:${tag}`,
|
|
265
|
+
requestId,
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
ws.send(
|
|
269
|
+
JSON.stringify({
|
|
270
|
+
type: "imagePulled",
|
|
271
|
+
image: {
|
|
272
|
+
id: `${existingImage.Id}-${name}:${tag}`,
|
|
273
|
+
dockerId: existingImage.Id,
|
|
274
|
+
name,
|
|
275
|
+
tag,
|
|
276
|
+
size: formatSize(existingImage.Size),
|
|
277
|
+
created: formatCreatedTime(existingImage.Created),
|
|
278
|
+
},
|
|
279
|
+
requestId,
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let pullCommand = `${name}:${tag}`;
|
|
286
|
+
|
|
287
|
+
// Handle different registry types
|
|
288
|
+
if (registry) {
|
|
289
|
+
console.log(
|
|
290
|
+
`🔍 Pulling image: ${pullCommand} from registry: ${registry.name} (${registry.type})`
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Send initial pulling started notification
|
|
294
|
+
ws.send(
|
|
295
|
+
JSON.stringify({
|
|
296
|
+
type: "pullStarted",
|
|
297
|
+
imageTag: pullCommand,
|
|
298
|
+
registry: registry.name,
|
|
299
|
+
requestId,
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
switch (registry.type) {
|
|
304
|
+
case "docker-hub":
|
|
305
|
+
pullCommand = `${name}:${tag}`;
|
|
306
|
+
// Docker Hub authentication will be handled in the pull options
|
|
307
|
+
break;
|
|
308
|
+
|
|
309
|
+
case "ecr": {
|
|
310
|
+
// Extract only the registry host (strip protocol and any path/tag)
|
|
311
|
+
const registryHost = (registry.url || "")
|
|
312
|
+
.replace(/^https?:\/\//, "")
|
|
313
|
+
.split("/")[0];
|
|
314
|
+
// If the provided image name already includes the registry host, don't prefix it again
|
|
315
|
+
if (name === registryHost || name.startsWith(`${registryHost}/`)) {
|
|
316
|
+
pullCommand = `${name}:${tag}`;
|
|
317
|
+
} else {
|
|
318
|
+
pullCommand = `${registryHost}/${name}:${tag}`;
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
case "gcr": {
|
|
324
|
+
const registryHost = (registry.url || "")
|
|
325
|
+
.replace(/^https?:\/\//, "")
|
|
326
|
+
.split("/")[0];
|
|
327
|
+
if (name === registryHost || name.startsWith(`${registryHost}/`)) {
|
|
328
|
+
pullCommand = `${name}:${tag}`;
|
|
329
|
+
} else {
|
|
330
|
+
pullCommand = `${registryHost}/${name}:${tag}`;
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
case "acr": {
|
|
336
|
+
const registryHost = (registry.url || "")
|
|
337
|
+
.replace(/^https?:\/\//, "")
|
|
338
|
+
.split("/")[0];
|
|
339
|
+
if (name === registryHost || name.startsWith(`${registryHost}/`)) {
|
|
340
|
+
pullCommand = `${name}:${tag}`;
|
|
341
|
+
} else {
|
|
342
|
+
pullCommand = `${registryHost}/${name}:${tag}`;
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
case "custom": {
|
|
348
|
+
const registryHost = (registry.url || "")
|
|
349
|
+
.replace(/^https?:\/\//, "")
|
|
350
|
+
.split("/")[0];
|
|
351
|
+
if (name === registryHost || name.startsWith(`${registryHost}/`)) {
|
|
352
|
+
pullCommand = `${name}:${tag}`;
|
|
353
|
+
} else {
|
|
354
|
+
pullCommand = `${registryHost}/${name}:${tag}`;
|
|
355
|
+
}
|
|
356
|
+
// Custom registry authentication will be handled in the pull options
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
console.log(`🔍 Pulling image: ${pullCommand} from Docker Hub (public)`);
|
|
362
|
+
|
|
363
|
+
ws.send(
|
|
364
|
+
JSON.stringify({
|
|
365
|
+
type: "pullStarted",
|
|
366
|
+
imageTag: pullCommand,
|
|
367
|
+
registry: "Docker Hub",
|
|
368
|
+
requestId,
|
|
369
|
+
})
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Attempt to pull the image
|
|
374
|
+
try {
|
|
375
|
+
// Prepare authentication options if needed
|
|
376
|
+
let pullOptions = {};
|
|
377
|
+
|
|
378
|
+
if (registry) {
|
|
379
|
+
let authconfig = {};
|
|
380
|
+
|
|
381
|
+
switch (registry.type) {
|
|
382
|
+
case "docker-hub":
|
|
383
|
+
if (
|
|
384
|
+
registry.credentials?.username &&
|
|
385
|
+
registry.credentials?.password
|
|
386
|
+
) {
|
|
387
|
+
authconfig = {
|
|
388
|
+
username: registry.credentials.username,
|
|
389
|
+
password: registry.credentials.password,
|
|
390
|
+
serveraddress: "https://index.docker.io/v1/",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
case "ecr":
|
|
395
|
+
try {
|
|
396
|
+
// Get ECR authentication token dynamically
|
|
397
|
+
authconfig = await getEcrAuth(registry.url, registry.credentials);
|
|
398
|
+
} catch (ecrAuthError) {
|
|
399
|
+
console.error(
|
|
400
|
+
chalk.red("❌ ECR authentication failed:", ecrAuthError.message)
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
case "custom":
|
|
405
|
+
if (
|
|
406
|
+
registry.credentials?.username &&
|
|
407
|
+
registry.credentials?.password
|
|
408
|
+
) {
|
|
409
|
+
authconfig = {
|
|
410
|
+
username: registry.credentials.username,
|
|
411
|
+
password: registry.credentials.password,
|
|
412
|
+
serveraddress: registry.url,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
// GCR and ACR typically use token-based auth which is handled differently
|
|
417
|
+
default:
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (Object.keys(authconfig).length > 0) {
|
|
422
|
+
pullOptions.authconfig = authconfig;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const stream = await docker.pull(pullCommand, pullOptions);
|
|
427
|
+
|
|
428
|
+
// Send progress updates
|
|
429
|
+
docker.modem.followProgress(
|
|
430
|
+
stream,
|
|
431
|
+
async (err, _) => {
|
|
432
|
+
if (err) {
|
|
433
|
+
console.error(
|
|
434
|
+
chalk.red(`❌ Failed to pull ${pullCommand}:`, err.message)
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Generate registry-specific error message
|
|
438
|
+
const errorMessage = getRegistrySpecificErrorMessage(
|
|
439
|
+
err,
|
|
440
|
+
registry,
|
|
441
|
+
pullCommand
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
ws.send(
|
|
445
|
+
JSON.stringify({
|
|
446
|
+
type: "pullError",
|
|
447
|
+
error: errorMessage,
|
|
448
|
+
requestId,
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Successfully pulled from the original registry
|
|
455
|
+
await handleSuccessfulPull(
|
|
456
|
+
ws,
|
|
457
|
+
pullCommand,
|
|
458
|
+
registry ? registry.name : "Docker Hub",
|
|
459
|
+
name,
|
|
460
|
+
tag,
|
|
461
|
+
requestId
|
|
462
|
+
);
|
|
463
|
+
},
|
|
464
|
+
(event) => {
|
|
465
|
+
if (event.progress || event.status) {
|
|
466
|
+
ws.send(
|
|
467
|
+
JSON.stringify({
|
|
468
|
+
type: "pullProgress",
|
|
469
|
+
imageTag: pullCommand,
|
|
470
|
+
progress: event,
|
|
471
|
+
requestId,
|
|
472
|
+
})
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
} catch (pullError) {
|
|
478
|
+
console.error(
|
|
479
|
+
chalk.red(`❌ Failed to initiate pull for ${pullCommand}:`),
|
|
480
|
+
pullError.message
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// Generate registry-specific error message
|
|
484
|
+
const errorMessage = getRegistrySpecificErrorMessage(
|
|
485
|
+
pullError,
|
|
486
|
+
registry,
|
|
487
|
+
pullCommand
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
ws.send(
|
|
491
|
+
JSON.stringify({
|
|
492
|
+
type: "pullError",
|
|
493
|
+
error: errorMessage,
|
|
494
|
+
requestId,
|
|
495
|
+
})
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.error("Error pulling image:", error);
|
|
500
|
+
ws.send(
|
|
501
|
+
JSON.stringify({
|
|
502
|
+
type: "pullError",
|
|
503
|
+
error: "Failed to pull image: " + error.message,
|
|
504
|
+
requestId: payload.requestId,
|
|
505
|
+
})
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Helper function to handle successful pull completion
|
|
511
|
+
async function handleSuccessfulPull(
|
|
512
|
+
ws,
|
|
513
|
+
pullCommand,
|
|
514
|
+
registryName,
|
|
515
|
+
name,
|
|
516
|
+
tag,
|
|
517
|
+
requestId
|
|
518
|
+
) {
|
|
519
|
+
try {
|
|
520
|
+
// Get the pulled image
|
|
521
|
+
const images = await docker.listImages({
|
|
522
|
+
filters: { reference: [pullCommand] },
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
if (images.length === 0) {
|
|
526
|
+
// Try to find image with just name:tag if full path didn't work
|
|
527
|
+
const alternativeImages = await docker.listImages({
|
|
528
|
+
filters: { reference: [`${name}:${tag}`] },
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
if (alternativeImages.length === 0) {
|
|
532
|
+
ws.send(
|
|
533
|
+
JSON.stringify({
|
|
534
|
+
type: "pullError",
|
|
535
|
+
error: "Image not found after pull",
|
|
536
|
+
requestId,
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const image = alternativeImages[0];
|
|
543
|
+
console.log(
|
|
544
|
+
`✅ Successfully pulled: ${name}:${tag} from ${registryName} registry`
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
ws.send(
|
|
548
|
+
JSON.stringify({
|
|
549
|
+
type: "pullCompleted",
|
|
550
|
+
imageTag: `${name}:${tag}`,
|
|
551
|
+
registry: registryName,
|
|
552
|
+
requestId,
|
|
553
|
+
})
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
ws.send(
|
|
557
|
+
JSON.stringify({
|
|
558
|
+
type: "imagePulled",
|
|
559
|
+
image: {
|
|
560
|
+
id: `${image.Id}-${name}:${tag}`,
|
|
561
|
+
dockerId: image.Id,
|
|
562
|
+
name,
|
|
563
|
+
tag,
|
|
564
|
+
size: formatSize(image.Size),
|
|
565
|
+
created: formatCreatedTime(image.Created),
|
|
566
|
+
},
|
|
567
|
+
requestId,
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const image = images[0];
|
|
574
|
+
console.log(
|
|
575
|
+
`✅ Successfully pulled: ${pullCommand} from ${registryName} registry`
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
ws.send(
|
|
579
|
+
JSON.stringify({
|
|
580
|
+
type: "pullCompleted",
|
|
581
|
+
imageTag: pullCommand,
|
|
582
|
+
registry: registryName,
|
|
583
|
+
requestId,
|
|
584
|
+
})
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
ws.send(
|
|
588
|
+
JSON.stringify({
|
|
589
|
+
type: "imagePulled",
|
|
590
|
+
image: {
|
|
591
|
+
id: `${image.Id}-${name}:${tag}`,
|
|
592
|
+
dockerId: image.Id,
|
|
593
|
+
name,
|
|
594
|
+
tag,
|
|
595
|
+
size: formatSize(image.Size),
|
|
596
|
+
created: formatCreatedTime(image.Created),
|
|
597
|
+
},
|
|
598
|
+
requestId,
|
|
599
|
+
})
|
|
600
|
+
);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
console.error("Error in handleSuccessfulPull:", error);
|
|
603
|
+
ws.send(
|
|
604
|
+
JSON.stringify({
|
|
605
|
+
type: "pullError",
|
|
606
|
+
error: "Failed to process pulled image: " + error.message,
|
|
607
|
+
requestId,
|
|
608
|
+
})
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function handleDeleteImage(ws, payload) {
|
|
614
|
+
try {
|
|
615
|
+
// Check if this is a specific tag deletion (format: imageId-name:tag)
|
|
616
|
+
const isTagSpecific = payload.id.includes("-") && payload.id.includes(":");
|
|
617
|
+
|
|
618
|
+
if (isTagSpecific) {
|
|
619
|
+
// Extract the tag name from the composite ID
|
|
620
|
+
const parts = payload.id.split("-");
|
|
621
|
+
if (parts.length >= 2) {
|
|
622
|
+
const tagName = parts.slice(1).join("-"); // Rejoin in case image name contains dashes
|
|
623
|
+
console.log(`🗑️ Deleting image: ${tagName}`);
|
|
624
|
+
|
|
625
|
+
// Use the tag name to remove just this tag
|
|
626
|
+
const image = docker.getImage(tagName);
|
|
627
|
+
await image.remove({ force: false, noprune: true });
|
|
628
|
+
} else {
|
|
629
|
+
throw new Error("Invalid tag format in image ID");
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
// This is a direct image ID deletion (fallback for old format)
|
|
633
|
+
const image = docker.getImage(payload.id);
|
|
634
|
+
|
|
635
|
+
// Check if force delete is requested
|
|
636
|
+
const options = {};
|
|
637
|
+
if (payload.force) {
|
|
638
|
+
options.force = true;
|
|
639
|
+
console.log(`⚠️ Force deleting image: ${payload.id}`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
await image.remove(options);
|
|
643
|
+
console.log(`✅ Successfully deleted image: ${payload.id}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
ws.send(
|
|
647
|
+
JSON.stringify({
|
|
648
|
+
type: "imageDeleted",
|
|
649
|
+
id: payload.id,
|
|
650
|
+
success: true,
|
|
651
|
+
requestId: payload.requestId,
|
|
652
|
+
})
|
|
653
|
+
);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
console.error("Error deleting image:", error);
|
|
656
|
+
|
|
657
|
+
let errorMessage = "Failed to delete image: " + error.message;
|
|
658
|
+
let errorCode = null;
|
|
659
|
+
|
|
660
|
+
// Handle specific Docker error cases
|
|
661
|
+
if (error.statusCode === 409) {
|
|
662
|
+
errorCode = "CONFLICT";
|
|
663
|
+
if (error.message.includes("image is being used by running container")) {
|
|
664
|
+
const containerMatch = error.message.match(/container ([a-f0-9]+)/);
|
|
665
|
+
const containerId = containerMatch ? containerMatch[1] : "unknown";
|
|
666
|
+
errorMessage = `Cannot delete image: it's being used by running container ${containerId}. Stop the container first or use force delete.`;
|
|
667
|
+
} else if (error.message.includes("image has dependent child images")) {
|
|
668
|
+
errorMessage =
|
|
669
|
+
"Cannot delete image: it has dependent child images. Delete child images first or use force delete.";
|
|
670
|
+
}
|
|
671
|
+
} else if (error.statusCode === 404) {
|
|
672
|
+
errorCode = "NOT_FOUND";
|
|
673
|
+
errorMessage = "Image not found. It may have already been deleted.";
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
ws.send(
|
|
677
|
+
JSON.stringify({
|
|
678
|
+
type: "error",
|
|
679
|
+
error: errorMessage,
|
|
680
|
+
errorCode,
|
|
681
|
+
requestId: payload.requestId,
|
|
682
|
+
})
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function handlePushImage(ws, payload) {
|
|
688
|
+
try {
|
|
689
|
+
const { imageTag, registryId, requestId } = payload;
|
|
690
|
+
|
|
691
|
+
// Get the registry details
|
|
692
|
+
const registry = await registryStore.getRegistryWithCredentials(registryId);
|
|
693
|
+
if (!registry) {
|
|
694
|
+
throw new Error("Registry not found");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
console.log(
|
|
698
|
+
`🚀 Pushing image: ${imageTag} to registry: ${registry.name} (${registry.type})`
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
// Validate registry has required credentials
|
|
702
|
+
if (registry.type === "docker-hub") {
|
|
703
|
+
if (!registry.credentials?.username && !registry.username) {
|
|
704
|
+
throw new Error(
|
|
705
|
+
"Docker Hub username not found in registry credentials"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (!registry.credentials?.password) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
"Docker Hub password not found in registry credentials"
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
ws.send(
|
|
716
|
+
JSON.stringify({
|
|
717
|
+
type: "pushStarted",
|
|
718
|
+
imageTag: imageTag,
|
|
719
|
+
registry: registry.name,
|
|
720
|
+
requestId,
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
const [imageName, imageTagPart = "latest"] = imageTag.split(":");
|
|
725
|
+
|
|
726
|
+
// Find the local image first
|
|
727
|
+
const images = await docker.listImages();
|
|
728
|
+
const localImage = images.find(
|
|
729
|
+
(img) => img.RepoTags && img.RepoTags.some((tag) => tag === imageTag)
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
if (!localImage) {
|
|
733
|
+
throw new Error(`Local image ${imageTag} not found`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Format the image name for the target registry
|
|
737
|
+
let targetImageName;
|
|
738
|
+
let authConfig = {};
|
|
739
|
+
|
|
740
|
+
switch (registry.type) {
|
|
741
|
+
case "docker-hub":
|
|
742
|
+
// For Docker Hub, extract the actual image name without any existing namespace
|
|
743
|
+
const baseImageName = imageName.includes("/")
|
|
744
|
+
? imageName.split("/").pop()
|
|
745
|
+
: imageName;
|
|
746
|
+
const dockerHubUsername =
|
|
747
|
+
registry.credentials?.username || registry.username;
|
|
748
|
+
targetImageName = `${dockerHubUsername}/${baseImageName}:${imageTagPart}`;
|
|
749
|
+
|
|
750
|
+
if (!dockerHubUsername) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
"Docker Hub username not found in registry credentials"
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (!registry.credentials?.password) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
"Docker Hub password not found in registry credentials"
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
authConfig = {
|
|
762
|
+
username: dockerHubUsername,
|
|
763
|
+
password: registry.credentials?.password,
|
|
764
|
+
serveraddress: "https://index.docker.io/v1/",
|
|
765
|
+
};
|
|
766
|
+
break;
|
|
767
|
+
|
|
768
|
+
case "ecr":
|
|
769
|
+
const ecrUrl = registry.url.replace("https://", "");
|
|
770
|
+
targetImageName = `${ecrUrl}/${imageName}:${imageTagPart}`;
|
|
771
|
+
try {
|
|
772
|
+
// Get ECR authentication token dynamically for push
|
|
773
|
+
authConfig = await getEcrAuth(registry.url, registry.credentials);
|
|
774
|
+
} catch (ecrAuthError) {
|
|
775
|
+
console.error(
|
|
776
|
+
chalk.red(
|
|
777
|
+
`❌ ECR authentication failed for push:`,
|
|
778
|
+
ecrAuthError.message
|
|
779
|
+
)
|
|
780
|
+
);
|
|
781
|
+
throw new Error(`ECR authentication failed: ${ecrAuthError.message}`);
|
|
782
|
+
}
|
|
783
|
+
break;
|
|
784
|
+
|
|
785
|
+
case "gcr":
|
|
786
|
+
const gcrUrl = registry.url.replace("https://", "");
|
|
787
|
+
targetImageName = `${gcrUrl}/${imageName}:${imageTagPart}`;
|
|
788
|
+
break;
|
|
789
|
+
|
|
790
|
+
case "acr":
|
|
791
|
+
const acrUrl = registry.url.replace("https://", "");
|
|
792
|
+
targetImageName = `${acrUrl}/${imageName}:${imageTagPart}`;
|
|
793
|
+
authConfig = {
|
|
794
|
+
username: registry.credentials?.username || registry.username,
|
|
795
|
+
password: registry.credentials?.password,
|
|
796
|
+
serveraddress: registry.url,
|
|
797
|
+
};
|
|
798
|
+
break;
|
|
799
|
+
|
|
800
|
+
case "custom":
|
|
801
|
+
const customUrl = registry.url.replace("https://", "");
|
|
802
|
+
targetImageName = `${customUrl}/${imageName}:${imageTagPart}`;
|
|
803
|
+
authConfig = {
|
|
804
|
+
username: registry.credentials?.username || registry.username,
|
|
805
|
+
password: registry.credentials?.password,
|
|
806
|
+
serveraddress: registry.url,
|
|
807
|
+
};
|
|
808
|
+
break;
|
|
809
|
+
|
|
810
|
+
default:
|
|
811
|
+
throw new Error(`Unsupported registry type: ${registry.type}`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Tag the image for the target registry
|
|
815
|
+
const dockerImage = docker.getImage(imageTag);
|
|
816
|
+
const [targetRepo, targetTag] = targetImageName.split(":");
|
|
817
|
+
|
|
818
|
+
await dockerImage.tag({ repo: targetRepo, tag: targetTag });
|
|
819
|
+
|
|
820
|
+
// Push the tagged image with authentication
|
|
821
|
+
const taggedImage = docker.getImage(targetImageName);
|
|
822
|
+
const pushOptions = {
|
|
823
|
+
tag: targetTag,
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
if (Object.keys(authConfig).length > 0) {
|
|
827
|
+
pushOptions.authconfig = authConfig;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const stream = await taggedImage.push(pushOptions);
|
|
831
|
+
|
|
832
|
+
// Send progress updates
|
|
833
|
+
docker.modem.followProgress(
|
|
834
|
+
stream,
|
|
835
|
+
async (err, output) => {
|
|
836
|
+
if (err) {
|
|
837
|
+
console.error(
|
|
838
|
+
chalk.red(`❌ Failed to push ${targetImageName}:`, err.message)
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
let errorMessage = `Failed to push image: ${err.message}`;
|
|
842
|
+
if (
|
|
843
|
+
registry.type === "docker-hub" &&
|
|
844
|
+
(err.message.includes("denied") ||
|
|
845
|
+
err.message.includes("unauthorized"))
|
|
846
|
+
) {
|
|
847
|
+
errorMessage = `Push denied to Docker Hub. Please check: 1) Repository name is correct, 2) You have push access to '${targetImageName}', 3) Your credentials are valid.`;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Clean up the temporary tag on error
|
|
851
|
+
try {
|
|
852
|
+
const tempTaggedImage = docker.getImage(targetImageName);
|
|
853
|
+
await tempTaggedImage.remove({ force: false, noprune: false });
|
|
854
|
+
console.log(
|
|
855
|
+
`🧹 Cleaned up temporary tag after error: ${targetImageName}`
|
|
856
|
+
);
|
|
857
|
+
} catch (cleanupError) {
|
|
858
|
+
console.warn(
|
|
859
|
+
`⚠️ Could not clean up temporary tag after error: ${cleanupError.message}`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
ws.send(
|
|
864
|
+
JSON.stringify({
|
|
865
|
+
type: "pushError",
|
|
866
|
+
error: errorMessage,
|
|
867
|
+
imageTag: imageTag,
|
|
868
|
+
requestId,
|
|
869
|
+
})
|
|
870
|
+
);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Check if push actually completed successfully by examining the output
|
|
875
|
+
let pushSuccess = false;
|
|
876
|
+
if (output && Array.isArray(output)) {
|
|
877
|
+
// Look for successful push indicators in the output
|
|
878
|
+
pushSuccess = output.some(
|
|
879
|
+
(item) =>
|
|
880
|
+
item.status &&
|
|
881
|
+
(item.status.includes("Pushed") ||
|
|
882
|
+
item.status.includes("Layer already exists") ||
|
|
883
|
+
item.status.includes(`${targetTag}: digest:`))
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (!pushSuccess) {
|
|
888
|
+
console.error(
|
|
889
|
+
chalk.red(
|
|
890
|
+
`❌ Push may have failed - no success indicators found in output`
|
|
891
|
+
)
|
|
892
|
+
);
|
|
893
|
+
ws.send(
|
|
894
|
+
JSON.stringify({
|
|
895
|
+
type: "pushError",
|
|
896
|
+
error: `Push to ${registry.name} may have failed - please check your registry access and credentials`,
|
|
897
|
+
imageTag: imageTag,
|
|
898
|
+
requestId,
|
|
899
|
+
})
|
|
900
|
+
);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
console.log(
|
|
905
|
+
`✅ Successfully pushed: ${targetImageName} to ${registry.name} registry`
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
// For Docker Hub, verify the push actually succeeded
|
|
909
|
+
let pushVerified = true;
|
|
910
|
+
if (registry.type === "docker-hub") {
|
|
911
|
+
const [dockerHubUsername, repoName] = targetRepo.split("/");
|
|
912
|
+
try {
|
|
913
|
+
pushVerified = await verifyDockerHubPush(
|
|
914
|
+
dockerHubUsername,
|
|
915
|
+
repoName,
|
|
916
|
+
targetTag,
|
|
917
|
+
authConfig.username,
|
|
918
|
+
authConfig.password
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
if (!pushVerified) {
|
|
922
|
+
console.error(
|
|
923
|
+
chalk.red(
|
|
924
|
+
`❌ Docker Hub verification failed - image not found in registry`
|
|
925
|
+
)
|
|
926
|
+
);
|
|
927
|
+
ws.send(
|
|
928
|
+
JSON.stringify({
|
|
929
|
+
type: "pushError",
|
|
930
|
+
error: `Push to Docker Hub failed - image ${targetImageName} was not found in the registry. Please check your repository name and permissions.`,
|
|
931
|
+
imageTag: imageTag,
|
|
932
|
+
requestId,
|
|
933
|
+
})
|
|
934
|
+
);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
} catch (verifyError) {
|
|
938
|
+
console.warn(
|
|
939
|
+
`⚠️ Could not verify Docker Hub push: ${verifyError.message}`
|
|
940
|
+
);
|
|
941
|
+
// Continue without verification if API check fails
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Clean up the temporary tag
|
|
946
|
+
try {
|
|
947
|
+
const tempTaggedImage = docker.getImage(targetImageName);
|
|
948
|
+
await tempTaggedImage.remove({ force: false, noprune: false });
|
|
949
|
+
} catch (cleanupError) {
|
|
950
|
+
console.warn(
|
|
951
|
+
`⚠️ Could not clean up temporary tag: ${cleanupError.message}`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
ws.send(
|
|
956
|
+
JSON.stringify({
|
|
957
|
+
type: "pushCompleted",
|
|
958
|
+
imageTag: imageTag,
|
|
959
|
+
registry: registry.name,
|
|
960
|
+
requestId,
|
|
961
|
+
})
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
ws.send(
|
|
965
|
+
JSON.stringify({
|
|
966
|
+
type: "imagePushed",
|
|
967
|
+
image: {
|
|
968
|
+
id: localImage.Id,
|
|
969
|
+
name: targetRepo,
|
|
970
|
+
tag: targetTag,
|
|
971
|
+
pushed: true,
|
|
972
|
+
registry: registry.name,
|
|
973
|
+
targetImageName: targetImageName,
|
|
974
|
+
},
|
|
975
|
+
requestId,
|
|
976
|
+
})
|
|
977
|
+
);
|
|
978
|
+
},
|
|
979
|
+
(event) => {
|
|
980
|
+
if (event.progress || event.status) {
|
|
981
|
+
ws.send(
|
|
982
|
+
JSON.stringify({
|
|
983
|
+
type: "pushProgress",
|
|
984
|
+
imageTag: targetImageName,
|
|
985
|
+
progress: event,
|
|
986
|
+
requestId,
|
|
987
|
+
})
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
} catch (error) {
|
|
993
|
+
console.error("Error pushing image:", error);
|
|
994
|
+
ws.send(
|
|
995
|
+
JSON.stringify({
|
|
996
|
+
type: "pushError",
|
|
997
|
+
error: "Failed to push image: " + error.message,
|
|
998
|
+
imageTag: payload.imageTag,
|
|
999
|
+
requestId: payload.requestId,
|
|
1000
|
+
})
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
async function handleTagImage(ws, payload) {
|
|
1006
|
+
try {
|
|
1007
|
+
const { id, newTag, requestId } = payload;
|
|
1008
|
+
|
|
1009
|
+
const [repo, tag = "latest"] = newTag.split(":");
|
|
1010
|
+
|
|
1011
|
+
// Validate the repo and tag format
|
|
1012
|
+
if (!repo || repo.trim() === "") {
|
|
1013
|
+
throw new Error("Repository name cannot be empty");
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Check if the tag already exists
|
|
1017
|
+
const existingImages = await docker.listImages();
|
|
1018
|
+
const tagExists = existingImages.some(
|
|
1019
|
+
(img) =>
|
|
1020
|
+
img.RepoTags && img.RepoTags.includes(`${repo.trim()}:${tag.trim()}`)
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
if (tagExists) {
|
|
1024
|
+
throw new Error(
|
|
1025
|
+
`Tag '${repo.trim()}:${tag.trim()}' already exists. Please choose a different tag name.`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const image = docker.getImage(id);
|
|
1030
|
+
await image.tag({ repo: repo.trim(), tag: tag.trim() });
|
|
1031
|
+
|
|
1032
|
+
console.log(`✅ Successfully tagged image ${id} as ${repo}:${tag}`);
|
|
1033
|
+
|
|
1034
|
+
// Get the tagged image
|
|
1035
|
+
const images = await docker.listImages();
|
|
1036
|
+
const taggedImage = images.find(
|
|
1037
|
+
(img) => img.RepoTags && img.RepoTags.includes(`${repo}:${tag}`)
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
if (!taggedImage) {
|
|
1041
|
+
console.error(
|
|
1042
|
+
chalk.red(`❌ Tagged image ${repo}:${tag} not found in list`)
|
|
1043
|
+
);
|
|
1044
|
+
ws.send(
|
|
1045
|
+
JSON.stringify({
|
|
1046
|
+
type: "error",
|
|
1047
|
+
error: "Tagged image not found after tagging operation",
|
|
1048
|
+
requestId,
|
|
1049
|
+
})
|
|
1050
|
+
);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const responseImage = {
|
|
1055
|
+
id: `${taggedImage.Id}-${repo}:${tag}`, // Create unique ID for the table
|
|
1056
|
+
dockerId: taggedImage.Id, // Original Docker image ID
|
|
1057
|
+
name: repo,
|
|
1058
|
+
tag,
|
|
1059
|
+
size: formatSize(taggedImage.Size),
|
|
1060
|
+
created: formatCreatedTime(taggedImage.Created),
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
ws.send(
|
|
1064
|
+
JSON.stringify({
|
|
1065
|
+
type: "imageTagged",
|
|
1066
|
+
image: responseImage,
|
|
1067
|
+
requestId,
|
|
1068
|
+
})
|
|
1069
|
+
);
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
let errorMessage = "Failed to tag image: " + error.message;
|
|
1072
|
+
|
|
1073
|
+
// Handle specific Docker error cases
|
|
1074
|
+
if (
|
|
1075
|
+
error.message.includes("already exists") ||
|
|
1076
|
+
error.message.includes("Tag")
|
|
1077
|
+
) {
|
|
1078
|
+
// This is our custom duplicate tag error
|
|
1079
|
+
errorMessage = error.message;
|
|
1080
|
+
} else if (error.statusCode === 409) {
|
|
1081
|
+
errorMessage = "Tag already exists. Please choose a different tag name.";
|
|
1082
|
+
} else if (error.statusCode === 404) {
|
|
1083
|
+
errorMessage = "Image not found. It may have been deleted.";
|
|
1084
|
+
} else if (
|
|
1085
|
+
error.message.includes("invalid") ||
|
|
1086
|
+
error.message.includes("Invalid")
|
|
1087
|
+
) {
|
|
1088
|
+
errorMessage = error.message;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
ws.send(
|
|
1092
|
+
JSON.stringify({
|
|
1093
|
+
type: "error",
|
|
1094
|
+
error: errorMessage,
|
|
1095
|
+
requestId: payload.requestId,
|
|
1096
|
+
})
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function handleInspectImage(ws, payload) {
|
|
1102
|
+
try {
|
|
1103
|
+
const { id, requestId } = payload;
|
|
1104
|
+
const image = docker.getImage(id);
|
|
1105
|
+
const inspectionData = await image.inspect();
|
|
1106
|
+
|
|
1107
|
+
ws.send(
|
|
1108
|
+
JSON.stringify({
|
|
1109
|
+
type: "imageInspected",
|
|
1110
|
+
data: inspectionData,
|
|
1111
|
+
requestId,
|
|
1112
|
+
})
|
|
1113
|
+
);
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
console.error("Error inspecting image:", error);
|
|
1116
|
+
ws.send(
|
|
1117
|
+
JSON.stringify({
|
|
1118
|
+
type: "error",
|
|
1119
|
+
error: "Failed to inspect image: " + error.message,
|
|
1120
|
+
requestId: payload.requestId,
|
|
1121
|
+
})
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
export default { handleImageAction };
|
|
1127
|
+
|
|
1128
|
+
export { handleImageAction };
|