@aigne/afs-gce 1.11.0-beta.6 → 1.11.0-beta.7
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/dist/index.d.mts +144 -32
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +609 -718
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -1,273 +1,9 @@
|
|
|
1
|
+
import { AFSBaseProvider, AFSNotFoundError, AFSReadonlyError, Actions, Delete, Explain, List, Meta, Read, Stat, Write } from "@aigne/afs";
|
|
2
|
+
import { camelize, optionalize, zodParse } from "@aigne/afs/utils/zod";
|
|
1
3
|
import { InstancesClient } from "@google-cloud/compute";
|
|
2
|
-
import {
|
|
4
|
+
import { joinURL } from "ufo";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
|
|
5
|
-
//#region src/errors.ts
|
|
6
|
-
/**
|
|
7
|
-
* GCE Provider Error Handling
|
|
8
|
-
*
|
|
9
|
-
* Maps GCE SDK errors to AFS error types.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* AFS error codes
|
|
13
|
-
*/
|
|
14
|
-
const AFSErrorCode = {
|
|
15
|
-
ENTRY_NOT_FOUND: "ENTRY_NOT_FOUND",
|
|
16
|
-
MODULE_NOT_FOUND: "MODULE_NOT_FOUND",
|
|
17
|
-
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
18
|
-
AUTH_ERROR: "AUTH_ERROR",
|
|
19
|
-
RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
|
|
20
|
-
INVALID_OPERATION: "INVALID_OPERATION",
|
|
21
|
-
ALREADY_EXISTS: "ALREADY_EXISTS",
|
|
22
|
-
SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
|
|
23
|
-
UNKNOWN: "UNKNOWN"
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* AFS Error class
|
|
27
|
-
*/
|
|
28
|
-
var AFSError = class extends Error {
|
|
29
|
-
code;
|
|
30
|
-
retryAfter;
|
|
31
|
-
constructor(code, message, options) {
|
|
32
|
-
super(message);
|
|
33
|
-
this.name = "AFSError";
|
|
34
|
-
this.code = code;
|
|
35
|
-
this.retryAfter = options?.retryAfter;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Map GCE HTTP status codes to AFS error codes
|
|
40
|
-
*/
|
|
41
|
-
const STATUS_TO_AFS_ERROR = {
|
|
42
|
-
400: AFSErrorCode.INVALID_OPERATION,
|
|
43
|
-
401: AFSErrorCode.AUTH_ERROR,
|
|
44
|
-
403: AFSErrorCode.PERMISSION_DENIED,
|
|
45
|
-
404: AFSErrorCode.ENTRY_NOT_FOUND,
|
|
46
|
-
409: AFSErrorCode.ALREADY_EXISTS,
|
|
47
|
-
429: AFSErrorCode.RATE_LIMIT_EXCEEDED,
|
|
48
|
-
503: AFSErrorCode.SERVICE_UNAVAILABLE
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Map GCE error to AFS error
|
|
52
|
-
*
|
|
53
|
-
* @param error - GCE SDK error or generic error
|
|
54
|
-
* @returns AFSError with appropriate error code
|
|
55
|
-
*/
|
|
56
|
-
function mapGCEError(error) {
|
|
57
|
-
if (typeof error === "object" && error !== null && "code" in error) {
|
|
58
|
-
const err = error;
|
|
59
|
-
const statusCode = typeof err.code === "number" ? err.code : 500;
|
|
60
|
-
const message = err.message ?? "Unknown GCE error";
|
|
61
|
-
return new AFSError(STATUS_TO_AFS_ERROR[statusCode] ?? AFSErrorCode.UNKNOWN, message);
|
|
62
|
-
}
|
|
63
|
-
if (error instanceof Error) return new AFSError(AFSErrorCode.UNKNOWN, error.message);
|
|
64
|
-
return new AFSError(AFSErrorCode.UNKNOWN, String(error));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region src/path-resolver.ts
|
|
69
|
-
/**
|
|
70
|
-
* Parse an AFS path into GCE resource identifiers
|
|
71
|
-
*
|
|
72
|
-
* Path structure:
|
|
73
|
-
* / - root
|
|
74
|
-
* /instances - list all instances
|
|
75
|
-
* /instances/{name} - specific instance
|
|
76
|
-
* /instances/{name}/metadata.json - instance metadata
|
|
77
|
-
* /instances/{name}/actions - list available actions
|
|
78
|
-
* /instances/{name}/actions/{action} - specific action
|
|
79
|
-
* /actions - global actions list
|
|
80
|
-
* /actions/{action} - global action
|
|
81
|
-
*
|
|
82
|
-
* @param path - AFS path
|
|
83
|
-
* @returns Parsed path with type and identifiers
|
|
84
|
-
*/
|
|
85
|
-
function parsePath(path) {
|
|
86
|
-
const segments = path.replace(/^\/+/, "").replace(/\/+$/, "").split("/").filter(Boolean);
|
|
87
|
-
if (segments.length === 0) return {
|
|
88
|
-
type: "root",
|
|
89
|
-
raw: path
|
|
90
|
-
};
|
|
91
|
-
const [first, second, third, fourth] = segments;
|
|
92
|
-
if (first === "instances") {
|
|
93
|
-
if (!second) return {
|
|
94
|
-
type: "instances",
|
|
95
|
-
raw: path
|
|
96
|
-
};
|
|
97
|
-
if (!third) return {
|
|
98
|
-
type: "instance",
|
|
99
|
-
instanceName: second,
|
|
100
|
-
raw: path
|
|
101
|
-
};
|
|
102
|
-
if (third === "metadata.json") return {
|
|
103
|
-
type: "instance-metadata",
|
|
104
|
-
instanceName: second,
|
|
105
|
-
fileName: third,
|
|
106
|
-
raw: path
|
|
107
|
-
};
|
|
108
|
-
if (third === "actions") {
|
|
109
|
-
if (!fourth) return {
|
|
110
|
-
type: "instance-actions",
|
|
111
|
-
instanceName: second,
|
|
112
|
-
raw: path
|
|
113
|
-
};
|
|
114
|
-
return {
|
|
115
|
-
type: "instance-action",
|
|
116
|
-
instanceName: second,
|
|
117
|
-
action: fourth,
|
|
118
|
-
raw: path
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
return {
|
|
122
|
-
type: "unknown",
|
|
123
|
-
raw: path
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
if (first === "actions") {
|
|
127
|
-
if (!second) return {
|
|
128
|
-
type: "actions",
|
|
129
|
-
raw: path
|
|
130
|
-
};
|
|
131
|
-
return {
|
|
132
|
-
type: "global-action",
|
|
133
|
-
action: second,
|
|
134
|
-
raw: path
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
type: "unknown",
|
|
139
|
-
raw: path
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Build an AFS path from components
|
|
144
|
-
*/
|
|
145
|
-
function buildPath(...segments) {
|
|
146
|
-
return `/${segments.filter(Boolean).join("/")}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
//#endregion
|
|
150
|
-
//#region src/operations/actions.ts
|
|
151
|
-
/**
|
|
152
|
-
* Execute an action on a GCE resource
|
|
153
|
-
*
|
|
154
|
-
* @param client - GCE InstancesClient
|
|
155
|
-
* @param projectId - GCP project ID
|
|
156
|
-
* @param zone - GCE zone
|
|
157
|
-
* @param path - AFS path to the action
|
|
158
|
-
* @returns Action result
|
|
159
|
-
*/
|
|
160
|
-
async function executeAction(client, projectId, zone, path) {
|
|
161
|
-
try {
|
|
162
|
-
const parsed = parsePath(path);
|
|
163
|
-
switch (parsed.type) {
|
|
164
|
-
case "instance-action": return executeInstanceAction(client, projectId, zone, parsed.instanceName, parsed.action);
|
|
165
|
-
case "global-action": return executeGlobalAction(parsed.action);
|
|
166
|
-
default: throw new Error(`Cannot execute action on path: ${path}`);
|
|
167
|
-
}
|
|
168
|
-
} catch (error) {
|
|
169
|
-
throw mapGCEError(error);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Execute an action on a specific instance
|
|
174
|
-
*/
|
|
175
|
-
async function executeInstanceAction(client, projectId, zone, instanceName, action) {
|
|
176
|
-
const [instance] = await client.get({
|
|
177
|
-
project: projectId,
|
|
178
|
-
zone,
|
|
179
|
-
instance: instanceName
|
|
180
|
-
});
|
|
181
|
-
const status = instance.status;
|
|
182
|
-
switch (action) {
|
|
183
|
-
case "start": return startInstance(client, projectId, zone, instanceName, status);
|
|
184
|
-
case "stop": return stopInstance(client, projectId, zone, instanceName, status);
|
|
185
|
-
case "reset": return resetInstance(client, projectId, zone, instanceName, status);
|
|
186
|
-
default: return {
|
|
187
|
-
success: false,
|
|
188
|
-
action,
|
|
189
|
-
error: `Unknown action: ${action}`
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Start an instance
|
|
195
|
-
*/
|
|
196
|
-
async function startInstance(client, projectId, zone, instanceName, currentStatus) {
|
|
197
|
-
if (currentStatus !== "STOPPED" && currentStatus !== "TERMINATED") return {
|
|
198
|
-
success: false,
|
|
199
|
-
action: "start",
|
|
200
|
-
error: `Cannot start instance in ${currentStatus} state. Instance must be STOPPED or TERMINATED.`
|
|
201
|
-
};
|
|
202
|
-
const [operation] = await client.start({
|
|
203
|
-
project: projectId,
|
|
204
|
-
zone,
|
|
205
|
-
instance: instanceName
|
|
206
|
-
});
|
|
207
|
-
return {
|
|
208
|
-
success: true,
|
|
209
|
-
action: "start",
|
|
210
|
-
operationName: operation.latestResponse?.name || "unknown"
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Stop an instance
|
|
215
|
-
*/
|
|
216
|
-
async function stopInstance(client, projectId, zone, instanceName, currentStatus) {
|
|
217
|
-
if (currentStatus !== "RUNNING") return {
|
|
218
|
-
success: false,
|
|
219
|
-
action: "stop",
|
|
220
|
-
error: `Cannot stop instance in ${currentStatus} state. Instance must be RUNNING.`
|
|
221
|
-
};
|
|
222
|
-
const [operation] = await client.stop({
|
|
223
|
-
project: projectId,
|
|
224
|
-
zone,
|
|
225
|
-
instance: instanceName
|
|
226
|
-
});
|
|
227
|
-
return {
|
|
228
|
-
success: true,
|
|
229
|
-
action: "stop",
|
|
230
|
-
operationName: operation.latestResponse?.name || "unknown"
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Reset an instance
|
|
235
|
-
*/
|
|
236
|
-
async function resetInstance(client, projectId, zone, instanceName, currentStatus) {
|
|
237
|
-
if (currentStatus !== "RUNNING") return {
|
|
238
|
-
success: false,
|
|
239
|
-
action: "reset",
|
|
240
|
-
error: `Cannot reset instance in ${currentStatus} state. Instance must be RUNNING.`
|
|
241
|
-
};
|
|
242
|
-
const [operation] = await client.reset({
|
|
243
|
-
project: projectId,
|
|
244
|
-
zone,
|
|
245
|
-
instance: instanceName
|
|
246
|
-
});
|
|
247
|
-
return {
|
|
248
|
-
success: true,
|
|
249
|
-
action: "reset",
|
|
250
|
-
operationName: operation.latestResponse?.name || "unknown"
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Execute a global action
|
|
255
|
-
*/
|
|
256
|
-
function executeGlobalAction(action) {
|
|
257
|
-
switch (action) {
|
|
258
|
-
case "refresh": return {
|
|
259
|
-
success: true,
|
|
260
|
-
action: "refresh"
|
|
261
|
-
};
|
|
262
|
-
default: return {
|
|
263
|
-
success: false,
|
|
264
|
-
action,
|
|
265
|
-
error: `Unknown global action: ${action}`
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
//#endregion
|
|
271
7
|
//#region src/platform-ref.ts
|
|
272
8
|
/**
|
|
273
9
|
* Generate platform reference with GCP Console URL for an instance
|
|
@@ -317,6 +53,19 @@ const afsgceOptionsSchema = camelize(z.object({
|
|
|
317
53
|
cacheTtl: optionalize(z.number().int().min(0))
|
|
318
54
|
}).strict());
|
|
319
55
|
/**
|
|
56
|
+
* GCE instance states
|
|
57
|
+
*/
|
|
58
|
+
const GCE_INSTANCE_STATES = [
|
|
59
|
+
"PROVISIONING",
|
|
60
|
+
"STAGING",
|
|
61
|
+
"RUNNING",
|
|
62
|
+
"STOPPING",
|
|
63
|
+
"STOPPED",
|
|
64
|
+
"SUSPENDING",
|
|
65
|
+
"SUSPENDED",
|
|
66
|
+
"TERMINATED"
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
320
69
|
* Kind constants for GCE resources
|
|
321
70
|
*/
|
|
322
71
|
const KINDS = {
|
|
@@ -328,433 +77,58 @@ const KINDS = {
|
|
|
328
77
|
};
|
|
329
78
|
|
|
330
79
|
//#endregion
|
|
331
|
-
//#region
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
* @param zone - GCE zone
|
|
338
|
-
* @param path - AFS path
|
|
339
|
-
* @param options - List options
|
|
340
|
-
* @returns List result with entries
|
|
341
|
-
*/
|
|
342
|
-
async function list(client, projectId, zone, path, options) {
|
|
343
|
-
try {
|
|
344
|
-
const parsed = parsePath(path);
|
|
345
|
-
switch (parsed.type) {
|
|
346
|
-
case "root": return listRoot(projectId);
|
|
347
|
-
case "instances": return listInstances(client, projectId, zone, options);
|
|
348
|
-
case "instance": return listInstanceChildren(client, projectId, zone, parsed.instanceName);
|
|
349
|
-
case "instance-actions": return listInstanceActions(client, projectId, zone, parsed.instanceName);
|
|
350
|
-
case "actions": return listGlobalActions(projectId);
|
|
351
|
-
default: return { data: [] };
|
|
352
|
-
}
|
|
353
|
-
} catch (error) {
|
|
354
|
-
throw mapGCEError(error);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* List root directory
|
|
359
|
-
*/
|
|
360
|
-
function listRoot(projectId) {
|
|
361
|
-
return { data: [{
|
|
362
|
-
id: `gce://${projectId}/instances`,
|
|
363
|
-
path: "/instances",
|
|
364
|
-
metadata: {
|
|
365
|
-
kind: KINDS.NODE,
|
|
366
|
-
kinds: [KINDS.NODE],
|
|
367
|
-
childrenCount: 0,
|
|
368
|
-
description: "GCE Instances",
|
|
369
|
-
platformRef: generateInstancesListPlatformRef(projectId)
|
|
370
|
-
}
|
|
371
|
-
}, {
|
|
372
|
-
id: `gce://${projectId}/actions`,
|
|
373
|
-
path: "/actions",
|
|
374
|
-
metadata: {
|
|
375
|
-
kind: KINDS.NODE,
|
|
376
|
-
kinds: [KINDS.NODE],
|
|
377
|
-
childrenCount: 0,
|
|
378
|
-
description: "Global Actions"
|
|
379
|
-
}
|
|
380
|
-
}] };
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* List all instances
|
|
384
|
-
*/
|
|
385
|
-
async function listInstances(client, projectId, zone, options) {
|
|
386
|
-
const maxChildren = options?.maxChildren ?? 1e3;
|
|
387
|
-
const [instances] = await client.list({
|
|
388
|
-
project: projectId,
|
|
389
|
-
zone,
|
|
390
|
-
maxResults: maxChildren
|
|
391
|
-
});
|
|
392
|
-
return { data: (instances || []).map((instance) => {
|
|
393
|
-
const name = instance.name;
|
|
394
|
-
const machineType = instance.machineType?.split("/").pop() || "unknown";
|
|
395
|
-
return {
|
|
396
|
-
id: `gce://${projectId}/${zone}/${name}`,
|
|
397
|
-
path: buildPath("instances", name),
|
|
398
|
-
metadata: {
|
|
399
|
-
kind: KINDS.INSTANCE,
|
|
400
|
-
kinds: [
|
|
401
|
-
KINDS.INSTANCE,
|
|
402
|
-
KINDS.GCP_RESOURCE,
|
|
403
|
-
KINDS.NODE
|
|
404
|
-
],
|
|
405
|
-
childrenCount: 2,
|
|
406
|
-
status: instance.status,
|
|
407
|
-
machineType,
|
|
408
|
-
privateIp: instance.networkInterfaces?.[0]?.networkIP,
|
|
409
|
-
publicIp: instance.networkInterfaces?.[0]?.accessConfigs?.[0]?.natIP,
|
|
410
|
-
createdAt: instance.creationTimestamp,
|
|
411
|
-
labels: instance.labels,
|
|
412
|
-
platformRef: generateInstancePlatformRef(projectId, zone, name)
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
}) };
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* List instance children (metadata.json, actions)
|
|
419
|
-
*/
|
|
420
|
-
async function listInstanceChildren(client, projectId, zone, instanceName) {
|
|
421
|
-
await client.get({
|
|
422
|
-
project: projectId,
|
|
423
|
-
zone,
|
|
424
|
-
instance: instanceName
|
|
425
|
-
});
|
|
426
|
-
return { data: [{
|
|
427
|
-
id: `gce://${projectId}/${zone}/${instanceName}/metadata.json`,
|
|
428
|
-
path: buildPath("instances", instanceName, "metadata.json"),
|
|
429
|
-
metadata: {
|
|
430
|
-
kind: KINDS.NODE,
|
|
431
|
-
kinds: [KINDS.NODE],
|
|
432
|
-
description: "Instance metadata"
|
|
433
|
-
}
|
|
434
|
-
}, {
|
|
435
|
-
id: `gce://${projectId}/${zone}/${instanceName}/actions`,
|
|
436
|
-
path: buildPath("instances", instanceName, "actions"),
|
|
437
|
-
metadata: {
|
|
438
|
-
kind: KINDS.NODE,
|
|
439
|
-
kinds: [KINDS.NODE],
|
|
440
|
-
childrenCount: 0,
|
|
441
|
-
description: "Instance actions"
|
|
442
|
-
}
|
|
443
|
-
}] };
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* List available actions for an instance
|
|
447
|
-
*/
|
|
448
|
-
async function listInstanceActions(client, projectId, zone, instanceName) {
|
|
449
|
-
const [instance] = await client.get({
|
|
450
|
-
project: projectId,
|
|
451
|
-
zone,
|
|
452
|
-
instance: instanceName
|
|
453
|
-
});
|
|
454
|
-
const status = instance.status;
|
|
455
|
-
const actions = [];
|
|
456
|
-
switch (status) {
|
|
457
|
-
case "RUNNING":
|
|
458
|
-
actions.push("stop", "reset");
|
|
459
|
-
break;
|
|
460
|
-
case "STOPPED":
|
|
461
|
-
case "TERMINATED":
|
|
462
|
-
actions.push("start");
|
|
463
|
-
break;
|
|
464
|
-
case "SUSPENDED":
|
|
465
|
-
actions.push("resume");
|
|
466
|
-
break;
|
|
467
|
-
default: break;
|
|
468
|
-
}
|
|
469
|
-
return { data: actions.map((action) => ({
|
|
470
|
-
id: `gce://${projectId}/${zone}/${instanceName}/actions/${action}`,
|
|
471
|
-
path: buildPath("instances", instanceName, "actions", action),
|
|
472
|
-
metadata: {
|
|
473
|
-
kind: KINDS.ACTION,
|
|
474
|
-
kinds: [
|
|
475
|
-
KINDS.ACTION,
|
|
476
|
-
KINDS.EXECUTABLE,
|
|
477
|
-
KINDS.NODE
|
|
478
|
-
],
|
|
479
|
-
description: `${action.charAt(0).toUpperCase() + action.slice(1)} the instance`
|
|
480
|
-
}
|
|
481
|
-
})) };
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* List global actions
|
|
485
|
-
*/
|
|
486
|
-
function listGlobalActions(projectId) {
|
|
487
|
-
return { data: [{
|
|
488
|
-
id: `gce://${projectId}/actions/refresh`,
|
|
489
|
-
path: "/actions/refresh",
|
|
490
|
-
metadata: {
|
|
491
|
-
kind: KINDS.ACTION,
|
|
492
|
-
kinds: [
|
|
493
|
-
KINDS.ACTION,
|
|
494
|
-
KINDS.EXECUTABLE,
|
|
495
|
-
KINDS.NODE
|
|
496
|
-
],
|
|
497
|
-
description: "Refresh instance cache"
|
|
498
|
-
}
|
|
499
|
-
}] };
|
|
80
|
+
//#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
|
|
81
|
+
function __decorate(decorators, target, key, desc) {
|
|
82
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
83
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
84
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
85
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
500
86
|
}
|
|
501
87
|
|
|
502
88
|
//#endregion
|
|
503
|
-
//#region src/
|
|
89
|
+
//#region src/index.ts
|
|
504
90
|
/**
|
|
505
|
-
*
|
|
91
|
+
* AFS GCE Provider
|
|
506
92
|
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* @param zone - GCE zone
|
|
510
|
-
* @param path - AFS path
|
|
511
|
-
* @param options - Read options
|
|
512
|
-
* @returns Read result with content
|
|
513
|
-
*/
|
|
514
|
-
async function read(client, projectId, zone, path, _options) {
|
|
515
|
-
try {
|
|
516
|
-
const parsed = parsePath(path);
|
|
517
|
-
switch (parsed.type) {
|
|
518
|
-
case "instance-metadata": return readInstanceMetadata(client, projectId, zone, parsed.instanceName);
|
|
519
|
-
default: throw new Error(`Cannot read path: ${path}`);
|
|
520
|
-
}
|
|
521
|
-
} catch (error) {
|
|
522
|
-
throw mapGCEError(error);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Read instance metadata as JSON
|
|
93
|
+
* GCE provider using AFSBaseProvider decorator routing pattern.
|
|
94
|
+
* Provides file-system-like access to Google Compute Engine instances.
|
|
527
95
|
*/
|
|
528
|
-
async function readInstanceMetadata(client, projectId, zone, instanceName) {
|
|
529
|
-
const [instance] = await client.get({
|
|
530
|
-
project: projectId,
|
|
531
|
-
zone,
|
|
532
|
-
instance: instanceName
|
|
533
|
-
});
|
|
534
|
-
return { data: {
|
|
535
|
-
id: `gce://${projectId}/${zone}/${instanceName}/metadata.json`,
|
|
536
|
-
path: buildPath("instances", instanceName, "metadata.json"),
|
|
537
|
-
metadata: {
|
|
538
|
-
kind: KINDS.NODE,
|
|
539
|
-
kinds: [KINDS.NODE],
|
|
540
|
-
description: "Instance metadata"
|
|
541
|
-
},
|
|
542
|
-
content: JSON.stringify(instance, null, 2)
|
|
543
|
-
} };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
//#endregion
|
|
547
|
-
//#region src/operations/stat.ts
|
|
548
96
|
/**
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
* @param client - GCE InstancesClient
|
|
552
|
-
* @param projectId - GCP project ID
|
|
553
|
-
* @param zone - GCE zone
|
|
554
|
-
* @param path - AFS path
|
|
555
|
-
* @returns Entry with metadata
|
|
97
|
+
* Map GCE SDK errors to appropriate AFS errors
|
|
556
98
|
*/
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
case "instances": return statInstances(projectId);
|
|
563
|
-
case "instance": return statInstance(client, projectId, zone, parsed.instanceName);
|
|
564
|
-
case "instance-metadata": return statInstanceMetadata(client, projectId, zone, parsed.instanceName);
|
|
565
|
-
case "instance-actions": return statInstanceActions(client, projectId, zone, parsed.instanceName);
|
|
566
|
-
case "instance-action": return statInstanceAction(client, projectId, zone, parsed.instanceName, parsed.action);
|
|
567
|
-
case "actions": return statGlobalActions(projectId);
|
|
568
|
-
case "global-action": return statGlobalAction(projectId, parsed.action);
|
|
569
|
-
default: throw new Error(`Path not found: ${path}`);
|
|
570
|
-
}
|
|
571
|
-
} catch (error) {
|
|
572
|
-
throw mapGCEError(error);
|
|
99
|
+
function mapGCEError(error, path) {
|
|
100
|
+
if (error instanceof AFSNotFoundError || error instanceof AFSReadonlyError) return error;
|
|
101
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
102
|
+
const err = error;
|
|
103
|
+
if (err.code === 404) return new AFSNotFoundError(path ?? "/", err.message);
|
|
573
104
|
}
|
|
105
|
+
if (error instanceof Error) return error;
|
|
106
|
+
return new Error(String(error));
|
|
574
107
|
}
|
|
575
108
|
/**
|
|
576
|
-
*
|
|
577
|
-
*/
|
|
578
|
-
function statRoot(projectId) {
|
|
579
|
-
return {
|
|
580
|
-
id: `gce://${projectId}/`,
|
|
581
|
-
path: "/",
|
|
582
|
-
metadata: {
|
|
583
|
-
kind: KINDS.NODE,
|
|
584
|
-
kinds: [KINDS.NODE],
|
|
585
|
-
childrenCount: 2,
|
|
586
|
-
description: "GCE Root"
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Stat instances directory
|
|
592
|
-
*/
|
|
593
|
-
function statInstances(projectId) {
|
|
594
|
-
return {
|
|
595
|
-
id: `gce://${projectId}/instances`,
|
|
596
|
-
path: "/instances",
|
|
597
|
-
metadata: {
|
|
598
|
-
kind: KINDS.NODE,
|
|
599
|
-
kinds: [KINDS.NODE],
|
|
600
|
-
childrenCount: 0,
|
|
601
|
-
description: "GCE Instances",
|
|
602
|
-
platformRef: generateInstancesListPlatformRef(projectId)
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* Stat a specific instance
|
|
608
|
-
*/
|
|
609
|
-
async function statInstance(client, projectId, zone, instanceName) {
|
|
610
|
-
const [instance] = await client.get({
|
|
611
|
-
project: projectId,
|
|
612
|
-
zone,
|
|
613
|
-
instance: instanceName
|
|
614
|
-
});
|
|
615
|
-
const machineType = instance.machineType?.split("/").pop() || "unknown";
|
|
616
|
-
return {
|
|
617
|
-
id: `gce://${projectId}/${zone}/${instanceName}`,
|
|
618
|
-
path: buildPath("instances", instanceName),
|
|
619
|
-
metadata: {
|
|
620
|
-
kind: KINDS.INSTANCE,
|
|
621
|
-
kinds: [
|
|
622
|
-
KINDS.INSTANCE,
|
|
623
|
-
KINDS.GCP_RESOURCE,
|
|
624
|
-
KINDS.NODE
|
|
625
|
-
],
|
|
626
|
-
childrenCount: 2,
|
|
627
|
-
status: instance.status,
|
|
628
|
-
machineType,
|
|
629
|
-
privateIp: instance.networkInterfaces?.[0]?.networkIP,
|
|
630
|
-
publicIp: instance.networkInterfaces?.[0]?.accessConfigs?.[0]?.natIP,
|
|
631
|
-
createdAt: instance.creationTimestamp,
|
|
632
|
-
labels: instance.labels,
|
|
633
|
-
platformRef: generateInstancePlatformRef(projectId, zone, instanceName)
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Stat instance metadata.json file
|
|
639
|
-
*/
|
|
640
|
-
async function statInstanceMetadata(client, projectId, zone, instanceName) {
|
|
641
|
-
await client.get({
|
|
642
|
-
project: projectId,
|
|
643
|
-
zone,
|
|
644
|
-
instance: instanceName
|
|
645
|
-
});
|
|
646
|
-
return {
|
|
647
|
-
id: `gce://${projectId}/${zone}/${instanceName}/metadata.json`,
|
|
648
|
-
path: buildPath("instances", instanceName, "metadata.json"),
|
|
649
|
-
metadata: {
|
|
650
|
-
kind: KINDS.NODE,
|
|
651
|
-
kinds: [KINDS.NODE],
|
|
652
|
-
description: "Instance metadata"
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* Stat instance actions directory
|
|
658
|
-
*/
|
|
659
|
-
async function statInstanceActions(client, projectId, zone, instanceName) {
|
|
660
|
-
await client.get({
|
|
661
|
-
project: projectId,
|
|
662
|
-
zone,
|
|
663
|
-
instance: instanceName
|
|
664
|
-
});
|
|
665
|
-
return {
|
|
666
|
-
id: `gce://${projectId}/${zone}/${instanceName}/actions`,
|
|
667
|
-
path: buildPath("instances", instanceName, "actions"),
|
|
668
|
-
metadata: {
|
|
669
|
-
kind: KINDS.NODE,
|
|
670
|
-
kinds: [KINDS.NODE],
|
|
671
|
-
childrenCount: 0,
|
|
672
|
-
description: "Instance actions"
|
|
673
|
-
}
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Stat a specific instance action
|
|
678
|
-
*/
|
|
679
|
-
async function statInstanceAction(client, projectId, zone, instanceName, action) {
|
|
680
|
-
await client.get({
|
|
681
|
-
project: projectId,
|
|
682
|
-
zone,
|
|
683
|
-
instance: instanceName
|
|
684
|
-
});
|
|
685
|
-
return {
|
|
686
|
-
id: `gce://${projectId}/${zone}/${instanceName}/actions/${action}`,
|
|
687
|
-
path: buildPath("instances", instanceName, "actions", action),
|
|
688
|
-
metadata: {
|
|
689
|
-
kind: KINDS.ACTION,
|
|
690
|
-
kinds: [
|
|
691
|
-
KINDS.ACTION,
|
|
692
|
-
KINDS.EXECUTABLE,
|
|
693
|
-
KINDS.NODE
|
|
694
|
-
],
|
|
695
|
-
description: `${action.charAt(0).toUpperCase() + action.slice(1)} the instance`
|
|
696
|
-
}
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* Stat global actions directory
|
|
701
|
-
*/
|
|
702
|
-
function statGlobalActions(projectId) {
|
|
703
|
-
return {
|
|
704
|
-
id: `gce://${projectId}/actions`,
|
|
705
|
-
path: "/actions",
|
|
706
|
-
metadata: {
|
|
707
|
-
kind: KINDS.NODE,
|
|
708
|
-
kinds: [KINDS.NODE],
|
|
709
|
-
childrenCount: 1,
|
|
710
|
-
description: "Global Actions"
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
/**
|
|
715
|
-
* Stat a specific global action
|
|
716
|
-
*/
|
|
717
|
-
function statGlobalAction(projectId, action) {
|
|
718
|
-
return {
|
|
719
|
-
id: `gce://${projectId}/actions/${action}`,
|
|
720
|
-
path: buildPath("actions", action),
|
|
721
|
-
metadata: {
|
|
722
|
-
kind: KINDS.ACTION,
|
|
723
|
-
kinds: [
|
|
724
|
-
KINDS.ACTION,
|
|
725
|
-
KINDS.EXECUTABLE,
|
|
726
|
-
KINDS.NODE
|
|
727
|
-
],
|
|
728
|
-
description: `${action.charAt(0).toUpperCase() + action.slice(1)} action`
|
|
729
|
-
}
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
//#endregion
|
|
734
|
-
//#region src/index.ts
|
|
735
|
-
/**
|
|
736
|
-
* AFS GCE Provider
|
|
109
|
+
* AFSGCE Provider using AFSBaseProvider pattern
|
|
737
110
|
*
|
|
738
|
-
* Provides
|
|
111
|
+
* Provides file-system-like access to GCE instances.
|
|
112
|
+
* Uses decorator routing (@List, @Read, @Write, @Delete, @Meta, @Actions, @Explain).
|
|
739
113
|
*
|
|
740
114
|
* @example
|
|
741
115
|
* ```typescript
|
|
742
|
-
* const
|
|
743
|
-
* projectId:
|
|
744
|
-
* zone:
|
|
116
|
+
* const gce = new AFSGCE({
|
|
117
|
+
* projectId: "my-project",
|
|
118
|
+
* zone: "us-central1-a",
|
|
745
119
|
* });
|
|
746
120
|
*
|
|
747
|
-
* //
|
|
748
|
-
*
|
|
121
|
+
* // Mount to AFS
|
|
122
|
+
* afs.mount(gce);
|
|
749
123
|
*
|
|
750
|
-
* //
|
|
751
|
-
* const
|
|
124
|
+
* // List instances
|
|
125
|
+
* const result = await afs.list("/modules/gce/instances");
|
|
752
126
|
*
|
|
753
|
-
* //
|
|
754
|
-
* await
|
|
127
|
+
* // Read instance metadata
|
|
128
|
+
* const meta = await afs.read("/modules/gce/instances/my-vm/metadata.json");
|
|
755
129
|
* ```
|
|
756
130
|
*/
|
|
757
|
-
var AFSGCE = class {
|
|
131
|
+
var AFSGCE = class AFSGCE extends AFSBaseProvider {
|
|
758
132
|
name;
|
|
759
133
|
description;
|
|
760
134
|
accessMode;
|
|
@@ -762,6 +136,7 @@ var AFSGCE = class {
|
|
|
762
136
|
projectId;
|
|
763
137
|
zone;
|
|
764
138
|
constructor(options) {
|
|
139
|
+
super();
|
|
765
140
|
const validated = afsgceOptionsSchema.parse(options);
|
|
766
141
|
this.name = validated.name || "gce";
|
|
767
142
|
this.description = validated.description;
|
|
@@ -776,61 +151,577 @@ var AFSGCE = class {
|
|
|
776
151
|
} : void 0
|
|
777
152
|
});
|
|
778
153
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
async
|
|
783
|
-
return
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
154
|
+
static schema() {
|
|
155
|
+
return afsgceOptionsSchema;
|
|
156
|
+
}
|
|
157
|
+
static async load({ basePath, config } = {}) {
|
|
158
|
+
return new AFSGCE(zodParse(afsgceOptionsSchema, config, { prefix: basePath }));
|
|
159
|
+
}
|
|
160
|
+
async getInstance(instanceName) {
|
|
161
|
+
try {
|
|
162
|
+
const [instance] = await this.client.get({
|
|
163
|
+
project: this.projectId,
|
|
164
|
+
zone: this.zone,
|
|
165
|
+
instance: instanceName
|
|
166
|
+
});
|
|
167
|
+
return instance;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw mapGCEError(error, joinURL("/instances", instanceName));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async listAllInstances(maxResults) {
|
|
173
|
+
const [instances] = await this.client.list({
|
|
174
|
+
project: this.projectId,
|
|
175
|
+
zone: this.zone,
|
|
176
|
+
maxResults: maxResults ?? 1e3
|
|
177
|
+
});
|
|
178
|
+
return instances || [];
|
|
179
|
+
}
|
|
180
|
+
buildInstanceEntry(instance, basePath) {
|
|
181
|
+
const name = instance.name;
|
|
182
|
+
const machineType = instance.machineType?.split("/").pop() || "unknown";
|
|
183
|
+
return {
|
|
184
|
+
id: `gce://${this.projectId}/${this.zone}/${name}`,
|
|
185
|
+
path: joinURL(basePath, name),
|
|
186
|
+
meta: {
|
|
187
|
+
kind: KINDS.INSTANCE,
|
|
188
|
+
kinds: [
|
|
189
|
+
KINDS.INSTANCE,
|
|
190
|
+
KINDS.GCP_RESOURCE,
|
|
191
|
+
KINDS.NODE
|
|
192
|
+
],
|
|
193
|
+
childrenCount: -1,
|
|
194
|
+
status: instance.status,
|
|
195
|
+
machineType,
|
|
196
|
+
privateIp: instance.networkInterfaces?.[0]?.networkIP,
|
|
197
|
+
publicIp: instance.networkInterfaces?.[0]?.accessConfigs?.[0]?.natIP,
|
|
198
|
+
createdAt: instance.creationTimestamp,
|
|
199
|
+
labels: instance.labels,
|
|
200
|
+
platformRef: generateInstancePlatformRef(this.projectId, this.zone, name)
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async listRoot(_ctx) {
|
|
205
|
+
return { data: [{
|
|
206
|
+
id: `gce://${this.projectId}/instances`,
|
|
207
|
+
path: "/instances",
|
|
208
|
+
meta: {
|
|
209
|
+
kind: KINDS.NODE,
|
|
210
|
+
kinds: [KINDS.NODE],
|
|
211
|
+
childrenCount: -1,
|
|
212
|
+
description: "GCE Instances",
|
|
213
|
+
platformRef: generateInstancesListPlatformRef(this.projectId)
|
|
214
|
+
}
|
|
215
|
+
}, {
|
|
216
|
+
id: `gce://${this.projectId}/by-status`,
|
|
217
|
+
path: "/by-status",
|
|
218
|
+
meta: {
|
|
219
|
+
kind: KINDS.NODE,
|
|
220
|
+
kinds: [KINDS.NODE],
|
|
221
|
+
childrenCount: GCE_INSTANCE_STATES.length,
|
|
222
|
+
description: "Instances grouped by status"
|
|
223
|
+
}
|
|
224
|
+
}] };
|
|
225
|
+
}
|
|
226
|
+
async listInstances(_ctx) {
|
|
227
|
+
try {
|
|
228
|
+
return { data: (await this.listAllInstances()).map((instance) => this.buildInstanceEntry(instance, "/instances")) };
|
|
229
|
+
} catch (error) {
|
|
230
|
+
throw mapGCEError(error, "/instances");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async listInstanceChildren(ctx) {
|
|
234
|
+
await this.getInstance(ctx.params.instanceId);
|
|
235
|
+
return { data: [{
|
|
236
|
+
id: `gce://${this.projectId}/${this.zone}/${ctx.params.instanceId}/metadata.json`,
|
|
237
|
+
path: joinURL(ctx.path, "metadata.json"),
|
|
238
|
+
meta: {
|
|
239
|
+
kind: KINDS.NODE,
|
|
240
|
+
kinds: [KINDS.NODE],
|
|
241
|
+
description: "Instance metadata"
|
|
242
|
+
}
|
|
243
|
+
}, {
|
|
244
|
+
id: `gce://${this.projectId}/${this.zone}/${ctx.params.instanceId}/labels`,
|
|
245
|
+
path: joinURL(ctx.path, "labels"),
|
|
246
|
+
meta: {
|
|
247
|
+
kind: KINDS.NODE,
|
|
248
|
+
kinds: [KINDS.NODE],
|
|
249
|
+
childrenCount: -1,
|
|
250
|
+
description: "Instance labels"
|
|
251
|
+
}
|
|
252
|
+
}] };
|
|
253
|
+
}
|
|
254
|
+
async listInstanceLabels(ctx) {
|
|
255
|
+
const labels = (await this.getInstance(ctx.params.instanceId)).labels || {};
|
|
256
|
+
return { data: Object.entries(labels).map(([key, value]) => ({
|
|
257
|
+
id: `gce://${this.projectId}/${this.zone}/${ctx.params.instanceId}/labels/${key}`,
|
|
258
|
+
path: joinURL(ctx.path, encodeURIComponent(key)),
|
|
259
|
+
content: value,
|
|
260
|
+
meta: { kind: "gce:label" }
|
|
261
|
+
})) };
|
|
262
|
+
}
|
|
263
|
+
async listByStatus(_ctx) {
|
|
264
|
+
return { data: GCE_INSTANCE_STATES.map((status) => ({
|
|
265
|
+
id: `gce://${this.projectId}/by-status/${status.toLowerCase()}`,
|
|
266
|
+
path: joinURL("/by-status", status.toLowerCase()),
|
|
267
|
+
meta: {
|
|
268
|
+
kind: KINDS.NODE,
|
|
269
|
+
kinds: [KINDS.NODE],
|
|
270
|
+
childrenCount: -1,
|
|
271
|
+
description: `Instances in ${status} state`
|
|
272
|
+
}
|
|
273
|
+
})) };
|
|
274
|
+
}
|
|
275
|
+
async listByStatusFilter(ctx) {
|
|
276
|
+
try {
|
|
277
|
+
const statusUpper = ctx.params.status.toUpperCase();
|
|
278
|
+
return { data: (await this.listAllInstances()).filter((inst) => inst.status?.toUpperCase() === statusUpper).map((instance) => this.buildInstanceEntry(instance, ctx.path)) };
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw mapGCEError(error, ctx.path);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async readCapabilities(_ctx) {
|
|
284
|
+
const actionCatalogs = [];
|
|
285
|
+
actionCatalogs.push({
|
|
286
|
+
kind: "gce:instance",
|
|
287
|
+
description: "Instance lifecycle operations",
|
|
288
|
+
catalog: [
|
|
289
|
+
{
|
|
290
|
+
name: "start",
|
|
291
|
+
description: "Start a stopped or terminated instance",
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: "object",
|
|
294
|
+
properties: {}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: "stop",
|
|
299
|
+
description: "Stop a running instance",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: {}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "reset",
|
|
307
|
+
description: "Reset (hard reboot) a running instance",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
],
|
|
314
|
+
discovery: {
|
|
315
|
+
pathTemplate: "/instances/:instanceName/.actions",
|
|
316
|
+
note: "Replace :instanceName with actual instance name"
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
if (this.accessMode === "readwrite") actionCatalogs.push({
|
|
320
|
+
kind: "gce:global",
|
|
321
|
+
description: "Global GCE operations",
|
|
322
|
+
catalog: [{
|
|
323
|
+
name: "refresh",
|
|
324
|
+
description: "Refresh cached instance data",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: "object",
|
|
327
|
+
properties: {}
|
|
328
|
+
}
|
|
329
|
+
}],
|
|
330
|
+
discovery: {
|
|
331
|
+
pathTemplate: "/.actions",
|
|
332
|
+
note: "Available at provider root"
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
id: "/.meta/.capabilities",
|
|
337
|
+
path: "/.meta/.capabilities",
|
|
338
|
+
content: {
|
|
339
|
+
schemaVersion: 1,
|
|
340
|
+
provider: this.name,
|
|
341
|
+
version: "1.0.0",
|
|
342
|
+
description: this.description,
|
|
343
|
+
tools: [],
|
|
344
|
+
actions: actionCatalogs,
|
|
345
|
+
operations: this.getOperationsDeclaration()
|
|
346
|
+
},
|
|
347
|
+
meta: { kind: "afs:capabilities" }
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async readInstanceMetadata(ctx) {
|
|
351
|
+
try {
|
|
352
|
+
const instance = await this.getInstance(ctx.params.instanceId);
|
|
353
|
+
return {
|
|
354
|
+
id: `gce://${this.projectId}/${this.zone}/${ctx.params.instanceId}/metadata.json`,
|
|
355
|
+
path: ctx.path,
|
|
356
|
+
meta: {
|
|
357
|
+
kind: KINDS.NODE,
|
|
358
|
+
kinds: [KINDS.NODE],
|
|
359
|
+
description: "Instance metadata"
|
|
360
|
+
},
|
|
361
|
+
content: JSON.stringify(instance, null, 2)
|
|
362
|
+
};
|
|
363
|
+
} catch (error) {
|
|
364
|
+
throw mapGCEError(error, ctx.path);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async readInstanceLabel(ctx) {
|
|
368
|
+
const value = ((await this.getInstance(ctx.params.instanceId)).labels || {})[ctx.params.labelKey];
|
|
369
|
+
if (value === void 0) throw new AFSNotFoundError(ctx.path);
|
|
370
|
+
return {
|
|
371
|
+
id: `gce://${this.projectId}/${this.zone}/${ctx.params.instanceId}/labels/${ctx.params.labelKey}`,
|
|
372
|
+
path: ctx.path,
|
|
373
|
+
content: value,
|
|
374
|
+
meta: { kind: "gce:label" }
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async metaRoot(ctx) {
|
|
378
|
+
return {
|
|
379
|
+
id: `gce://${this.projectId}/`,
|
|
380
|
+
path: ctx.path,
|
|
381
|
+
meta: {
|
|
382
|
+
kind: KINDS.NODE,
|
|
383
|
+
kinds: [KINDS.NODE],
|
|
384
|
+
childrenCount: 2,
|
|
385
|
+
description: "GCE Root"
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
async metaInstances(ctx) {
|
|
390
|
+
return {
|
|
391
|
+
id: `gce://${this.projectId}/instances`,
|
|
392
|
+
path: ctx.path,
|
|
393
|
+
meta: {
|
|
394
|
+
kind: KINDS.NODE,
|
|
395
|
+
kinds: [KINDS.NODE],
|
|
396
|
+
childrenCount: -1,
|
|
397
|
+
description: "GCE Instances",
|
|
398
|
+
platformRef: generateInstancesListPlatformRef(this.projectId)
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async metaInstance(ctx) {
|
|
403
|
+
try {
|
|
404
|
+
const instance = await this.getInstance(ctx.params.instanceId);
|
|
405
|
+
const machineType = instance.machineType?.split("/").pop() || "unknown";
|
|
406
|
+
return {
|
|
407
|
+
id: `gce://${this.projectId}/${this.zone}/${ctx.params.instanceId}`,
|
|
408
|
+
path: ctx.path,
|
|
409
|
+
meta: {
|
|
410
|
+
kind: KINDS.INSTANCE,
|
|
411
|
+
kinds: [
|
|
412
|
+
KINDS.INSTANCE,
|
|
413
|
+
KINDS.GCP_RESOURCE,
|
|
414
|
+
KINDS.NODE
|
|
415
|
+
],
|
|
416
|
+
childrenCount: -1,
|
|
417
|
+
status: instance.status,
|
|
418
|
+
machineType,
|
|
419
|
+
privateIp: instance.networkInterfaces?.[0]?.networkIP,
|
|
420
|
+
publicIp: instance.networkInterfaces?.[0]?.accessConfigs?.[0]?.natIP,
|
|
421
|
+
createdAt: instance.creationTimestamp,
|
|
422
|
+
labels: instance.labels,
|
|
423
|
+
platformRef: generateInstancePlatformRef(this.projectId, this.zone, ctx.params.instanceId)
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
} catch (error) {
|
|
427
|
+
throw mapGCEError(error, ctx.path);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async statRoot(ctx) {
|
|
431
|
+
const meta = await this.metaRoot(ctx);
|
|
790
432
|
return { data: {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
meta:
|
|
433
|
+
id: meta.id,
|
|
434
|
+
path: ctx.path,
|
|
435
|
+
meta: meta.meta
|
|
794
436
|
} };
|
|
795
437
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
438
|
+
async statInstances(ctx) {
|
|
439
|
+
const meta = await this.metaInstances(ctx);
|
|
440
|
+
return { data: {
|
|
441
|
+
id: meta.id,
|
|
442
|
+
path: ctx.path,
|
|
443
|
+
meta: meta.meta
|
|
444
|
+
} };
|
|
445
|
+
}
|
|
446
|
+
async statInstance(ctx) {
|
|
447
|
+
const meta = await this.metaInstance(ctx);
|
|
448
|
+
return { data: {
|
|
449
|
+
id: meta.id,
|
|
450
|
+
path: ctx.path,
|
|
451
|
+
meta: meta.meta
|
|
452
|
+
} };
|
|
453
|
+
}
|
|
454
|
+
async writeInstanceLabel(ctx, payload) {
|
|
455
|
+
if (this.accessMode !== "readwrite") throw new AFSReadonlyError("Write operations require readwrite access mode");
|
|
456
|
+
try {
|
|
457
|
+
const instance = await this.getInstance(ctx.params.instanceId);
|
|
458
|
+
const labels = { ...instance.labels || {} };
|
|
459
|
+
labels[ctx.params.labelKey] = String(payload.content ?? "");
|
|
460
|
+
await this.client.setLabels({
|
|
461
|
+
project: this.projectId,
|
|
462
|
+
zone: this.zone,
|
|
463
|
+
instance: ctx.params.instanceId,
|
|
464
|
+
instancesSetLabelsRequestResource: {
|
|
465
|
+
labels,
|
|
466
|
+
labelFingerprint: instance.labelFingerprint
|
|
467
|
+
}
|
|
468
|
+
});
|
|
812
469
|
return {
|
|
813
470
|
data: {
|
|
814
|
-
id: `gce://${this.projectId}/${this.zone}/${
|
|
815
|
-
path,
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
kinds: [
|
|
819
|
-
KINDS.ACTION,
|
|
820
|
-
KINDS.EXECUTABLE,
|
|
821
|
-
KINDS.NODE
|
|
822
|
-
],
|
|
823
|
-
success: result.success,
|
|
824
|
-
operationName: result.operationName,
|
|
825
|
-
error: result.error
|
|
826
|
-
}
|
|
471
|
+
id: `gce://${this.projectId}/${this.zone}/${ctx.params.instanceId}/labels/${ctx.params.labelKey}`,
|
|
472
|
+
path: ctx.path,
|
|
473
|
+
content: payload.content,
|
|
474
|
+
meta: { kind: "gce:label" }
|
|
827
475
|
},
|
|
828
|
-
message:
|
|
476
|
+
message: `Label '${ctx.params.labelKey}' set on instance '${ctx.params.instanceId}'`
|
|
477
|
+
};
|
|
478
|
+
} catch (error) {
|
|
479
|
+
throw mapGCEError(error, ctx.path);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async deleteInstanceLabel(ctx) {
|
|
483
|
+
if (this.accessMode !== "readwrite") throw new AFSReadonlyError("Delete operations require readwrite access mode");
|
|
484
|
+
try {
|
|
485
|
+
const instance = await this.getInstance(ctx.params.instanceId);
|
|
486
|
+
const labels = { ...instance.labels || {} };
|
|
487
|
+
if (!(ctx.params.labelKey in labels)) throw new AFSNotFoundError(ctx.path, `Label '${ctx.params.labelKey}' not found on instance '${ctx.params.instanceId}'`);
|
|
488
|
+
delete labels[ctx.params.labelKey];
|
|
489
|
+
await this.client.setLabels({
|
|
490
|
+
project: this.projectId,
|
|
491
|
+
zone: this.zone,
|
|
492
|
+
instance: ctx.params.instanceId,
|
|
493
|
+
instancesSetLabelsRequestResource: {
|
|
494
|
+
labels,
|
|
495
|
+
labelFingerprint: instance.labelFingerprint
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
return { message: `Label '${ctx.params.labelKey}' removed from instance '${ctx.params.instanceId}'` };
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (error instanceof AFSNotFoundError) throw error;
|
|
501
|
+
throw mapGCEError(error, ctx.path);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async listGlobalActions(ctx) {
|
|
505
|
+
return { data: [{
|
|
506
|
+
id: "refresh",
|
|
507
|
+
path: joinURL(ctx.path, "refresh"),
|
|
508
|
+
summary: "Refresh instance cache",
|
|
509
|
+
meta: {
|
|
510
|
+
kind: "afs:executable",
|
|
511
|
+
kinds: ["afs:executable", "afs:node"],
|
|
512
|
+
inputSchema: {
|
|
513
|
+
type: "object",
|
|
514
|
+
properties: {}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}] };
|
|
518
|
+
}
|
|
519
|
+
async refreshAction(_ctx, _args) {
|
|
520
|
+
return {
|
|
521
|
+
success: true,
|
|
522
|
+
data: { message: "Cache refreshed" }
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
async listInstanceActions(ctx) {
|
|
526
|
+
const status = (await this.getInstance(ctx.params.instanceId)).status;
|
|
527
|
+
const actions = [];
|
|
528
|
+
switch (status) {
|
|
529
|
+
case "RUNNING":
|
|
530
|
+
actions.push({
|
|
531
|
+
name: "stop",
|
|
532
|
+
summary: "Stop the instance"
|
|
533
|
+
}, {
|
|
534
|
+
name: "reset",
|
|
535
|
+
summary: "Reset (hard reboot) the instance"
|
|
536
|
+
});
|
|
537
|
+
break;
|
|
538
|
+
case "STOPPED":
|
|
539
|
+
case "TERMINATED":
|
|
540
|
+
actions.push({
|
|
541
|
+
name: "start",
|
|
542
|
+
summary: "Start the instance"
|
|
543
|
+
});
|
|
544
|
+
break;
|
|
545
|
+
case "SUSPENDED":
|
|
546
|
+
actions.push({
|
|
547
|
+
name: "start",
|
|
548
|
+
summary: "Resume the instance"
|
|
549
|
+
});
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
return { data: actions.map((action) => ({
|
|
553
|
+
id: action.name,
|
|
554
|
+
path: joinURL(ctx.path, action.name),
|
|
555
|
+
summary: action.summary,
|
|
556
|
+
meta: {
|
|
557
|
+
kind: "afs:executable",
|
|
558
|
+
kinds: ["afs:executable", "afs:node"],
|
|
559
|
+
inputSchema: {
|
|
560
|
+
type: "object",
|
|
561
|
+
properties: {}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
})) };
|
|
565
|
+
}
|
|
566
|
+
async startInstanceAction(ctx, _args) {
|
|
567
|
+
try {
|
|
568
|
+
const status = (await this.getInstance(ctx.params.instanceId)).status;
|
|
569
|
+
if (status !== "STOPPED" && status !== "TERMINATED") return {
|
|
570
|
+
success: false,
|
|
571
|
+
error: {
|
|
572
|
+
code: "INVALID_STATE",
|
|
573
|
+
message: `Cannot start instance in ${status} state. Instance must be STOPPED or TERMINATED.`
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
const [operation] = await this.client.start({
|
|
577
|
+
project: this.projectId,
|
|
578
|
+
zone: this.zone,
|
|
579
|
+
instance: ctx.params.instanceId
|
|
580
|
+
});
|
|
581
|
+
return {
|
|
582
|
+
success: true,
|
|
583
|
+
data: { operationName: operation.latestResponse?.name || "unknown" }
|
|
584
|
+
};
|
|
585
|
+
} catch (error) {
|
|
586
|
+
throw mapGCEError(error, ctx.path);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async stopInstanceAction(ctx, _args) {
|
|
590
|
+
try {
|
|
591
|
+
const status = (await this.getInstance(ctx.params.instanceId)).status;
|
|
592
|
+
if (status !== "RUNNING") return {
|
|
593
|
+
success: false,
|
|
594
|
+
error: {
|
|
595
|
+
code: "INVALID_STATE",
|
|
596
|
+
message: `Cannot stop instance in ${status} state. Instance must be RUNNING.`
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
const [operation] = await this.client.stop({
|
|
600
|
+
project: this.projectId,
|
|
601
|
+
zone: this.zone,
|
|
602
|
+
instance: ctx.params.instanceId
|
|
603
|
+
});
|
|
604
|
+
return {
|
|
605
|
+
success: true,
|
|
606
|
+
data: { operationName: operation.latestResponse?.name || "unknown" }
|
|
607
|
+
};
|
|
608
|
+
} catch (error) {
|
|
609
|
+
throw mapGCEError(error, ctx.path);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async resetInstanceAction(ctx, _args) {
|
|
613
|
+
try {
|
|
614
|
+
const status = (await this.getInstance(ctx.params.instanceId)).status;
|
|
615
|
+
if (status !== "RUNNING") return {
|
|
616
|
+
success: false,
|
|
617
|
+
error: {
|
|
618
|
+
code: "INVALID_STATE",
|
|
619
|
+
message: `Cannot reset instance in ${status} state. Instance must be RUNNING.`
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
const [operation] = await this.client.reset({
|
|
623
|
+
project: this.projectId,
|
|
624
|
+
zone: this.zone,
|
|
625
|
+
instance: ctx.params.instanceId
|
|
626
|
+
});
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
data: { operationName: operation.latestResponse?.name || "unknown" }
|
|
630
|
+
};
|
|
631
|
+
} catch (error) {
|
|
632
|
+
throw mapGCEError(error, ctx.path);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async explainRoot(_ctx) {
|
|
636
|
+
try {
|
|
637
|
+
const instances = await this.listAllInstances();
|
|
638
|
+
const byStatus = {};
|
|
639
|
+
for (const inst of instances) {
|
|
640
|
+
const status = inst.status || "UNKNOWN";
|
|
641
|
+
byStatus[status] = (byStatus[status] || 0) + 1;
|
|
642
|
+
}
|
|
643
|
+
const statusLines = Object.entries(byStatus).map(([status, count]) => `- **${status}**: ${count}`).join("\n");
|
|
644
|
+
return {
|
|
645
|
+
format: "markdown",
|
|
646
|
+
content: `# GCE Provider
|
|
647
|
+
|
|
648
|
+
- **Project**: ${this.projectId}
|
|
649
|
+
- **Zone**: ${this.zone}
|
|
650
|
+
- **Access Mode**: ${this.accessMode}
|
|
651
|
+
- **Total Instances**: ${instances.length}
|
|
652
|
+
|
|
653
|
+
## Instances by Status
|
|
654
|
+
|
|
655
|
+
${statusLines || "- No instances found"}
|
|
656
|
+
|
|
657
|
+
## Navigation
|
|
658
|
+
|
|
659
|
+
- \`/instances/\` — list all instances
|
|
660
|
+
- \`/by-status/\` — instances grouped by status
|
|
661
|
+
- \`/.actions/\` — global actions
|
|
662
|
+
`
|
|
663
|
+
};
|
|
664
|
+
} catch (error) {
|
|
665
|
+
throw mapGCEError(error);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async explainInstance(ctx) {
|
|
669
|
+
try {
|
|
670
|
+
const instance = await this.getInstance(ctx.params.instanceId);
|
|
671
|
+
const machineType = instance.machineType?.split("/").pop() || "unknown";
|
|
672
|
+
const privateIp = instance.networkInterfaces?.[0]?.networkIP || "none";
|
|
673
|
+
const publicIp = instance.networkInterfaces?.[0]?.accessConfigs?.[0]?.natIP || "none";
|
|
674
|
+
const labels = instance.labels || {};
|
|
675
|
+
const labelLines = Object.entries(labels).map(([k, v]) => `- \`${k}\`: ${v}`).join("\n");
|
|
676
|
+
return {
|
|
677
|
+
format: "markdown",
|
|
678
|
+
content: `# Instance: ${ctx.params.instanceId}
|
|
679
|
+
|
|
680
|
+
- **Status**: ${instance.status}
|
|
681
|
+
- **Machine Type**: ${machineType}
|
|
682
|
+
- **Zone**: ${this.zone}
|
|
683
|
+
- **Created**: ${instance.creationTimestamp || "unknown"}
|
|
684
|
+
|
|
685
|
+
## Network
|
|
686
|
+
|
|
687
|
+
- **Private IP**: ${privateIp}
|
|
688
|
+
- **Public IP**: ${publicIp}
|
|
689
|
+
|
|
690
|
+
## Labels
|
|
691
|
+
|
|
692
|
+
${labelLines || "- No labels"}
|
|
693
|
+
`
|
|
829
694
|
};
|
|
695
|
+
} catch (error) {
|
|
696
|
+
throw mapGCEError(error, ctx.path);
|
|
830
697
|
}
|
|
831
|
-
throw mapGCEError(/* @__PURE__ */ new Error(`Cannot write to path: ${path}`));
|
|
832
698
|
}
|
|
833
699
|
};
|
|
700
|
+
__decorate([List("/")], AFSGCE.prototype, "listRoot", null);
|
|
701
|
+
__decorate([List("/instances")], AFSGCE.prototype, "listInstances", null);
|
|
702
|
+
__decorate([List("/instances/:instanceId"), List("/by-status/:status/:instanceId")], AFSGCE.prototype, "listInstanceChildren", null);
|
|
703
|
+
__decorate([List("/instances/:instanceId/labels"), List("/by-status/:status/:instanceId/labels")], AFSGCE.prototype, "listInstanceLabels", null);
|
|
704
|
+
__decorate([List("/by-status")], AFSGCE.prototype, "listByStatus", null);
|
|
705
|
+
__decorate([List("/by-status/:status")], AFSGCE.prototype, "listByStatusFilter", null);
|
|
706
|
+
__decorate([Read("/.meta/.capabilities")], AFSGCE.prototype, "readCapabilities", null);
|
|
707
|
+
__decorate([Read("/instances/:instanceId/metadata.json"), Read("/by-status/:status/:instanceId/metadata.json")], AFSGCE.prototype, "readInstanceMetadata", null);
|
|
708
|
+
__decorate([Read("/instances/:instanceId/labels/:labelKey"), Read("/by-status/:status/:instanceId/labels/:labelKey")], AFSGCE.prototype, "readInstanceLabel", null);
|
|
709
|
+
__decorate([Meta("/")], AFSGCE.prototype, "metaRoot", null);
|
|
710
|
+
__decorate([Meta("/instances")], AFSGCE.prototype, "metaInstances", null);
|
|
711
|
+
__decorate([Meta("/instances/:instanceId"), Meta("/by-status/:status/:instanceId")], AFSGCE.prototype, "metaInstance", null);
|
|
712
|
+
__decorate([Stat("/")], AFSGCE.prototype, "statRoot", null);
|
|
713
|
+
__decorate([Stat("/instances")], AFSGCE.prototype, "statInstances", null);
|
|
714
|
+
__decorate([Stat("/instances/:instanceId"), Stat("/by-status/:status/:instanceId")], AFSGCE.prototype, "statInstance", null);
|
|
715
|
+
__decorate([Write("/instances/:instanceId/labels/:labelKey"), Write("/by-status/:status/:instanceId/labels/:labelKey")], AFSGCE.prototype, "writeInstanceLabel", null);
|
|
716
|
+
__decorate([Delete("/instances/:instanceId/labels/:labelKey"), Delete("/by-status/:status/:instanceId/labels/:labelKey")], AFSGCE.prototype, "deleteInstanceLabel", null);
|
|
717
|
+
__decorate([Actions("/")], AFSGCE.prototype, "listGlobalActions", null);
|
|
718
|
+
__decorate([Actions.Exec("/", "refresh")], AFSGCE.prototype, "refreshAction", null);
|
|
719
|
+
__decorate([Actions("/instances/:instanceId"), Actions("/by-status/:status/:instanceId")], AFSGCE.prototype, "listInstanceActions", null);
|
|
720
|
+
__decorate([Actions.Exec("/instances/:instanceId", "start"), Actions.Exec("/by-status/:status/:instanceId", "start")], AFSGCE.prototype, "startInstanceAction", null);
|
|
721
|
+
__decorate([Actions.Exec("/instances/:instanceId", "stop"), Actions.Exec("/by-status/:status/:instanceId", "stop")], AFSGCE.prototype, "stopInstanceAction", null);
|
|
722
|
+
__decorate([Actions.Exec("/instances/:instanceId", "reset"), Actions.Exec("/by-status/:status/:instanceId", "reset")], AFSGCE.prototype, "resetInstanceAction", null);
|
|
723
|
+
__decorate([Explain("/")], AFSGCE.prototype, "explainRoot", null);
|
|
724
|
+
__decorate([Explain("/instances/:instanceId"), Explain("/by-status/:status/:instanceId")], AFSGCE.prototype, "explainInstance", null);
|
|
834
725
|
|
|
835
726
|
//#endregion
|
|
836
727
|
export { AFSGCE, afsgceOptionsSchema };
|