@bifos/nhncloud-cli 0.10.0 → 0.10.1
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/README.md +5 -0
- package/dist/index.js +39 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# nhncloud-cli
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@bifos/nhncloud-cli)
|
|
4
|
+
[](https://www.npmjs.com/package/@bifos/nhncloud-cli)
|
|
5
|
+
[](https://github.com/jon890/nhncloud-cli/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/jon890/nhncloud-cli/blob/main/LICENSE)
|
|
7
|
+
|
|
3
8
|
NHN Cloud 서비스를 AWS CLI 방식으로 호출하는 통합 CLI.
|
|
4
9
|
현재 106개 command catalog 항목을 지원한다.
|
|
5
10
|
`configure`, `commands`, `logncrash search/send/export`, `deploy`, `instance`, `network`, `volume`, `floatingip`, `ncr`, `nks`, `ncs` 명령으로 NHN Cloud 서비스를 조회·운영할 수 있다.
|
package/dist/index.js
CHANGED
|
@@ -315,15 +315,18 @@ var import_node_path2 = require("path");
|
|
|
315
315
|
var import_node_os2 = require("os");
|
|
316
316
|
var import_node_crypto = require("crypto");
|
|
317
317
|
var CACHE_DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".nhncloud", "cache");
|
|
318
|
+
function credentialFingerprint(material) {
|
|
319
|
+
return (0, import_node_crypto.createHash)("sha256").update(material).digest("hex");
|
|
320
|
+
}
|
|
318
321
|
function iaasCachePath(profile, region) {
|
|
319
322
|
return (0, import_node_path2.join)(CACHE_DIR, `iaas-token-${profile}-${region}.json`);
|
|
320
323
|
}
|
|
321
324
|
function isIaasTokenCache(val) {
|
|
322
325
|
if (typeof val !== "object" || val === null) return false;
|
|
323
326
|
const obj = val;
|
|
324
|
-
return typeof obj["tokenId"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["computeEndpoint"] === "string" && typeof obj["imageEndpoint"] === "string" && typeof obj["networkEndpoint"] === "string" && typeof obj["blockStorageEndpoint"] === "string" && typeof obj["nksEndpoint"] === "string";
|
|
327
|
+
return typeof obj["tokenId"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["credentialHash"] === "string" && typeof obj["computeEndpoint"] === "string" && typeof obj["imageEndpoint"] === "string" && typeof obj["networkEndpoint"] === "string" && typeof obj["blockStorageEndpoint"] === "string" && typeof obj["nksEndpoint"] === "string";
|
|
325
328
|
}
|
|
326
|
-
async function readIaasToken(profile, region) {
|
|
329
|
+
async function readIaasToken(profile, region, credentialHash) {
|
|
327
330
|
const filePath = iaasCachePath(profile, region);
|
|
328
331
|
try {
|
|
329
332
|
const raw = await (0, import_promises2.readFile)(filePath, "utf-8");
|
|
@@ -332,6 +335,7 @@ async function readIaasToken(profile, region) {
|
|
|
332
335
|
const expiresAt = new Date(parsed.expiresAt).getTime();
|
|
333
336
|
const BUFFER_MS = 6e4;
|
|
334
337
|
if (expiresAt - Date.now() < BUFFER_MS) return null;
|
|
338
|
+
if (parsed.credentialHash !== credentialHash) return null;
|
|
335
339
|
return {
|
|
336
340
|
tokenId: parsed.tokenId,
|
|
337
341
|
expiresAt: parsed.expiresAt,
|
|
@@ -351,6 +355,7 @@ async function writeIaasToken(profile, region, data) {
|
|
|
351
355
|
const cache = {
|
|
352
356
|
tokenId: data.tokenId,
|
|
353
357
|
expiresAt: data.expiresAt,
|
|
358
|
+
credentialHash: data.credentialHash,
|
|
354
359
|
computeEndpoint: data.computeEndpoint,
|
|
355
360
|
imageEndpoint: data.imageEndpoint,
|
|
356
361
|
networkEndpoint: data.networkEndpoint,
|
|
@@ -362,14 +367,14 @@ async function writeIaasToken(profile, region, data) {
|
|
|
362
367
|
await (0, import_promises2.rename)(tmp, filePath);
|
|
363
368
|
}
|
|
364
369
|
function tokenCachePath(profile) {
|
|
365
|
-
return (0, import_node_path2.join)(CACHE_DIR, `
|
|
370
|
+
return (0, import_node_path2.join)(CACHE_DIR, `user-access-token-${profile}.json`);
|
|
366
371
|
}
|
|
367
372
|
function isTokenCache(val) {
|
|
368
373
|
if (typeof val !== "object" || val === null) return false;
|
|
369
374
|
const obj = val;
|
|
370
|
-
return typeof obj["accessToken"] === "string" && typeof obj["expiresAt"] === "string";
|
|
375
|
+
return typeof obj["accessToken"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["credentialHash"] === "string";
|
|
371
376
|
}
|
|
372
|
-
async function readToken(profile) {
|
|
377
|
+
async function readToken(profile, credentialHash) {
|
|
373
378
|
const filePath = tokenCachePath(profile);
|
|
374
379
|
try {
|
|
375
380
|
const raw = await (0, import_promises2.readFile)(filePath, "utf-8");
|
|
@@ -379,17 +384,19 @@ async function readToken(profile) {
|
|
|
379
384
|
const now = Date.now();
|
|
380
385
|
const BUFFER_MS = 6e4;
|
|
381
386
|
if (expiresAt - now < BUFFER_MS) return null;
|
|
387
|
+
if (parsed.credentialHash !== credentialHash) return null;
|
|
382
388
|
return { accessToken: parsed.accessToken, expiresAt: parsed.expiresAt };
|
|
383
389
|
} catch {
|
|
384
390
|
return null;
|
|
385
391
|
}
|
|
386
392
|
}
|
|
387
|
-
async function writeToken(profile, accessToken, expiresAt) {
|
|
393
|
+
async function writeToken(profile, accessToken, expiresAt, credentialHash) {
|
|
388
394
|
const filePath = tokenCachePath(profile);
|
|
389
395
|
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(filePath), { recursive: true });
|
|
390
396
|
const data = {
|
|
391
397
|
accessToken,
|
|
392
|
-
expiresAt: expiresAt.toISOString()
|
|
398
|
+
expiresAt: expiresAt.toISOString(),
|
|
399
|
+
credentialHash
|
|
393
400
|
};
|
|
394
401
|
const tmp = filePath + "." + (0, import_node_crypto.randomBytes)(4).toString("hex") + ".tmp";
|
|
395
402
|
await (0, import_promises2.writeFile)(tmp, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
@@ -424,8 +431,9 @@ function isTokenResponse(val) {
|
|
|
424
431
|
return typeof obj["access_token"] === "string" && typeof obj["expires_in"] === "number";
|
|
425
432
|
}
|
|
426
433
|
async function getAccessToken(profile, uakId, uakSecret, forceRefresh = false) {
|
|
434
|
+
const credentialHash = credentialFingerprint(JSON.stringify([uakId, uakSecret]));
|
|
427
435
|
if (!forceRefresh) {
|
|
428
|
-
const cached = await readToken(profile);
|
|
436
|
+
const cached = await readToken(profile, credentialHash);
|
|
429
437
|
if (cached !== null) {
|
|
430
438
|
return cached.accessToken;
|
|
431
439
|
}
|
|
@@ -449,7 +457,7 @@ async function getAccessToken(profile, uakId, uakSecret, forceRefresh = false) {
|
|
|
449
457
|
}
|
|
450
458
|
if (!forceRefresh) {
|
|
451
459
|
const expiresAt = new Date(Date.now() + raw.expires_in * 1e3);
|
|
452
|
-
await writeToken(profile, raw.access_token, expiresAt);
|
|
460
|
+
await writeToken(profile, raw.access_token, expiresAt, credentialHash);
|
|
453
461
|
}
|
|
454
462
|
return raw.access_token;
|
|
455
463
|
}
|
|
@@ -597,8 +605,11 @@ function isKeystoneTokenResponse(val) {
|
|
|
597
605
|
return typeof token["id"] === "string" && typeof token["expires"] === "string";
|
|
598
606
|
}
|
|
599
607
|
async function getIaasToken(profile, iaas, forceRefresh = false) {
|
|
608
|
+
const credentialHash = credentialFingerprint(
|
|
609
|
+
JSON.stringify([iaas.tenantId, iaas.username, iaas.password])
|
|
610
|
+
);
|
|
600
611
|
if (!forceRefresh) {
|
|
601
|
-
const cached = await readIaasToken(profile, iaas.region);
|
|
612
|
+
const cached = await readIaasToken(profile, iaas.region, credentialHash);
|
|
602
613
|
if (cached !== null) {
|
|
603
614
|
return {
|
|
604
615
|
tokenId: cached.tokenId,
|
|
@@ -642,7 +653,7 @@ async function getIaasToken(profile, iaas, forceRefresh = false) {
|
|
|
642
653
|
const tokenId = raw.access.token.id;
|
|
643
654
|
const expiresAt = raw.access.token.expires;
|
|
644
655
|
if (!forceRefresh) {
|
|
645
|
-
await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint, nksEndpoint });
|
|
656
|
+
await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, credentialHash, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint, nksEndpoint });
|
|
646
657
|
}
|
|
647
658
|
return { tokenId, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint, nksEndpoint };
|
|
648
659
|
}
|
|
@@ -3887,11 +3898,6 @@ function isVolume(val) {
|
|
|
3887
3898
|
return typeof obj["id"] === "string" && // Cinder 는 --name 미지정 시 null — null 인 볼륨 하나가 list 전체를 거부하지 않게 허용 (isImage 선례).
|
|
3888
3899
|
(typeof obj["name"] === "string" || obj["name"] === null) && typeof obj["size"] === "number" && typeof obj["status"] === "string" && (obj["availability_zone"] === void 0 || typeof obj["availability_zone"] === "string") && Array.isArray(obj["attachments"]);
|
|
3889
3900
|
}
|
|
3890
|
-
function isVolumesResponse(val) {
|
|
3891
|
-
if (typeof val !== "object" || val === null) return false;
|
|
3892
|
-
const obj = val;
|
|
3893
|
-
return Array.isArray(obj["volumes"]) && obj["volumes"].every(isVolume);
|
|
3894
|
-
}
|
|
3895
3901
|
function isVolumeResponse(val) {
|
|
3896
3902
|
if (typeof val !== "object" || val === null) return false;
|
|
3897
3903
|
const obj = val;
|
|
@@ -3909,7 +3915,7 @@ var BlockStorageClient = class {
|
|
|
3909
3915
|
return { "X-Auth-Token": this.tokenId };
|
|
3910
3916
|
}
|
|
3911
3917
|
async list(params) {
|
|
3912
|
-
const url = `${this.endpoint}/volumes`;
|
|
3918
|
+
const url = `${this.endpoint}/volumes/detail`;
|
|
3913
3919
|
const searchParams = {};
|
|
3914
3920
|
if (params?.sort !== void 0) searchParams["sort"] = params.sort;
|
|
3915
3921
|
if (params?.limit !== void 0) searchParams["limit"] = params.limit;
|
|
@@ -3917,13 +3923,26 @@ var BlockStorageClient = class {
|
|
|
3917
3923
|
if (params?.marker !== void 0) searchParams["marker"] = params.marker;
|
|
3918
3924
|
try {
|
|
3919
3925
|
const raw = await import_ky9.default.get(url, { headers: this.authHeaders(), searchParams, retry: 0, timeout: DEFAULT_TIMEOUT_MS5 }).json();
|
|
3920
|
-
if (
|
|
3926
|
+
if (typeof raw !== "object" || raw === null) {
|
|
3921
3927
|
throw new NhnCloudCliError(
|
|
3922
3928
|
"volume list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumes \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
3923
3929
|
EXIT_API_ERROR
|
|
3924
3930
|
);
|
|
3925
3931
|
}
|
|
3926
|
-
|
|
3932
|
+
const obj = raw;
|
|
3933
|
+
if (!Array.isArray(obj["volumes"])) {
|
|
3934
|
+
throw new NhnCloudCliError(
|
|
3935
|
+
"volume list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumes \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
3936
|
+
EXIT_API_ERROR
|
|
3937
|
+
);
|
|
3938
|
+
}
|
|
3939
|
+
if (!obj["volumes"].every(isVolume)) {
|
|
3940
|
+
throw new NhnCloudCliError(
|
|
3941
|
+
"volume list \uC751\uB2F5\uC758 \uBCFC\uB968 \uD56D\uBAA9 \uD615\uC2DD\uC774 \uC608\uC0C1\uACFC \uB2E4\uB985\uB2C8\uB2E4 \u2014 API \uC751\uB2F5 \uD544\uB4DC\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
3942
|
+
EXIT_API_ERROR
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
return obj["volumes"];
|
|
3927
3946
|
} catch (err) {
|
|
3928
3947
|
throw toNhnCloudCliError(err);
|
|
3929
3948
|
}
|
|
@@ -7592,7 +7611,7 @@ Agent workflow:
|
|
|
7592
7611
|
3. nhncloud ncs workload get <id> --json
|
|
7593
7612
|
`;
|
|
7594
7613
|
var program = new import_commander48.Command();
|
|
7595
|
-
program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.10.
|
|
7614
|
+
program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.10.1").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "\uCD5C\uC18C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9)").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654").addHelpText("after", rootAgentHints);
|
|
7596
7615
|
program.hook("preAction", () => {
|
|
7597
7616
|
const opts = program.opts();
|
|
7598
7617
|
if (!opts.color || process.env["NO_COLOR"]) {
|