@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,1100 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import {
|
|
4
|
+
ECRClient,
|
|
5
|
+
DescribeRepositoriesCommand,
|
|
6
|
+
DescribeImagesCommand,
|
|
7
|
+
} from "@aws-sdk/client-ecr";
|
|
8
|
+
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
|
|
9
|
+
import { GoogleAuth } from "google-auth-library";
|
|
10
|
+
import { formatSize, formatCreatedTime } from "../helper-functions.js";
|
|
11
|
+
import registryStore from "../store/registryStore.js";
|
|
12
|
+
|
|
13
|
+
async function handleRegistryAction(ws, action, payload) {
|
|
14
|
+
switch (action) {
|
|
15
|
+
case "fetchRegistries":
|
|
16
|
+
return await handleFetchRegistries(ws, payload);
|
|
17
|
+
case "connectRegistry":
|
|
18
|
+
return await handleConnectRegistry(ws, payload);
|
|
19
|
+
case "disconnectRegistry":
|
|
20
|
+
return await handleDisconnectRegistry(ws, payload);
|
|
21
|
+
case "renameRegistry":
|
|
22
|
+
return await handleRenameRegistry(ws, payload);
|
|
23
|
+
case "setActiveRegistry":
|
|
24
|
+
return await handleSetActiveRegistry(ws, payload);
|
|
25
|
+
case "fetchRegistryImages":
|
|
26
|
+
return await handleFetchRegistryImages(ws, payload);
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unknown registry action: ${action}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function handleFetchRegistries(ws, payload = {}) {
|
|
33
|
+
try {
|
|
34
|
+
// Get all registries from persistent store
|
|
35
|
+
const registries = await registryStore.getAllRegistries();
|
|
36
|
+
|
|
37
|
+
ws.send(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
type: "registries",
|
|
40
|
+
registries,
|
|
41
|
+
requestId: payload.requestId,
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("Error fetching registries:", error);
|
|
46
|
+
ws.send(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
type: "error",
|
|
49
|
+
error: "Failed to fetch registries: " + error.message,
|
|
50
|
+
requestId: payload.requestId,
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function handleConnectRegistry(ws, payload) {
|
|
57
|
+
try {
|
|
58
|
+
const {
|
|
59
|
+
name,
|
|
60
|
+
url,
|
|
61
|
+
type,
|
|
62
|
+
username,
|
|
63
|
+
password,
|
|
64
|
+
accessKeyId,
|
|
65
|
+
secretAccessKey,
|
|
66
|
+
region,
|
|
67
|
+
sessionToken,
|
|
68
|
+
serviceAccountJson,
|
|
69
|
+
requestId,
|
|
70
|
+
} = payload;
|
|
71
|
+
|
|
72
|
+
// Basic URL validation
|
|
73
|
+
try {
|
|
74
|
+
new URL(url);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(`Invalid registry URL: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let authenticationValid = false;
|
|
80
|
+
let errorMessage = "";
|
|
81
|
+
let extractedUsername = username;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
switch (type) {
|
|
85
|
+
case "docker-hub":
|
|
86
|
+
authenticationValid = await testDockerHubCredentials(
|
|
87
|
+
username,
|
|
88
|
+
password
|
|
89
|
+
);
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
case "ecr":
|
|
93
|
+
const ecrResult = await testECRCredentials(
|
|
94
|
+
accessKeyId,
|
|
95
|
+
secretAccessKey,
|
|
96
|
+
region,
|
|
97
|
+
sessionToken
|
|
98
|
+
);
|
|
99
|
+
authenticationValid = ecrResult.success;
|
|
100
|
+
extractedUsername = ecrResult.username;
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case "gcr":
|
|
104
|
+
authenticationValid = await testGCRCredentials(
|
|
105
|
+
serviceAccountJson,
|
|
106
|
+
url
|
|
107
|
+
);
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case "acr":
|
|
111
|
+
authenticationValid = await testACRCredentials(
|
|
112
|
+
username,
|
|
113
|
+
password,
|
|
114
|
+
url
|
|
115
|
+
);
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case "custom":
|
|
119
|
+
authenticationValid = await testCustomRegistryCredentials(
|
|
120
|
+
username,
|
|
121
|
+
password,
|
|
122
|
+
url
|
|
123
|
+
);
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
default:
|
|
127
|
+
throw new Error(`Unsupported registry type: ${type}`);
|
|
128
|
+
}
|
|
129
|
+
} catch (authError) {
|
|
130
|
+
authenticationValid = false;
|
|
131
|
+
errorMessage = authError.message;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!authenticationValid) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
errorMessage || "Authentication failed: Invalid credentials"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Generate a unique ID for the registry
|
|
141
|
+
const registryId = `reg-${Date.now()}`;
|
|
142
|
+
|
|
143
|
+
// Create registry object with proper credential storage and metadata
|
|
144
|
+
const registry = {
|
|
145
|
+
id: registryId,
|
|
146
|
+
name,
|
|
147
|
+
url,
|
|
148
|
+
type,
|
|
149
|
+
connected: true,
|
|
150
|
+
connectedAt: new Date().toISOString(),
|
|
151
|
+
// Store metadata at top level for API exposure
|
|
152
|
+
...(type === "ecr" && {
|
|
153
|
+
username: extractedUsername,
|
|
154
|
+
region,
|
|
155
|
+
}),
|
|
156
|
+
...(type === "gcr" &&
|
|
157
|
+
serviceAccountJson &&
|
|
158
|
+
(() => {
|
|
159
|
+
try {
|
|
160
|
+
const serviceAccount = JSON.parse(serviceAccountJson);
|
|
161
|
+
return { projectId: serviceAccount.project_id };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error("Error parsing service account JSON:", error);
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
})()),
|
|
167
|
+
...(username && { username }),
|
|
168
|
+
credentials: {
|
|
169
|
+
// Store credentials based on registry type
|
|
170
|
+
...(type === "ecr" && {
|
|
171
|
+
accessKeyId,
|
|
172
|
+
secretAccessKey,
|
|
173
|
+
sessionToken,
|
|
174
|
+
}),
|
|
175
|
+
...(type === "gcr" && { serviceAccountJson }),
|
|
176
|
+
...(type === "docker-hub" && { username, password }),
|
|
177
|
+
...(type === "acr" && { username, password }),
|
|
178
|
+
...(type === "custom" && { username, password }),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Store the registry persistently
|
|
183
|
+
await registryStore.upsertRegistry(registry);
|
|
184
|
+
|
|
185
|
+
console.log(`✅ Successfully connected to registry: ${name}`);
|
|
186
|
+
|
|
187
|
+
// Return registry with safe metadata (credentials excluded)
|
|
188
|
+
const { credentials, ...safeRegistry } = registry;
|
|
189
|
+
|
|
190
|
+
ws.send(
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
type: "registryConnected",
|
|
193
|
+
registry: safeRegistry,
|
|
194
|
+
requestId,
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(chalk.red("❌ Error connecting to registry:", error));
|
|
199
|
+
ws.send(
|
|
200
|
+
JSON.stringify({
|
|
201
|
+
type: "error",
|
|
202
|
+
error: "Failed to connect to registry: " + error.message,
|
|
203
|
+
requestId: payload.requestId,
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Test Docker Hub credentials
|
|
210
|
+
async function testDockerHubCredentials(username, password) {
|
|
211
|
+
const baseUrl = "https://hub.docker.com/v2";
|
|
212
|
+
try {
|
|
213
|
+
const tokenResponse = await axios.post(
|
|
214
|
+
`${baseUrl}/users/login/`,
|
|
215
|
+
{
|
|
216
|
+
username: username,
|
|
217
|
+
password: password,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
headers: {
|
|
221
|
+
"Content-Type": "application/json",
|
|
222
|
+
},
|
|
223
|
+
timeout: 15000,
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const token = tokenResponse.data?.token;
|
|
228
|
+
if (!token) {
|
|
229
|
+
throw new Error("No authentication token received");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Fetch user repositories
|
|
233
|
+
const testResponse = await axios.get(
|
|
234
|
+
`${baseUrl}/repositories/${username}/`,
|
|
235
|
+
{
|
|
236
|
+
headers: {
|
|
237
|
+
Authorization: `JWT ${token}`,
|
|
238
|
+
},
|
|
239
|
+
params: {
|
|
240
|
+
page_size: 1,
|
|
241
|
+
},
|
|
242
|
+
timeout: 10000,
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (testResponse.status !== 200) {
|
|
247
|
+
throw new Error(`Unexpected response status: ${testResponse.status}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return true;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error(
|
|
253
|
+
chalk.red(`❌ Docker Hub authentication failed:`, error.message)
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (error.response?.status === 401) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
error.response?.data?.detail || "Invalid username or password/token"
|
|
259
|
+
);
|
|
260
|
+
} else if (error.response?.status === 429) {
|
|
261
|
+
throw new Error("Rate limited by Docker Hub API");
|
|
262
|
+
} else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
263
|
+
throw new Error("Cannot connect to Docker Hub");
|
|
264
|
+
} else {
|
|
265
|
+
throw new Error(`Authentication test failed: ${error.message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Test ECR registry credentials
|
|
271
|
+
async function testECRCredentials(
|
|
272
|
+
accessKeyId,
|
|
273
|
+
secretAccessKey,
|
|
274
|
+
region,
|
|
275
|
+
sessionToken
|
|
276
|
+
) {
|
|
277
|
+
try {
|
|
278
|
+
if (!region || !region.match(/^[a-z0-9-]+$/)) {
|
|
279
|
+
throw new Error("Invalid AWS region format");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const credentials = {
|
|
283
|
+
accessKeyId,
|
|
284
|
+
secretAccessKey,
|
|
285
|
+
sessionToken,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const stsClient = new STSClient({ region, credentials });
|
|
289
|
+
const identity = await stsClient.send(new GetCallerIdentityCommand({}));
|
|
290
|
+
|
|
291
|
+
let username = "Unknown";
|
|
292
|
+
if (identity.Arn) {
|
|
293
|
+
const arnParts = identity.Arn.split("/");
|
|
294
|
+
if (arnParts.length > 0) {
|
|
295
|
+
username = arnParts[arnParts.length - 1];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const ecrClient = new ECRClient({ region, credentials });
|
|
300
|
+
const command = new DescribeRepositoriesCommand({ maxResults: 1 });
|
|
301
|
+
|
|
302
|
+
await ecrClient.send(command);
|
|
303
|
+
|
|
304
|
+
return { success: true, username };
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error(
|
|
307
|
+
chalk.red(`❌ AWS authentication failed:`, error.name, ":", error.message)
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
switch (error.name) {
|
|
311
|
+
case "ExpiredTokenException":
|
|
312
|
+
throw new Error(
|
|
313
|
+
"AWS session has expired. Please use fresh credentials."
|
|
314
|
+
);
|
|
315
|
+
case "AccessDeniedException":
|
|
316
|
+
case "UnrecognizedClientException":
|
|
317
|
+
throw new Error("Invalid AWS credentials or insufficient permissions");
|
|
318
|
+
case "InvalidSignatureException":
|
|
319
|
+
throw new Error("Invalid AWS Secret Access Key");
|
|
320
|
+
case "ValidationException":
|
|
321
|
+
throw new Error("Invalid AWS region or parameter");
|
|
322
|
+
case "CredentialsProviderError":
|
|
323
|
+
throw new Error("AWS Access Key ID not found");
|
|
324
|
+
case "NetworkingError":
|
|
325
|
+
throw new Error("Cannot connect to AWS service");
|
|
326
|
+
case "TokenRefreshRequired":
|
|
327
|
+
throw new Error(
|
|
328
|
+
"AWS credentials need to be refreshed. Please use fresh credentials."
|
|
329
|
+
);
|
|
330
|
+
default:
|
|
331
|
+
throw new Error(`Authentication test failed: ${error.message}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Test GCR registry credentials
|
|
337
|
+
async function testGCRCredentials(serviceAccountJson) {
|
|
338
|
+
try {
|
|
339
|
+
let serviceAccountKey;
|
|
340
|
+
try {
|
|
341
|
+
serviceAccountKey = JSON.parse(serviceAccountJson);
|
|
342
|
+
} catch (parseError) {
|
|
343
|
+
throw new Error("Invalid service account JSON format");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (
|
|
347
|
+
!serviceAccountKey.type ||
|
|
348
|
+
serviceAccountKey.type !== "service_account"
|
|
349
|
+
) {
|
|
350
|
+
throw new Error("Invalid service account type");
|
|
351
|
+
}
|
|
352
|
+
if (!serviceAccountKey.project_id) {
|
|
353
|
+
throw new Error("Missing project_id in service account");
|
|
354
|
+
}
|
|
355
|
+
if (!serviceAccountKey.private_key || !serviceAccountKey.client_email) {
|
|
356
|
+
throw new Error("Missing required credentials in service account");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const auth = new GoogleAuth({
|
|
360
|
+
credentials: serviceAccountKey,
|
|
361
|
+
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const authClient = await auth.getClient();
|
|
365
|
+
const accessToken = await authClient.getAccessToken();
|
|
366
|
+
|
|
367
|
+
if (!accessToken.token) {
|
|
368
|
+
throw new Error("Failed to obtain access token");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const projectId = serviceAccountKey.project_id;
|
|
372
|
+
const registryUrl = `https://gcr.io/v2/${projectId}/tags/list`;
|
|
373
|
+
|
|
374
|
+
const response = await axios.get(registryUrl, {
|
|
375
|
+
headers: {
|
|
376
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
377
|
+
Accept: "application/json",
|
|
378
|
+
},
|
|
379
|
+
timeout: 10000,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (response.status === 200) {
|
|
383
|
+
return true;
|
|
384
|
+
} else {
|
|
385
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error(chalk.red(`❌ GCR authentication failed:`, error.message));
|
|
389
|
+
|
|
390
|
+
if (error.response?.status === 401) {
|
|
391
|
+
throw new Error("Invalid service account credentials");
|
|
392
|
+
} else if (error.response?.status === 403) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
"Service account lacks required permissions for Container Registry"
|
|
395
|
+
);
|
|
396
|
+
} else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
397
|
+
throw new Error("Cannot connect to Google Container Registry");
|
|
398
|
+
} else if (error.message.includes("JSON")) {
|
|
399
|
+
throw new Error("Invalid service account JSON format");
|
|
400
|
+
} else {
|
|
401
|
+
throw new Error(`GCR authentication test failed: ${error.message}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Test ACR registry credentials
|
|
407
|
+
async function testACRCredentials(username, password, url) {
|
|
408
|
+
try {
|
|
409
|
+
const registryName = url.replace("https://", "").replace(".azurecr.io", "");
|
|
410
|
+
|
|
411
|
+
const catalogUrl = `${url}/v2/_catalog`;
|
|
412
|
+
|
|
413
|
+
const response = await axios.get(catalogUrl, {
|
|
414
|
+
headers: {
|
|
415
|
+
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
|
|
416
|
+
"base64"
|
|
417
|
+
)}`,
|
|
418
|
+
},
|
|
419
|
+
timeout: 10000,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
if (response.status === 200) {
|
|
423
|
+
return true;
|
|
424
|
+
} else {
|
|
425
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.error(chalk.red(`❌ ACR authentication failed:`, error.message));
|
|
429
|
+
|
|
430
|
+
if (error.response?.status === 401) {
|
|
431
|
+
throw new Error("Invalid username or password/token");
|
|
432
|
+
} else if (error.response?.status === 404) {
|
|
433
|
+
throw new Error("Registry not found or API not supported");
|
|
434
|
+
} else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
435
|
+
throw new Error("Cannot connect to Azure Container Registry");
|
|
436
|
+
} else {
|
|
437
|
+
throw new Error(`ACR authentication test failed: ${error.message}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Test custom registry credentials
|
|
443
|
+
async function testCustomRegistryCredentials(username, password, url) {
|
|
444
|
+
try {
|
|
445
|
+
const response = await axios.get(`${url}/v2/_catalog`, {
|
|
446
|
+
headers: {
|
|
447
|
+
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
|
|
448
|
+
"base64"
|
|
449
|
+
)}`,
|
|
450
|
+
},
|
|
451
|
+
timeout: 10000,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (response.status === 200) {
|
|
455
|
+
return true;
|
|
456
|
+
} else {
|
|
457
|
+
throw new Error(`Unexpected response status: ${response.status}`);
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error(
|
|
461
|
+
chalk.red(`❌ Custom registry authentication failed:`, error.message)
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
if (error.response?.status === 401) {
|
|
465
|
+
throw new Error("Invalid username or password/token");
|
|
466
|
+
} else if (error.response?.status === 404) {
|
|
467
|
+
throw new Error("Registry not found or API not supported");
|
|
468
|
+
} else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
469
|
+
throw new Error("Cannot connect to registry");
|
|
470
|
+
} else {
|
|
471
|
+
throw new Error(`Authentication test failed: ${error.message}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function handleDisconnectRegistry(ws, payload) {
|
|
477
|
+
try {
|
|
478
|
+
const { id, requestId } = payload;
|
|
479
|
+
|
|
480
|
+
if (!id) {
|
|
481
|
+
throw new Error("Registry ID is required");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Get registry from store first to check if it exists
|
|
485
|
+
const registry = await registryStore.getRegistryWithCredentials(id);
|
|
486
|
+
if (!registry) {
|
|
487
|
+
throw new Error("Registry not found");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Remove from persistent store
|
|
491
|
+
await registryStore.removeRegistry(id);
|
|
492
|
+
|
|
493
|
+
console.log(`✅ Successfully disconnected from registry: ${registry.name}`);
|
|
494
|
+
|
|
495
|
+
ws.send(
|
|
496
|
+
JSON.stringify({
|
|
497
|
+
type: "registryDisconnected",
|
|
498
|
+
id,
|
|
499
|
+
success: true,
|
|
500
|
+
requestId,
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error("Error disconnecting registry:", error);
|
|
505
|
+
ws.send(
|
|
506
|
+
JSON.stringify({
|
|
507
|
+
type: "error",
|
|
508
|
+
error: "Failed to disconnect registry: " + error.message,
|
|
509
|
+
requestId: payload.requestId,
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function handleRenameRegistry(ws, payload) {
|
|
516
|
+
try {
|
|
517
|
+
const { id, newName, requestId } = payload;
|
|
518
|
+
|
|
519
|
+
if (!id) {
|
|
520
|
+
throw new Error("Registry ID is required");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!newName || newName.trim() === "") {
|
|
524
|
+
throw new Error("New name is required");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Get registry from store first to check if it exists
|
|
528
|
+
const registry = await registryStore.getRegistryWithCredentials(id);
|
|
529
|
+
if (!registry) {
|
|
530
|
+
throw new Error("Registry not found");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const oldName = registry.name;
|
|
534
|
+
|
|
535
|
+
// Update the registry with the new name
|
|
536
|
+
const updatedRegistry = {
|
|
537
|
+
...registry,
|
|
538
|
+
name: newName.trim(),
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// Save the updated registry
|
|
542
|
+
await registryStore.upsertRegistry(updatedRegistry);
|
|
543
|
+
|
|
544
|
+
console.log(
|
|
545
|
+
`✅ Successfully renamed registry from: ${oldName} to: ${newName.trim()}`
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
// Return safe registry data (without credentials)
|
|
549
|
+
const { credentials, ...safeRegistry } = updatedRegistry;
|
|
550
|
+
|
|
551
|
+
ws.send(
|
|
552
|
+
JSON.stringify({
|
|
553
|
+
type: "registryRenamed",
|
|
554
|
+
registry: safeRegistry,
|
|
555
|
+
requestId,
|
|
556
|
+
})
|
|
557
|
+
);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
console.error("Error renaming registry:", error);
|
|
560
|
+
ws.send(
|
|
561
|
+
JSON.stringify({
|
|
562
|
+
type: "error",
|
|
563
|
+
error: "Failed to rename registry: " + error.message,
|
|
564
|
+
requestId: payload.requestId,
|
|
565
|
+
})
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function handleSetActiveRegistry(ws, payload) {
|
|
571
|
+
try {
|
|
572
|
+
const { id, requestId } = payload;
|
|
573
|
+
|
|
574
|
+
if (!id) {
|
|
575
|
+
throw new Error("Registry ID is required");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Set the active registry
|
|
579
|
+
const activeRegistry = await registryStore.setActiveRegistry(id);
|
|
580
|
+
|
|
581
|
+
if (!activeRegistry) {
|
|
582
|
+
throw new Error("Registry not found");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Return safe registry data (without credentials)
|
|
586
|
+
const { credentials, ...safeRegistry } = activeRegistry;
|
|
587
|
+
|
|
588
|
+
ws.send(
|
|
589
|
+
JSON.stringify({
|
|
590
|
+
type: "activeRegistrySet",
|
|
591
|
+
registry: safeRegistry,
|
|
592
|
+
requestId,
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error("Error setting active registry:", error);
|
|
597
|
+
ws.send(
|
|
598
|
+
JSON.stringify({
|
|
599
|
+
type: "error",
|
|
600
|
+
error: "Failed to set active registry: " + error.message,
|
|
601
|
+
requestId: payload.requestId,
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function handleFetchRegistryImages(ws, payload) {
|
|
608
|
+
try {
|
|
609
|
+
const { registryId, requestId } = payload;
|
|
610
|
+
|
|
611
|
+
// Get registry from persistent store
|
|
612
|
+
const registry = await registryStore.getRegistryWithCredentials(registryId);
|
|
613
|
+
if (!registry) {
|
|
614
|
+
throw new Error("Registry not found");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
let formattedImages = [];
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
switch (registry.type) {
|
|
621
|
+
case "docker-hub":
|
|
622
|
+
formattedImages = await fetchDockerHubImages(registry);
|
|
623
|
+
break;
|
|
624
|
+
case "ecr":
|
|
625
|
+
formattedImages = await fetchECRImages(registry);
|
|
626
|
+
break;
|
|
627
|
+
case "gcr":
|
|
628
|
+
formattedImages = await fetchGCRImages(registry);
|
|
629
|
+
break;
|
|
630
|
+
case "acr":
|
|
631
|
+
formattedImages = await fetchACRImages(registry);
|
|
632
|
+
break;
|
|
633
|
+
case "custom":
|
|
634
|
+
formattedImages = await fetchCustomRegistryImages(registry);
|
|
635
|
+
break;
|
|
636
|
+
default:
|
|
637
|
+
throw new Error(`Unsupported registry type: ${registry.type}`);
|
|
638
|
+
}
|
|
639
|
+
} catch (fetchError) {
|
|
640
|
+
console.error(
|
|
641
|
+
`Error fetching images from registry ${registry.name}:`,
|
|
642
|
+
fetchError.message
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
ws.send(
|
|
646
|
+
JSON.stringify({
|
|
647
|
+
type: "error",
|
|
648
|
+
error: `Failed to fetch images from registry ${registry.name}: ${fetchError.message}`,
|
|
649
|
+
requestId,
|
|
650
|
+
})
|
|
651
|
+
);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
ws.send(
|
|
656
|
+
JSON.stringify({
|
|
657
|
+
type: "registryImages",
|
|
658
|
+
images: formattedImages,
|
|
659
|
+
requestId,
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
console.error("Error fetching registry images:", error);
|
|
664
|
+
ws.send(
|
|
665
|
+
JSON.stringify({
|
|
666
|
+
type: "error",
|
|
667
|
+
error: "Failed to fetch registry images: " + error.message,
|
|
668
|
+
requestId: payload.requestId,
|
|
669
|
+
})
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Helper function to fetch images from Docker Hub
|
|
675
|
+
async function fetchDockerHubImages(registry) {
|
|
676
|
+
const baseUrl = "https://hub.docker.com/v2";
|
|
677
|
+
try {
|
|
678
|
+
const username = registry.credentials.username;
|
|
679
|
+
const password = registry.credentials.password;
|
|
680
|
+
|
|
681
|
+
const tokenResponse = await axios.post(
|
|
682
|
+
`${baseUrl}/users/login/`,
|
|
683
|
+
{
|
|
684
|
+
username: username,
|
|
685
|
+
password: password,
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
headers: {
|
|
689
|
+
"Content-Type": "application/json",
|
|
690
|
+
},
|
|
691
|
+
timeout: 15000,
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
const token = tokenResponse.data?.token;
|
|
696
|
+
if (!token) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
"Failed to get authentication token - invalid credentials"
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const reposResponse = await axios.get(
|
|
703
|
+
`${baseUrl}/repositories/${username}/`,
|
|
704
|
+
{
|
|
705
|
+
headers: {
|
|
706
|
+
Authorization: `JWT ${token}`,
|
|
707
|
+
},
|
|
708
|
+
params: {
|
|
709
|
+
page_size: 100,
|
|
710
|
+
},
|
|
711
|
+
timeout: 15000,
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
const repositories = reposResponse.data.results || [];
|
|
716
|
+
if (repositories.length === 0) {
|
|
717
|
+
console.warn(`⚠️ No repositories found for user: ${username}`);
|
|
718
|
+
return [];
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const imagePromises = repositories.map(async (repo) => {
|
|
722
|
+
try {
|
|
723
|
+
const tagsResponse = await axios.get(
|
|
724
|
+
`${baseUrl}/repositories/${username}/${repo.name}/tags/`,
|
|
725
|
+
{
|
|
726
|
+
headers: {
|
|
727
|
+
Authorization: `JWT ${token}`,
|
|
728
|
+
},
|
|
729
|
+
params: {
|
|
730
|
+
page_size: 100,
|
|
731
|
+
},
|
|
732
|
+
timeout: 10000,
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const tags = tagsResponse.data.results || [];
|
|
737
|
+
|
|
738
|
+
return tags.map((tag) => ({
|
|
739
|
+
id: `dh-${repo.name}-${tag.name}`,
|
|
740
|
+
name: `${username}/${repo.name}`,
|
|
741
|
+
tag: tag.name,
|
|
742
|
+
size: tag.full_size ? formatSize(tag.full_size) : "Unknown",
|
|
743
|
+
created: tag.last_updated
|
|
744
|
+
? formatCreatedTime(new Date(tag.last_updated).getTime() / 1000)
|
|
745
|
+
: "Unknown",
|
|
746
|
+
registry: registry.id,
|
|
747
|
+
isPrivate: repo.is_private || false,
|
|
748
|
+
starCount: repo.star_count || 0,
|
|
749
|
+
pullCount: repo.pull_count || 0,
|
|
750
|
+
architecture: tag.images?.[0]?.architecture || "amd64",
|
|
751
|
+
os: tag.images?.[0]?.os || "linux",
|
|
752
|
+
}));
|
|
753
|
+
} catch (tagError) {
|
|
754
|
+
console.error(
|
|
755
|
+
chalk.red(
|
|
756
|
+
`❌ Error fetching tags for repository ${repo.name}:`,
|
|
757
|
+
tagError.message
|
|
758
|
+
)
|
|
759
|
+
);
|
|
760
|
+
return [];
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const imageArrays = await Promise.all(imagePromises);
|
|
765
|
+
const allImages = imageArrays.flat().filter((image) => image);
|
|
766
|
+
|
|
767
|
+
return allImages;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
console.error(chalk.red("❌ Error fetching Docker Hub images:", error));
|
|
770
|
+
|
|
771
|
+
if (error.response?.status === 401) {
|
|
772
|
+
throw new Error("Authentication failed: Invalid credentials");
|
|
773
|
+
} else if (error.response?.status === 429) {
|
|
774
|
+
throw new Error("Rate limited: Too many requests to Docker Hub API");
|
|
775
|
+
} else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
776
|
+
throw new Error("Cannot connect to Docker Hub");
|
|
777
|
+
} else {
|
|
778
|
+
throw new Error(`Docker Hub API error: ${error.message}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Helper function to fetch images from AWS ECR
|
|
784
|
+
async function fetchECRImages(registry) {
|
|
785
|
+
try {
|
|
786
|
+
const credentials = {
|
|
787
|
+
accessKeyId: registry.credentials.accessKeyId,
|
|
788
|
+
secretAccessKey: registry.credentials.secretAccessKey,
|
|
789
|
+
sessionToken: registry.credentials.sessionToken,
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// Remove undefined values
|
|
793
|
+
Object.keys(credentials).forEach((key) => {
|
|
794
|
+
if (credentials[key] === undefined) {
|
|
795
|
+
delete credentials[key];
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const ecr = new ECRClient({
|
|
800
|
+
region: registry.region,
|
|
801
|
+
credentials,
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
const reposResult = await ecr.send(
|
|
805
|
+
new DescribeRepositoriesCommand({
|
|
806
|
+
maxResults: 100,
|
|
807
|
+
})
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
const repositories = reposResult.repositories || [];
|
|
811
|
+
|
|
812
|
+
if (repositories.length === 0) {
|
|
813
|
+
return [];
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const imagePromises = repositories.map(async (repo) => {
|
|
817
|
+
try {
|
|
818
|
+
const imagesResult = await ecr.send(
|
|
819
|
+
new DescribeImagesCommand({
|
|
820
|
+
repositoryName: repo.repositoryName,
|
|
821
|
+
maxResults: 100,
|
|
822
|
+
})
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
const images = imagesResult.imageDetails || [];
|
|
826
|
+
|
|
827
|
+
return images.map((image) => {
|
|
828
|
+
const tags = image.imageTags || ["<untagged>"];
|
|
829
|
+
const primaryTag = tags[0];
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
id: `ecr-${repo.repositoryName}-${primaryTag}`,
|
|
833
|
+
name: `${registry.url.replace("https://", "")}/${
|
|
834
|
+
repo.repositoryName
|
|
835
|
+
}`,
|
|
836
|
+
tag: primaryTag,
|
|
837
|
+
size: image.imageSizeInBytes
|
|
838
|
+
? formatSize(image.imageSizeInBytes)
|
|
839
|
+
: "Unknown",
|
|
840
|
+
created: image.imagePushedAt
|
|
841
|
+
? formatCreatedTime(image.imagePushedAt.getTime() / 1000)
|
|
842
|
+
: "Unknown",
|
|
843
|
+
registry: registry.id,
|
|
844
|
+
isPrivate: true,
|
|
845
|
+
pullCount: 0,
|
|
846
|
+
starCount: 0,
|
|
847
|
+
architecture: "amd64",
|
|
848
|
+
os: "linux",
|
|
849
|
+
};
|
|
850
|
+
});
|
|
851
|
+
} catch (imageError) {
|
|
852
|
+
console.error(
|
|
853
|
+
chalk.red(
|
|
854
|
+
`❌ Error fetching images for repository ${repo.repositoryName}:`,
|
|
855
|
+
imageError.message
|
|
856
|
+
)
|
|
857
|
+
);
|
|
858
|
+
return [];
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
const imageArrays = await Promise.all(imagePromises);
|
|
863
|
+
const allImages = imageArrays.flat().filter((image) => image);
|
|
864
|
+
|
|
865
|
+
return allImages;
|
|
866
|
+
} catch (error) {
|
|
867
|
+
console.error(chalk.red("❌ Error fetching ECR images:", error));
|
|
868
|
+
|
|
869
|
+
// Handle expired token specifically
|
|
870
|
+
if (error.code === "ExpiredTokenException") {
|
|
871
|
+
throw new Error(
|
|
872
|
+
"AWS session has expired. Please reconnect your ECR registry with fresh credentials."
|
|
873
|
+
);
|
|
874
|
+
} else if (
|
|
875
|
+
error.code === "UnauthorizedOperation" ||
|
|
876
|
+
error.code === "AccessDenied"
|
|
877
|
+
) {
|
|
878
|
+
throw new Error("Insufficient permissions to access ECR repositories");
|
|
879
|
+
} else if (error.code === "InvalidUserID.NotFound") {
|
|
880
|
+
throw new Error("Invalid AWS credentials");
|
|
881
|
+
} else if (error.code === "TokenRefreshRequired") {
|
|
882
|
+
throw new Error(
|
|
883
|
+
"AWS credentials need to be refreshed. Please reconnect your ECR registry."
|
|
884
|
+
);
|
|
885
|
+
} else {
|
|
886
|
+
throw new Error(`ECR API error: ${error.message}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
//! TO BE ENHANCED IN THE FUTURE
|
|
892
|
+
// Helper function to fetch images from Google GCR
|
|
893
|
+
async function fetchGCRImages(registry) {
|
|
894
|
+
try {
|
|
895
|
+
const serviceAccountKey = JSON.parse(
|
|
896
|
+
registry.credentials.serviceAccountJson
|
|
897
|
+
);
|
|
898
|
+
const projectId = serviceAccountKey.project_id;
|
|
899
|
+
|
|
900
|
+
const auth = new GoogleAuth({
|
|
901
|
+
credentials: serviceAccountKey,
|
|
902
|
+
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
const authClient = await auth.getClient();
|
|
906
|
+
const accessToken = await authClient.getAccessToken();
|
|
907
|
+
|
|
908
|
+
const catalogUrl = `https://gcr.io/v2/${projectId}/tags/list`;
|
|
909
|
+
|
|
910
|
+
const catalogResponse = await axios.get(catalogUrl, {
|
|
911
|
+
headers: {
|
|
912
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
913
|
+
Accept: "application/json",
|
|
914
|
+
},
|
|
915
|
+
timeout: 15000,
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
const repositories = catalogResponse.data.child || [];
|
|
919
|
+
|
|
920
|
+
if (repositories.length === 0) {
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const imagePromises = repositories.slice(0, 20).map(async (repoName) => {
|
|
925
|
+
try {
|
|
926
|
+
const tagsUrl = `https://gcr.io/v2/${projectId}/${repoName}/tags/list`;
|
|
927
|
+
|
|
928
|
+
const tagsResponse = await axios.get(tagsUrl, {
|
|
929
|
+
headers: {
|
|
930
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
931
|
+
Accept: "application/json",
|
|
932
|
+
},
|
|
933
|
+
timeout: 10000,
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
const tags = tagsResponse.data.tags || [];
|
|
937
|
+
|
|
938
|
+
return tags.slice(0, 5).map((tag) => ({
|
|
939
|
+
id: `gcr-${repoName}-${tag}`,
|
|
940
|
+
name: `gcr.io/${projectId}/${repoName}`,
|
|
941
|
+
tag: tag,
|
|
942
|
+
size: "Unknown",
|
|
943
|
+
created: "Unknown",
|
|
944
|
+
registry: registry.id,
|
|
945
|
+
isPrivate: true,
|
|
946
|
+
pullCount: 0,
|
|
947
|
+
starCount: 0,
|
|
948
|
+
architecture: "amd64",
|
|
949
|
+
os: "linux",
|
|
950
|
+
}));
|
|
951
|
+
} catch (tagError) {
|
|
952
|
+
console.error(
|
|
953
|
+
chalk.red(
|
|
954
|
+
`❌ Error fetching tags for repository ${repoName}:`,
|
|
955
|
+
tagError.message
|
|
956
|
+
)
|
|
957
|
+
);
|
|
958
|
+
return [];
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
const imageArrays = await Promise.all(imagePromises);
|
|
963
|
+
const allImages = imageArrays.flat().filter((image) => image);
|
|
964
|
+
|
|
965
|
+
return allImages;
|
|
966
|
+
} catch (error) {
|
|
967
|
+
console.error(chalk.red("❌ Error fetching GCR images:", error));
|
|
968
|
+
|
|
969
|
+
if (error.response?.status === 401) {
|
|
970
|
+
throw new Error("Invalid service account credentials");
|
|
971
|
+
} else if (error.response?.status === 403) {
|
|
972
|
+
throw new Error("Service account lacks required permissions");
|
|
973
|
+
} else {
|
|
974
|
+
throw new Error(`GCR API error: ${error.message}`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
//! TO BE ENHANCED IN THE FUTURE
|
|
980
|
+
// Helper function to fetch images from Azure ACR
|
|
981
|
+
async function fetchACRImages(registry) {
|
|
982
|
+
try {
|
|
983
|
+
const registryName = registry.url
|
|
984
|
+
.replace("https://", "")
|
|
985
|
+
.replace(".azurecr.io", "");
|
|
986
|
+
|
|
987
|
+
const catalogUrl = `${registry.url}/v2/_catalog`;
|
|
988
|
+
|
|
989
|
+
const catalogResponse = await axios.get(catalogUrl, {
|
|
990
|
+
headers: {
|
|
991
|
+
Authorization: `Basic ${Buffer.from(
|
|
992
|
+
`${registry.credentials.username}:${registry.credentials.password}`
|
|
993
|
+
).toString("base64")}`,
|
|
994
|
+
},
|
|
995
|
+
timeout: 15000,
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const repositories = catalogResponse.data.repositories || [];
|
|
999
|
+
|
|
1000
|
+
if (repositories.length === 0) {
|
|
1001
|
+
return [];
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const imagePromises = repositories.slice(0, 20).map(async (repoName) => {
|
|
1005
|
+
try {
|
|
1006
|
+
const tagsUrl = `${registry.url}/v2/${repoName}/tags/list`;
|
|
1007
|
+
|
|
1008
|
+
const tagsResponse = await axios.get(tagsUrl, {
|
|
1009
|
+
headers: {
|
|
1010
|
+
Authorization: `Basic ${Buffer.from(
|
|
1011
|
+
`${registry.credentials.username}:${registry.credentials.password}`
|
|
1012
|
+
).toString("base64")}`,
|
|
1013
|
+
},
|
|
1014
|
+
timeout: 10000,
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
const tags = tagsResponse.data.tags || [];
|
|
1018
|
+
|
|
1019
|
+
return tags.slice(0, 5).map((tag) => ({
|
|
1020
|
+
id: `acr-${repoName}-${tag}`,
|
|
1021
|
+
name: `${registryName}.azurecr.io/${repoName}`,
|
|
1022
|
+
tag: tag,
|
|
1023
|
+
size: "Unknown",
|
|
1024
|
+
created: "Unknown",
|
|
1025
|
+
registry: registry.id,
|
|
1026
|
+
isPrivate: true,
|
|
1027
|
+
pullCount: 0,
|
|
1028
|
+
starCount: 0,
|
|
1029
|
+
architecture: "amd64",
|
|
1030
|
+
os: "linux",
|
|
1031
|
+
}));
|
|
1032
|
+
} catch (tagError) {
|
|
1033
|
+
console.error(
|
|
1034
|
+
chalk.red(
|
|
1035
|
+
`❌ Error fetching tags for repository ${repoName}:`,
|
|
1036
|
+
tagError.message
|
|
1037
|
+
)
|
|
1038
|
+
);
|
|
1039
|
+
return [];
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
const imageArrays = await Promise.all(imagePromises);
|
|
1044
|
+
const allImages = imageArrays.flat().filter((image) => image);
|
|
1045
|
+
|
|
1046
|
+
return allImages;
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
console.error(chalk.red("❌ Error fetching ACR images:", error));
|
|
1049
|
+
|
|
1050
|
+
if (error.response?.status === 401) {
|
|
1051
|
+
throw new Error("Invalid username or password/token");
|
|
1052
|
+
} else if (error.response?.status === 404) {
|
|
1053
|
+
throw new Error("Registry not found");
|
|
1054
|
+
} else {
|
|
1055
|
+
throw new Error(`ACR API error: ${error.message}`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
//! TO BE ENHANCED IN THE FUTURE
|
|
1061
|
+
// Helper function to fetch images from custom registry
|
|
1062
|
+
async function fetchCustomRegistryImages(registry) {
|
|
1063
|
+
try {
|
|
1064
|
+
const apiUrl = `${registry.url}/v2/_catalog`;
|
|
1065
|
+
|
|
1066
|
+
const response = await fetch(apiUrl, {
|
|
1067
|
+
headers: {
|
|
1068
|
+
Authorization: `Basic ${Buffer.from(
|
|
1069
|
+
`${registry.credentials.username}:${registry.credentials.password}`
|
|
1070
|
+
).toString("base64")}`,
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
if (!response.ok) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`Registry API error: ${response.status} ${response.statusText}`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const data = await response.json();
|
|
1081
|
+
|
|
1082
|
+
const repositories = data.repositories || [];
|
|
1083
|
+
|
|
1084
|
+
return repositories.map((repo, index) => ({
|
|
1085
|
+
id: `custom-${index}-${repo}`,
|
|
1086
|
+
name: repo,
|
|
1087
|
+
tag: "latest",
|
|
1088
|
+
size: "Unknown",
|
|
1089
|
+
created: "Unknown",
|
|
1090
|
+
registry: registry.id,
|
|
1091
|
+
}));
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
console.error("Error fetching custom registry images:", error);
|
|
1094
|
+
throw error;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export default { handleRegistryAction };
|
|
1099
|
+
|
|
1100
|
+
export { handleRegistryAction };
|