@bifos/nhncloud-cli 0.2.0 → 0.4.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/README.md +232 -1
- package/dist/index.js +2334 -113
- package/package.json +1 -1
- package/skills/nhncloud-cli/SKILL.md +259 -2
package/dist/index.js
CHANGED
|
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
27
|
+
var import_commander34 = require("commander");
|
|
28
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
29
29
|
|
|
30
30
|
// src/utils/spinner.ts
|
|
31
31
|
var import_ora = __toESM(require("ora"));
|
|
@@ -317,7 +317,7 @@ function iaasCachePath(profile, region) {
|
|
|
317
317
|
function isIaasTokenCache(val) {
|
|
318
318
|
if (typeof val !== "object" || val === null) return false;
|
|
319
319
|
const obj = val;
|
|
320
|
-
return typeof obj["tokenId"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["computeEndpoint"] === "string";
|
|
320
|
+
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";
|
|
321
321
|
}
|
|
322
322
|
async function readIaasToken(profile, region) {
|
|
323
323
|
const filePath = iaasCachePath(profile, region);
|
|
@@ -331,7 +331,10 @@ async function readIaasToken(profile, region) {
|
|
|
331
331
|
return {
|
|
332
332
|
tokenId: parsed.tokenId,
|
|
333
333
|
expiresAt: parsed.expiresAt,
|
|
334
|
-
computeEndpoint: parsed.computeEndpoint
|
|
334
|
+
computeEndpoint: parsed.computeEndpoint,
|
|
335
|
+
imageEndpoint: parsed.imageEndpoint,
|
|
336
|
+
networkEndpoint: parsed.networkEndpoint,
|
|
337
|
+
blockStorageEndpoint: parsed.blockStorageEndpoint
|
|
335
338
|
};
|
|
336
339
|
} catch {
|
|
337
340
|
return null;
|
|
@@ -343,7 +346,10 @@ async function writeIaasToken(profile, region, data) {
|
|
|
343
346
|
const cache = {
|
|
344
347
|
tokenId: data.tokenId,
|
|
345
348
|
expiresAt: data.expiresAt,
|
|
346
|
-
computeEndpoint: data.computeEndpoint
|
|
349
|
+
computeEndpoint: data.computeEndpoint,
|
|
350
|
+
imageEndpoint: data.imageEndpoint,
|
|
351
|
+
networkEndpoint: data.networkEndpoint,
|
|
352
|
+
blockStorageEndpoint: data.blockStorageEndpoint
|
|
347
353
|
};
|
|
348
354
|
const tmp = filePath + "." + (0, import_node_crypto.randomBytes)(4).toString("hex") + ".tmp";
|
|
349
355
|
await (0, import_promises2.writeFile)(tmp, JSON.stringify(cache, null, 2), { encoding: "utf-8", mode: 384 });
|
|
@@ -448,6 +454,7 @@ var import_ky3 = __toESM(require("ky"));
|
|
|
448
454
|
// src/api/endpoints.ts
|
|
449
455
|
var ENDPOINTS = {
|
|
450
456
|
logncrash: "https://api-lncs-search.nhncloudservice.com",
|
|
457
|
+
"logncrash-collector": "https://api-logncrash.nhncloudservice.com",
|
|
451
458
|
deploy: "https://api-deploy.nhncloudservice.com"
|
|
452
459
|
};
|
|
453
460
|
function endpointFor(service) {
|
|
@@ -469,11 +476,60 @@ var INSTANCE_HOST = {
|
|
|
469
476
|
kr3: "kr3-api-instance-infrastructure.nhncloudservice.com",
|
|
470
477
|
jp1: "jp1-api-instance-infrastructure.nhncloudservice.com"
|
|
471
478
|
};
|
|
479
|
+
var IMAGE_HOST = {
|
|
480
|
+
kr1: "kr1-api-image-infrastructure.nhncloudservice.com",
|
|
481
|
+
kr2: "kr2-api-image-infrastructure.nhncloudservice.com",
|
|
482
|
+
kr3: "kr3-api-image-infrastructure.nhncloudservice.com",
|
|
483
|
+
jp1: "jp1-api-image-infrastructure.nhncloudservice.com"
|
|
484
|
+
};
|
|
485
|
+
var NETWORK_HOST = {
|
|
486
|
+
kr1: "kr1-api-network-infrastructure.nhncloudservice.com",
|
|
487
|
+
kr2: "kr2-api-network-infrastructure.nhncloudservice.com",
|
|
488
|
+
kr3: "kr3-api-network-infrastructure.nhncloudservice.com",
|
|
489
|
+
jp1: "jp1-api-network-infrastructure.nhncloudservice.com"
|
|
490
|
+
};
|
|
491
|
+
var BLOCKSTORAGE_HOST = {
|
|
492
|
+
kr1: "kr1-api-block-storage-infrastructure.nhncloudservice.com",
|
|
493
|
+
kr2: "kr2-api-block-storage-infrastructure.nhncloudservice.com",
|
|
494
|
+
kr3: "kr3-api-block-storage-infrastructure.nhncloudservice.com",
|
|
495
|
+
jp1: "jp1-api-block-storage-infrastructure.nhncloudservice.com"
|
|
496
|
+
};
|
|
497
|
+
var IAAS_REGIONS = Object.keys(INSTANCE_HOST).join(", ");
|
|
472
498
|
function instanceHost(region) {
|
|
473
499
|
const host = INSTANCE_HOST[region];
|
|
474
500
|
if (!host) {
|
|
475
501
|
throw new NhnCloudCliError(
|
|
476
|
-
`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${
|
|
502
|
+
`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
|
|
503
|
+
EXIT_PARAM_ERROR
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
return host;
|
|
507
|
+
}
|
|
508
|
+
function imageHost(region) {
|
|
509
|
+
const host = IMAGE_HOST[region];
|
|
510
|
+
if (!host) {
|
|
511
|
+
throw new NhnCloudCliError(
|
|
512
|
+
`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
|
|
513
|
+
EXIT_PARAM_ERROR
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return host;
|
|
517
|
+
}
|
|
518
|
+
function networkHost(region) {
|
|
519
|
+
const host = NETWORK_HOST[region];
|
|
520
|
+
if (!host) {
|
|
521
|
+
throw new NhnCloudCliError(
|
|
522
|
+
`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
|
|
523
|
+
EXIT_PARAM_ERROR
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return host;
|
|
527
|
+
}
|
|
528
|
+
function blockStorageHost(region) {
|
|
529
|
+
const host = BLOCKSTORAGE_HOST[region];
|
|
530
|
+
if (!host) {
|
|
531
|
+
throw new NhnCloudCliError(
|
|
532
|
+
`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
|
|
477
533
|
EXIT_PARAM_ERROR
|
|
478
534
|
);
|
|
479
535
|
}
|
|
@@ -494,11 +550,20 @@ async function getIaasToken(profile, iaas, forceRefresh = false) {
|
|
|
494
550
|
if (!forceRefresh) {
|
|
495
551
|
const cached = await readIaasToken(profile, iaas.region);
|
|
496
552
|
if (cached !== null) {
|
|
497
|
-
return {
|
|
553
|
+
return {
|
|
554
|
+
tokenId: cached.tokenId,
|
|
555
|
+
computeEndpoint: cached.computeEndpoint,
|
|
556
|
+
imageEndpoint: cached.imageEndpoint,
|
|
557
|
+
networkEndpoint: cached.networkEndpoint,
|
|
558
|
+
blockStorageEndpoint: cached.blockStorageEndpoint
|
|
559
|
+
};
|
|
498
560
|
}
|
|
499
561
|
}
|
|
500
562
|
const host = instanceHost(iaas.region);
|
|
501
563
|
const computeEndpoint = `https://${host}/v2/${encodeURIComponent(iaas.tenantId)}`;
|
|
564
|
+
const imageEndpoint = `https://${imageHost(iaas.region)}/v2`;
|
|
565
|
+
const networkEndpoint = `https://${networkHost(iaas.region)}/v2.0`;
|
|
566
|
+
const blockStorageEndpoint = `https://${blockStorageHost(iaas.region)}/v2/${encodeURIComponent(iaas.tenantId)}`;
|
|
502
567
|
let raw;
|
|
503
568
|
try {
|
|
504
569
|
raw = await import_ky3.default.post(keystoneIdentityUrl(), {
|
|
@@ -525,22 +590,25 @@ async function getIaasToken(profile, iaas, forceRefresh = false) {
|
|
|
525
590
|
const tokenId = raw.access.token.id;
|
|
526
591
|
const expiresAt = raw.access.token.expires;
|
|
527
592
|
if (!forceRefresh) {
|
|
528
|
-
await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, computeEndpoint });
|
|
593
|
+
await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint });
|
|
529
594
|
}
|
|
530
|
-
return { tokenId, computeEndpoint };
|
|
595
|
+
return { tokenId, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint };
|
|
531
596
|
}
|
|
532
597
|
|
|
533
598
|
// src/services/logncrash/client.ts
|
|
534
599
|
var import_ky4 = __toESM(require("ky"));
|
|
535
600
|
|
|
536
601
|
// src/api/envelope.ts
|
|
537
|
-
function
|
|
602
|
+
function unwrapHeader(res) {
|
|
538
603
|
if (!res.header.isSuccessful) {
|
|
539
604
|
throw new NhnCloudCliError(
|
|
540
605
|
`API \uC624\uB958: ${res.header.resultMessage}`,
|
|
541
606
|
EXIT_API_ERROR
|
|
542
607
|
);
|
|
543
608
|
}
|
|
609
|
+
}
|
|
610
|
+
function unwrap(res) {
|
|
611
|
+
unwrapHeader(res);
|
|
544
612
|
if (res.body === void 0) {
|
|
545
613
|
throw new NhnCloudCliError("API \uC751\uB2F5\uC5D0 body \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", EXIT_API_ERROR);
|
|
546
614
|
}
|
|
@@ -550,12 +618,19 @@ function unwrap(res) {
|
|
|
550
618
|
// src/services/logncrash/client.ts
|
|
551
619
|
var LogncrashClient = class {
|
|
552
620
|
appkey;
|
|
621
|
+
/** 검색(X-LNCS-SECRET)에만 필요. collector send 는 secret 을 쓰지 않으므로 옵셔널 (ADR-014). */
|
|
553
622
|
secret;
|
|
554
623
|
constructor(appkey, secret) {
|
|
555
624
|
this.appkey = appkey;
|
|
556
625
|
this.secret = secret;
|
|
557
626
|
}
|
|
558
627
|
async search(params) {
|
|
628
|
+
if (!this.secret) {
|
|
629
|
+
throw new NhnCloudCliError(
|
|
630
|
+
"logncrash search \uC5D0\uB294 secret \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. configure \uB85C logncrash secret \uC744 \uC124\uC815\uD558\uC138\uC694.",
|
|
631
|
+
EXIT_CONFIG_ERROR
|
|
632
|
+
);
|
|
633
|
+
}
|
|
559
634
|
const endpoint = endpointFor("logncrash");
|
|
560
635
|
const url = `${endpoint}/api/v2/search/${encodeURIComponent(this.appkey)}`;
|
|
561
636
|
try {
|
|
@@ -577,6 +652,92 @@ var LogncrashClient = class {
|
|
|
577
652
|
throw toNhnCloudCliError(err);
|
|
578
653
|
}
|
|
579
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* scroll 검색을 시작한다. POST /api/v2/search/scroll/{appkey}.
|
|
657
|
+
* body 는 search 와 동일(query/from/to/pageSize). 응답 scrollKey 로 scrollNext 를 이어 호출한다.
|
|
658
|
+
*/
|
|
659
|
+
async scrollStart(params) {
|
|
660
|
+
if (!this.secret) {
|
|
661
|
+
throw new NhnCloudCliError(
|
|
662
|
+
"logncrash scroll \uC5D0\uB294 secret \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. configure \uB85C logncrash secret \uC744 \uC124\uC815\uD558\uC138\uC694.",
|
|
663
|
+
EXIT_CONFIG_ERROR
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
const endpoint = endpointFor("logncrash");
|
|
667
|
+
const url = `${endpoint}/api/v2/search/scroll/${encodeURIComponent(this.appkey)}`;
|
|
668
|
+
try {
|
|
669
|
+
const res = await import_ky4.default.post(url, {
|
|
670
|
+
headers: {
|
|
671
|
+
"X-LNCS-SECRET": this.secret,
|
|
672
|
+
"Content-Type": "application/json"
|
|
673
|
+
},
|
|
674
|
+
json: {
|
|
675
|
+
query: params.query,
|
|
676
|
+
from: params.from,
|
|
677
|
+
to: params.to,
|
|
678
|
+
pageSize: params.pageSize ?? 100
|
|
679
|
+
}
|
|
680
|
+
}).json();
|
|
681
|
+
return unwrap(res);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
throw toNhnCloudCliError(err);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* scroll 다음 페이지를 가져온다. POST /api/v2/search/scroll/{appkey}/{scrollKey}.
|
|
688
|
+
* body 는 보내지 않는다(scrollKey 가 좌표). scrollKey 만료 시 API 가 실패 봉투를 주며,
|
|
689
|
+
* unwrap 이 EXIT_API_ERROR 로 변환한다 — 호출부에서 만료 안내 메시지로 감싼다.
|
|
690
|
+
*/
|
|
691
|
+
async scrollNext(scrollKey) {
|
|
692
|
+
if (!this.secret) {
|
|
693
|
+
throw new NhnCloudCliError(
|
|
694
|
+
"logncrash scroll \uC5D0\uB294 secret \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. configure \uB85C logncrash secret \uC744 \uC124\uC815\uD558\uC138\uC694.",
|
|
695
|
+
EXIT_CONFIG_ERROR
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
const endpoint = endpointFor("logncrash");
|
|
699
|
+
const url = `${endpoint}/api/v2/search/scroll/${encodeURIComponent(this.appkey)}/${encodeURIComponent(scrollKey)}`;
|
|
700
|
+
try {
|
|
701
|
+
const res = await import_ky4.default.post(url, {
|
|
702
|
+
headers: {
|
|
703
|
+
"X-LNCS-SECRET": this.secret,
|
|
704
|
+
"Content-Type": "application/json"
|
|
705
|
+
}
|
|
706
|
+
}).json();
|
|
707
|
+
return unwrap(res);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
throw toNhnCloudCliError(err);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* 로그 한 건을 Log & Crash collector 로 전송한다 (ADR-014).
|
|
714
|
+
* - host: api-logncrash (검색의 api-lncs-search 와 별도)
|
|
715
|
+
* - 인증: 헤더 없음 — body 의 projectName=appkey 로 식별 (secret 불요)
|
|
716
|
+
* - logVersion 은 "v2" 고정. logSource/logType 미지정 시 collector 기본값("http"/"log") 적용.
|
|
717
|
+
*/
|
|
718
|
+
async send(params) {
|
|
719
|
+
const endpoint = endpointFor("logncrash-collector");
|
|
720
|
+
const url = `${endpoint}/v2/log`;
|
|
721
|
+
const payload = {
|
|
722
|
+
projectName: this.appkey,
|
|
723
|
+
projectVersion: params.projectVersion,
|
|
724
|
+
logVersion: "v2",
|
|
725
|
+
body: params.body
|
|
726
|
+
};
|
|
727
|
+
if (params.logLevel !== void 0) payload["logLevel"] = params.logLevel;
|
|
728
|
+
if (params.logSource !== void 0) payload["logSource"] = params.logSource;
|
|
729
|
+
if (params.logType !== void 0) payload["logType"] = params.logType;
|
|
730
|
+
if (params.host !== void 0) payload["host"] = params.host;
|
|
731
|
+
try {
|
|
732
|
+
const res = await import_ky4.default.post(url, {
|
|
733
|
+
headers: { "Content-Type": "application/json" },
|
|
734
|
+
json: payload
|
|
735
|
+
}).json();
|
|
736
|
+
unwrapHeader(res);
|
|
737
|
+
} catch (err) {
|
|
738
|
+
throw toNhnCloudCliError(err);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
580
741
|
};
|
|
581
742
|
|
|
582
743
|
// src/commands/configure-verify.ts
|
|
@@ -985,11 +1146,249 @@ credentials.json \uC5D0 "secret": "<secretkey>" \uB97C \uCD94\uAC00\uD558\uC138\
|
|
|
985
1146
|
});
|
|
986
1147
|
});
|
|
987
1148
|
|
|
988
|
-
// src/commands/
|
|
1149
|
+
// src/commands/logncrash/send.ts
|
|
989
1150
|
var import_commander3 = require("commander");
|
|
1151
|
+
var import_node_fs = require("fs");
|
|
1152
|
+
var MAX_LOG_BYTES = 8 * 1024 * 1024;
|
|
1153
|
+
var VALID_LEVELS = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"];
|
|
1154
|
+
function resolveBody(opts) {
|
|
1155
|
+
if (opts.body !== void 0) return opts.body;
|
|
1156
|
+
if (opts.file !== void 0) {
|
|
1157
|
+
let stat;
|
|
1158
|
+
try {
|
|
1159
|
+
stat = (0, import_node_fs.statSync)(opts.file);
|
|
1160
|
+
} catch (e) {
|
|
1161
|
+
const reason = e.code ?? (e instanceof Error ? e.message : String(e));
|
|
1162
|
+
throw new NhnCloudCliError(`\uB85C\uADF8 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.file} (${reason})`, EXIT_PARAM_ERROR);
|
|
1163
|
+
}
|
|
1164
|
+
if (!stat.isFile()) {
|
|
1165
|
+
throw new NhnCloudCliError(`\uB85C\uADF8 \uD30C\uC77C\uC774 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${opts.file}`, EXIT_PARAM_ERROR);
|
|
1166
|
+
}
|
|
1167
|
+
if (stat.size > MAX_LOG_BYTES) {
|
|
1168
|
+
throw new NhnCloudCliError(
|
|
1169
|
+
`\uB85C\uADF8 \uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${stat.size} \uBC14\uC774\uD2B8 (\uD55C\uB3C4 ${MAX_LOG_BYTES} \uBC14\uC774\uD2B8).`,
|
|
1170
|
+
EXIT_PARAM_ERROR
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
return (0, import_node_fs.readFileSync)(opts.file, "utf-8");
|
|
1174
|
+
}
|
|
1175
|
+
if (!process.stdin.isTTY) {
|
|
1176
|
+
return (0, import_node_fs.readFileSync)(0, "utf-8");
|
|
1177
|
+
}
|
|
1178
|
+
throw new NhnCloudCliError(
|
|
1179
|
+
"\uB85C\uADF8 \uBCF8\uBB38\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. --body <text> \uB610\uB294 --file <path> \uB610\uB294 \uD45C\uC900\uC785\uB825(\uD30C\uC774\uD504)\uC73C\uB85C \uC804\uB2EC\uD558\uC138\uC694.",
|
|
1180
|
+
EXIT_PARAM_ERROR
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
var sendCommand = new import_commander3.Command("send").description("\uB85C\uADF8 \uD55C \uAC74\uC744 Log & Crash \uB85C \uC804\uC1A1\uD55C\uB2E4 (--body / --file / stdin)").option("--body <text>", "\uB85C\uADF8 \uBA54\uC2DC\uC9C0 \uBCF8\uBB38 (\uBBF8\uC9C0\uC815 \uC2DC --file \uB610\uB294 stdin)").option("--file <path>", "\uB85C\uADF8 \uBCF8\uBB38\uC744 \uC77D\uC744 \uD30C\uC77C \uACBD\uB85C").option("--level <level>", "\uB85C\uADF8 \uB808\uBCA8 (DEBUG/INFO/WARN/ERROR/FATAL)").option("--app-version <ver>", "projectVersion (\uBBF8\uC9C0\uC815 \uC2DC \uAE30\uBCF8 '1.0.0')").option("--source <source>", "logSource (collector \uAE30\uBCF8 'http')").option("--type <type>", "logType (collector \uAE30\uBCF8 'log')").option("--host <host>", "\uB85C\uADF8\uB97C \uBCF4\uB0B8 host \uC2DD\uBCC4").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
1184
|
+
const opts = cmd.optsWithGlobals();
|
|
1185
|
+
const body = resolveBody(opts);
|
|
1186
|
+
const bytes = Buffer.byteLength(body, "utf-8");
|
|
1187
|
+
if (bytes === 0) {
|
|
1188
|
+
throw new NhnCloudCliError("\uB85C\uADF8 \uBCF8\uBB38\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
|
|
1189
|
+
}
|
|
1190
|
+
if (bytes > MAX_LOG_BYTES) {
|
|
1191
|
+
throw new NhnCloudCliError(
|
|
1192
|
+
`\uB85C\uADF8 \uBCF8\uBB38\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${bytes} \uBC14\uC774\uD2B8 (\uD55C\uB3C4 ${MAX_LOG_BYTES} \uBC14\uC774\uD2B8).`,
|
|
1193
|
+
EXIT_PARAM_ERROR
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
let logLevel;
|
|
1197
|
+
if (opts.level !== void 0) {
|
|
1198
|
+
const upper = opts.level.toUpperCase();
|
|
1199
|
+
if (!VALID_LEVELS.includes(upper)) {
|
|
1200
|
+
throw new NhnCloudCliError(
|
|
1201
|
+
`--level \uC740 ${VALID_LEVELS.join("/")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${opts.level}).`,
|
|
1202
|
+
EXIT_PARAM_ERROR
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
logLevel = upper;
|
|
1206
|
+
}
|
|
1207
|
+
const profileName = await resolveProfileName(opts.profile);
|
|
1208
|
+
const cred = await getServiceCredential("logncrash", profileName);
|
|
1209
|
+
if (!cred.appkey) {
|
|
1210
|
+
throw new NhnCloudCliError(
|
|
1211
|
+
`profile "${profileName}" \uC758 logncrash \uC790\uACA9\uC99D\uBA85\uC5D0 appkey \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
|
|
1212
|
+
credentials.json \uC5D0 "appkey": "<appkey>" \uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
|
|
1213
|
+
EXIT_CONFIG_ERROR
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
const client = new LogncrashClient(cred.appkey);
|
|
1217
|
+
startSpinner("\uB85C\uADF8 \uC804\uC1A1 \uC911...");
|
|
1218
|
+
try {
|
|
1219
|
+
await client.send({
|
|
1220
|
+
body,
|
|
1221
|
+
projectVersion: opts.appVersion ?? "1.0.0",
|
|
1222
|
+
logLevel,
|
|
1223
|
+
logSource: opts.source,
|
|
1224
|
+
logType: opts.type,
|
|
1225
|
+
host: opts.host
|
|
1226
|
+
});
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
stopSpinner(false);
|
|
1229
|
+
throw err;
|
|
1230
|
+
}
|
|
1231
|
+
stopSpinner(true, "\uB85C\uADF8\uB97C \uC804\uC1A1\uD588\uC2B5\uB2C8\uB2E4.");
|
|
1232
|
+
if (opts.json) {
|
|
1233
|
+
process.stdout.write(JSON.stringify({ ok: true, bytes }) + "\n");
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// src/commands/logncrash/export.ts
|
|
1238
|
+
var import_commander4 = require("commander");
|
|
1239
|
+
var import_promises3 = require("fs/promises");
|
|
1240
|
+
var import_node_fs2 = require("fs");
|
|
1241
|
+
var import_node_crypto2 = require("crypto");
|
|
1242
|
+
var MAX_TOTAL = 1e5;
|
|
1243
|
+
function assertWritable(path, force) {
|
|
1244
|
+
if (force) return;
|
|
1245
|
+
try {
|
|
1246
|
+
(0, import_node_fs2.statSync)(path);
|
|
1247
|
+
} catch (e) {
|
|
1248
|
+
if (e.code === "ENOENT") return;
|
|
1249
|
+
const reason = e.code ?? (e instanceof Error ? e.message : String(e));
|
|
1250
|
+
throw new NhnCloudCliError(`--output \uACBD\uB85C\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${path} (${reason})`, EXIT_PARAM_ERROR);
|
|
1251
|
+
}
|
|
1252
|
+
throw new NhnCloudCliError(
|
|
1253
|
+
`--output \uB300\uC0C1\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${path}. \uB36E\uC5B4\uC4F0\uB824\uBA74 --force \uB97C \uC4F0\uC138\uC694.`,
|
|
1254
|
+
EXIT_PARAM_ERROR
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
var exportCommand = new import_commander4.Command("export").description("Log & Crash \uB85C\uADF8\uB97C scroll \uB85C \uC804\uCCB4 \uCD94\uCD9C\uD574 \uD30C\uC77C\uB85C \uC800\uC7A5 (\uB300\uB7C9 \uCD94\uCD9C)").option("--query <lucene>", "Lucene \uC9C8\uC758 \uBB38\uC790\uC5F4 (\uD544\uC218)").option("--from <time>", "\uAC80\uC0C9 \uC2DC\uC791: ISO8601 \uB610\uB294 \uC0C1\uB300\uC2DC\uAC04 (1h/30m/2d/now) (\uD544\uC218)").option("--to <time>", "\uAC80\uC0C9 \uB05D: ISO8601 \uB610\uB294 \uC0C1\uB300\uC2DC\uAC04 (\uD544\uC218)").option("--output <file>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C (\uD544\uC218)").option("--format <fmt>", "\uCD9C\uB825 \uD615\uC2DD: jsonl(\uAE30\uBCF8, \uD55C \uC904\uB2F9 \uD55C \uB85C\uADF8) \uB610\uB294 json(\uBC30\uC5F4)", "jsonl").option("--size <n>", "scroll \uD398\uC774\uC9C0 \uD06C\uAE30 (docs \uBC94\uC704 10~100, \uAE30\uBCF8 100)", "100").option("--force", "\uCD9C\uB825 \uD30C\uC77C\uC774 \uC788\uC73C\uBA74 \uB36E\uC5B4\uC4F4\uB2E4").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
1258
|
+
const opts = cmd.optsWithGlobals();
|
|
1259
|
+
if (!opts.query) {
|
|
1260
|
+
throw new NhnCloudCliError(`--query \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --query 'logType:"NORMAL"'`, EXIT_PARAM_ERROR);
|
|
1261
|
+
}
|
|
1262
|
+
if (!opts.from) {
|
|
1263
|
+
throw new NhnCloudCliError("--from \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --from 1h", EXIT_PARAM_ERROR);
|
|
1264
|
+
}
|
|
1265
|
+
if (!opts.to) {
|
|
1266
|
+
throw new NhnCloudCliError("--to \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --to now", EXIT_PARAM_ERROR);
|
|
1267
|
+
}
|
|
1268
|
+
if (!opts.output) {
|
|
1269
|
+
throw new NhnCloudCliError("--output \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --output logs.jsonl", EXIT_PARAM_ERROR);
|
|
1270
|
+
}
|
|
1271
|
+
const format = opts.format ?? "jsonl";
|
|
1272
|
+
if (format !== "jsonl" && format !== "json") {
|
|
1273
|
+
throw new NhnCloudCliError("--format \uC740 jsonl \uB610\uB294 json \uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
|
|
1274
|
+
}
|
|
1275
|
+
const sizeRaw = opts.size ?? "100";
|
|
1276
|
+
if (!/^[1-9]\d*$/.test(sizeRaw)) {
|
|
1277
|
+
throw new NhnCloudCliError("--size \uB294 \uC591\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (docs \uBC94\uC704 10~100).", EXIT_PARAM_ERROR);
|
|
1278
|
+
}
|
|
1279
|
+
const size = parseInt(sizeRaw, 10);
|
|
1280
|
+
if (size < 10 || size > 100) {
|
|
1281
|
+
throw new NhnCloudCliError("--size \uB294 10~100 \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4 (Log & Crash scroll pageSize \uD55C\uB3C4).", EXIT_PARAM_ERROR);
|
|
1282
|
+
}
|
|
1283
|
+
const fromIso = resolveTime(opts.from);
|
|
1284
|
+
const toIso = resolveTime(opts.to);
|
|
1285
|
+
assertSearchRange(fromIso, toIso);
|
|
1286
|
+
assertWritable(opts.output, opts.force ?? false);
|
|
1287
|
+
const profileName = await resolveProfileName(opts.profile);
|
|
1288
|
+
const cred = await getServiceCredential("logncrash", profileName);
|
|
1289
|
+
if (!cred.appkey) {
|
|
1290
|
+
throw new NhnCloudCliError(
|
|
1291
|
+
`profile "${profileName}" \uC758 logncrash \uC790\uACA9\uC99D\uBA85\uC5D0 appkey \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
|
|
1292
|
+
credentials.json \uC5D0 "appkey": "<appkey>" \uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
|
|
1293
|
+
EXIT_CONFIG_ERROR
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
if (!cred.secret) {
|
|
1297
|
+
throw new NhnCloudCliError(
|
|
1298
|
+
`profile "${profileName}" \uC758 logncrash \uC790\uACA9\uC99D\uBA85\uC5D0 secret \uC774 \uC5C6\uC2B5\uB2C8\uB2E4.
|
|
1299
|
+
credentials.json \uC5D0 "secret": "<secret>" \uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
|
|
1300
|
+
EXIT_CONFIG_ERROR
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
const client = new LogncrashClient(cred.appkey, cred.secret);
|
|
1304
|
+
const tmp = opts.output + "." + (0, import_node_crypto2.randomBytes)(4).toString("hex") + ".tmp";
|
|
1305
|
+
const stream = (0, import_node_fs2.createWriteStream)(tmp, { encoding: "utf-8" });
|
|
1306
|
+
const spinner = startSpinner("\uB85C\uADF8 \uCD94\uCD9C \uC911...");
|
|
1307
|
+
let count = 0;
|
|
1308
|
+
let total = 0;
|
|
1309
|
+
let first = true;
|
|
1310
|
+
const writePage = (data) => {
|
|
1311
|
+
for (const log of data) {
|
|
1312
|
+
if (count >= MAX_TOTAL) break;
|
|
1313
|
+
const json = JSON.stringify(log);
|
|
1314
|
+
stream.write(format === "json" ? first ? json : "," + json : json + "\n");
|
|
1315
|
+
first = false;
|
|
1316
|
+
count++;
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
try {
|
|
1320
|
+
if (format === "json") stream.write("[");
|
|
1321
|
+
let res = await client.scrollStart({ query: opts.query, from: fromIso, to: toIso, pageSize: size });
|
|
1322
|
+
total = res.totalItems;
|
|
1323
|
+
writePage(res.data);
|
|
1324
|
+
spinner.text = `\uB85C\uADF8 \uCD94\uCD9C \uC911... ${count}/${total}`;
|
|
1325
|
+
while (res.data.length > 0 && res.scrollKey && count < Math.min(total, MAX_TOTAL)) {
|
|
1326
|
+
res = await scrollNextOrExpire(client, res.scrollKey);
|
|
1327
|
+
writePage(res.data);
|
|
1328
|
+
spinner.text = `\uB85C\uADF8 \uCD94\uCD9C \uC911... ${count}/${total}`;
|
|
1329
|
+
}
|
|
1330
|
+
if (format === "json") stream.write("]\n");
|
|
1331
|
+
await new Promise((resolve, reject) => {
|
|
1332
|
+
stream.once("error", reject);
|
|
1333
|
+
stream.end(resolve);
|
|
1334
|
+
});
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
stopSpinner(false);
|
|
1337
|
+
stream.destroy();
|
|
1338
|
+
await (0, import_promises3.rm)(tmp, { force: true }).catch(() => {
|
|
1339
|
+
});
|
|
1340
|
+
throw err;
|
|
1341
|
+
}
|
|
1342
|
+
stopSpinner(true, `${count}\uAC74 \uCD94\uCD9C \uC644\uB8CC`);
|
|
1343
|
+
try {
|
|
1344
|
+
await (0, import_promises3.rename)(tmp, opts.output);
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
await (0, import_promises3.rm)(tmp, { force: true }).catch(() => {
|
|
1347
|
+
});
|
|
1348
|
+
const reason = err.code ?? (err instanceof Error ? err.message : String(err));
|
|
1349
|
+
throw new NhnCloudCliError(`\uCD9C\uB825 \uD30C\uC77C\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.output} (${reason})`, EXIT_PARAM_ERROR);
|
|
1350
|
+
}
|
|
1351
|
+
if (count >= MAX_TOTAL && total > MAX_TOTAL) {
|
|
1352
|
+
process.stderr.write(
|
|
1353
|
+
`\uACBD\uACE0: \uC804\uCCB4 ${total}\uAC74 \uC911 \uC0C1\uD55C ${MAX_TOTAL}\uAC74\uAE4C\uC9C0\uB9CC \uCD94\uCD9C\uD588\uC2B5\uB2C8\uB2E4. \uAC80\uC0C9 \uBC94\uC704\uB97C \uC881\uD600 \uB098\uB220 \uCD94\uCD9C\uD558\uC138\uC694.
|
|
1354
|
+
`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
process.stderr.write(`${opts.output} \uC5D0 ${count}\uAC74 \uC800\uC7A5
|
|
1358
|
+
`);
|
|
1359
|
+
});
|
|
1360
|
+
async function scrollNextOrExpire(client, scrollKey) {
|
|
1361
|
+
try {
|
|
1362
|
+
return await client.scrollNext(scrollKey);
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
if (err instanceof NhnCloudCliError && err.exitCode === EXIT_API_ERROR) {
|
|
1365
|
+
throw new NhnCloudCliError(
|
|
1366
|
+
`scroll \uB2E4\uC74C \uD398\uC774\uC9C0 \uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (\uC6D0\uC778: ${err.message}). scrollKey \uB9CC\uB8CC(\uC720\uD6A8 1\uBD84)\uC77C \uC218 \uC788\uC73C\uB2C8, \uB9CC\uB8CC\uB77C\uBA74 \uAC80\uC0C9 \uBC94\uC704\uB97C \uC881\uD788\uAC70\uB098 --size \uB97C \uD0A4\uC6CC \uD398\uC774\uC9C0 \uC218\uB97C \uC904\uC778 \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`,
|
|
1367
|
+
EXIT_API_ERROR
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
throw err;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/commands/deploy/run.ts
|
|
1375
|
+
var import_commander5 = require("commander");
|
|
990
1376
|
|
|
991
1377
|
// src/services/deploy/client.ts
|
|
992
1378
|
var import_ky5 = __toESM(require("ky"));
|
|
1379
|
+
function isBinaryGroup(val) {
|
|
1380
|
+
if (typeof val !== "object" || val === null) return false;
|
|
1381
|
+
const obj = val;
|
|
1382
|
+
const keyType = typeof obj["key"];
|
|
1383
|
+
return (keyType === "number" || keyType === "string") && typeof obj["name"] === "string";
|
|
1384
|
+
}
|
|
1385
|
+
function isBinary(val) {
|
|
1386
|
+
if (typeof val !== "object" || val === null) return false;
|
|
1387
|
+
const obj = val;
|
|
1388
|
+
const binaryKeyType = typeof obj["binaryKey"];
|
|
1389
|
+
const binarySizeType = typeof obj["binarySize"];
|
|
1390
|
+
return (binaryKeyType === "number" || binaryKeyType === "string") && (binarySizeType === "number" || binarySizeType === "string");
|
|
1391
|
+
}
|
|
993
1392
|
var SYNC_TIMEOUT_MS = 6e5;
|
|
994
1393
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
995
1394
|
var DeployClient = class {
|
|
@@ -1010,7 +1409,7 @@ var DeployClient = class {
|
|
|
1010
1409
|
* - async=false(기본) 일 때 서버가 완료까지 응답을 보류하므로 ky timeout 을 600s 로 설정한다.
|
|
1011
1410
|
*/
|
|
1012
1411
|
async run(params) {
|
|
1013
|
-
const url = `${this.baseUrl}/api/v2.1/projects/${params.appKey}/artifacts/${params.artifactId}/server-group/${params.serverGroupId}/deploy`;
|
|
1412
|
+
const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(params.appKey)}/artifacts/${encodeURIComponent(params.artifactId)}/server-group/${encodeURIComponent(params.serverGroupId)}/deploy`;
|
|
1014
1413
|
const isAsync = params.async ?? false;
|
|
1015
1414
|
const payload = {
|
|
1016
1415
|
concurrentNum: params.concurrentNum ?? 1,
|
|
@@ -1041,7 +1440,7 @@ var DeployClient = class {
|
|
|
1041
1440
|
* 아티팩트 목록을 조회한다.
|
|
1042
1441
|
*/
|
|
1043
1442
|
async artifacts(appKey) {
|
|
1044
|
-
const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts`;
|
|
1443
|
+
const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts`;
|
|
1045
1444
|
try {
|
|
1046
1445
|
const res = await import_ky5.default.get(url, {
|
|
1047
1446
|
headers: this.authHeaders(),
|
|
@@ -1057,7 +1456,7 @@ var DeployClient = class {
|
|
|
1057
1456
|
* 서버그룹 목록을 조회한다.
|
|
1058
1457
|
*/
|
|
1059
1458
|
async serverGroups(appKey, artifactId) {
|
|
1060
|
-
const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts/${artifactId}/server-groups`;
|
|
1459
|
+
const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/server-groups`;
|
|
1061
1460
|
try {
|
|
1062
1461
|
const res = await import_ky5.default.get(url, {
|
|
1063
1462
|
headers: this.authHeaders(),
|
|
@@ -1073,7 +1472,7 @@ var DeployClient = class {
|
|
|
1073
1472
|
* 배포 이력을 조회한다.
|
|
1074
1473
|
*/
|
|
1075
1474
|
async histories(appKey, artifactId) {
|
|
1076
|
-
const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts/${artifactId}/deploy-histories`;
|
|
1475
|
+
const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/deploy-histories`;
|
|
1077
1476
|
try {
|
|
1078
1477
|
const res = await import_ky5.default.get(url, {
|
|
1079
1478
|
headers: this.authHeaders(),
|
|
@@ -1085,44 +1484,169 @@ var DeployClient = class {
|
|
|
1085
1484
|
throw toNhnCloudCliError(err);
|
|
1086
1485
|
}
|
|
1087
1486
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
async
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
appKey,
|
|
1112
|
-
artifactId,
|
|
1113
|
-
serverGroupId,
|
|
1114
|
-
scenarioIds,
|
|
1115
|
-
targetHosts: opts.targetHosts,
|
|
1116
|
-
concurrentNum: parseInt(opts.concurrent ?? "1", 10),
|
|
1117
|
-
nextWhenFail: opts.nextWhenFail ?? false,
|
|
1118
|
-
deployNote: opts.note,
|
|
1119
|
-
async: opts.async ?? false
|
|
1120
|
-
});
|
|
1121
|
-
} catch (err) {
|
|
1122
|
-
stopSpinner(false);
|
|
1123
|
-
throw err;
|
|
1487
|
+
/**
|
|
1488
|
+
* 바이너리 그룹 목록을 조회한다.
|
|
1489
|
+
*/
|
|
1490
|
+
async binaryGroups(appKey, artifactId) {
|
|
1491
|
+
const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-groups`;
|
|
1492
|
+
try {
|
|
1493
|
+
const res = await import_ky5.default.get(url, {
|
|
1494
|
+
headers: this.authHeaders(),
|
|
1495
|
+
retry: 0,
|
|
1496
|
+
timeout: DEFAULT_TIMEOUT_MS
|
|
1497
|
+
}).json();
|
|
1498
|
+
const body = unwrap(res);
|
|
1499
|
+
const list = body.binaryGroups;
|
|
1500
|
+
if (!Array.isArray(list) || !list.every(isBinaryGroup)) {
|
|
1501
|
+
throw new NhnCloudCliError(
|
|
1502
|
+
"binary-groups \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 binaryGroups \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1503
|
+
EXIT_API_ERROR
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
return list;
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
throw toNhnCloudCliError(err);
|
|
1509
|
+
}
|
|
1124
1510
|
}
|
|
1125
|
-
|
|
1511
|
+
/**
|
|
1512
|
+
* 특정 바이너리 그룹의 바이너리 목록을 조회한다.
|
|
1513
|
+
* pageNum/pageSize/sortKey/sortDirection 은 NHN docs 의 쿼리 파라미터로 그대로 전달한다.
|
|
1514
|
+
*/
|
|
1515
|
+
async binaries(appKey, artifactId, binaryGroupKey, params = {}) {
|
|
1516
|
+
const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-groups/${binaryGroupKey}/binaries`;
|
|
1517
|
+
const searchParams = {};
|
|
1518
|
+
if (params.pageNum !== void 0) searchParams["pageNum"] = params.pageNum;
|
|
1519
|
+
if (params.pageSize !== void 0) searchParams["pageSize"] = params.pageSize;
|
|
1520
|
+
if (params.sortKey !== void 0) searchParams["sortKey"] = params.sortKey;
|
|
1521
|
+
if (params.sortDirection !== void 0) searchParams["sortDirection"] = params.sortDirection;
|
|
1522
|
+
try {
|
|
1523
|
+
const res = await import_ky5.default.get(url, {
|
|
1524
|
+
headers: this.authHeaders(),
|
|
1525
|
+
searchParams,
|
|
1526
|
+
retry: 0,
|
|
1527
|
+
timeout: DEFAULT_TIMEOUT_MS
|
|
1528
|
+
}).json();
|
|
1529
|
+
const body = unwrap(res);
|
|
1530
|
+
const list = body.binaries;
|
|
1531
|
+
if (!Array.isArray(list) || !list.every(isBinary)) {
|
|
1532
|
+
throw new NhnCloudCliError(
|
|
1533
|
+
"binaries \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 binaries \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1534
|
+
EXIT_API_ERROR
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
const tc = body.totalCount;
|
|
1538
|
+
const totalCount = typeof tc === "number" ? tc : typeof tc === "string" && /^\d+$/.test(tc) ? Number(tc) : list.length;
|
|
1539
|
+
return { totalCount, binaries: list };
|
|
1540
|
+
} catch (err) {
|
|
1541
|
+
throw toNhnCloudCliError(err);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* 바이너리를 multipart/form-data 로 업로드한다.
|
|
1546
|
+
*
|
|
1547
|
+
* 신규 전송 경로 — 기존 메서드는 ky `json:`(JSON body) 만 쓴다 (ADR-015).
|
|
1548
|
+
* - 파일 파트(binaryFile)는 command 에서 statSync 가드 후 읽은 Buffer 를 Blob 으로 감싼다.
|
|
1549
|
+
* - Content-Type 은 수동으로 박지 않는다 — ky 가 FormData 에서 multipart boundary 를 자동 설정한다.
|
|
1550
|
+
*
|
|
1551
|
+
* ⚠️ 실측 pending — 수동 QA 로 확정 필요:
|
|
1552
|
+
* - endpoint 경로 세그먼트 단/복수(`binary-group` vs `binary-groups`) — 404 시 복수형으로 교체.
|
|
1553
|
+
* - 응답 binaryKey 타입(number|string) — 코드는 둘 다 수용 후 Number() 정규화.
|
|
1554
|
+
*/
|
|
1555
|
+
async uploadBinary(params) {
|
|
1556
|
+
const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(params.appKey)}/artifacts/${encodeURIComponent(params.artifactId)}/binary-group/${params.binaryGroupKey}`;
|
|
1557
|
+
const form = new FormData();
|
|
1558
|
+
const blob = new Blob([new Uint8Array(params.fileBuffer)]);
|
|
1559
|
+
form.append("binaryFile", blob, params.fileName);
|
|
1560
|
+
form.append("applicationType", params.applicationType);
|
|
1561
|
+
if (params.description !== void 0) {
|
|
1562
|
+
form.append("description", params.description);
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
const res = await import_ky5.default.post(url, {
|
|
1566
|
+
headers: this.authHeaders(),
|
|
1567
|
+
// 인증 헤더만 — multipart boundary 는 ky 가 자동 설정
|
|
1568
|
+
body: form,
|
|
1569
|
+
retry: 0,
|
|
1570
|
+
timeout: SYNC_TIMEOUT_MS
|
|
1571
|
+
// 업로드는 파일 크기에 따라 길 수 있어 긴 timeout
|
|
1572
|
+
}).json();
|
|
1573
|
+
const body = unwrap(res);
|
|
1574
|
+
const normalizedKey = Number(body.binaryKey);
|
|
1575
|
+
if (typeof body.downloadUrl !== "string" || !Number.isFinite(normalizedKey)) {
|
|
1576
|
+
throw new NhnCloudCliError(
|
|
1577
|
+
"upload \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 downloadUrl/binaryKey \uB204\uB77D \uB610\uB294 \uBE44\uC22B\uC790.",
|
|
1578
|
+
EXIT_API_ERROR
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
return { downloadUrl: body.downloadUrl, binaryKey: normalizedKey };
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
throw toNhnCloudCliError(err);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* 바이너리를 다운로드해 내용(Buffer)을 반환한다.
|
|
1588
|
+
*
|
|
1589
|
+
* 신규 수신 경로 — 응답이 봉투 JSON 이 아니라 파일 바이너리 스트림이다 (ADR-015).
|
|
1590
|
+
* 다른 메서드처럼 .json()/unwrap 을 쓰면 바이너리를 JSON 으로 파싱하다 깨진다 —
|
|
1591
|
+
* 반드시 .arrayBuffer() 로 받는다. 성공/실패는 HTTP status(ky throwHttpErrors)로만 판정.
|
|
1592
|
+
* 파일 쓰기는 command 가 담당한다 (client 는 내용만 반환 — 테스트 용이).
|
|
1593
|
+
*
|
|
1594
|
+
* ⚠️ 실측 pending — 수동 QA round-trip 으로 확정 필요:
|
|
1595
|
+
* - endpoint 단/복수(`binary-group` vs `binary-groups`) — 404 시 복수형으로 교체.
|
|
1596
|
+
* - 응답이 raw 바이너리인지 downloadUrl JSON 인지 — QA step 5 diff 로 확인.
|
|
1597
|
+
*/
|
|
1598
|
+
async downloadBinary(appKey, artifactId, binaryGroupKey, binaryKey) {
|
|
1599
|
+
const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-group/${binaryGroupKey}/binaries/${binaryKey}`;
|
|
1600
|
+
try {
|
|
1601
|
+
const ab = await import_ky5.default.get(url, {
|
|
1602
|
+
headers: this.authHeaders(),
|
|
1603
|
+
retry: 0,
|
|
1604
|
+
timeout: SYNC_TIMEOUT_MS
|
|
1605
|
+
// 큰 파일 다운로드 — 긴 timeout
|
|
1606
|
+
}).arrayBuffer();
|
|
1607
|
+
return Buffer.from(ab);
|
|
1608
|
+
} catch (err) {
|
|
1609
|
+
throw toNhnCloudCliError(err);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
// src/commands/deploy/helpers.ts
|
|
1615
|
+
async function createDeployClient(profileOpt) {
|
|
1616
|
+
const profileName = await resolveProfileName(profileOpt);
|
|
1617
|
+
const uak = await getUserAccessKey(profileName);
|
|
1618
|
+
const accessToken = await getAccessToken(profileName, uak.id, uak.secret);
|
|
1619
|
+
return { client: new DeployClient(accessToken), profileName };
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/commands/deploy/run.ts
|
|
1623
|
+
var runCommand = new import_commander5.Command("run").description("\uBC30\uD3EC\uB97C \uC2E4\uD589\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--server-group-id <id>", "target \uC758 serverGroupId override").option("--scenario-ids <csv>", "target \uC758 scenarioIds override").option("--target-hosts <csv>", "\uB300\uC0C1 \uD638\uC2A4\uD2B8 (\uC0DD\uB7B5 \uC2DC \uC11C\uBC84\uADF8\uB8F9 \uC804\uCCB4)").option("--concurrent <n>", "\uBCD1\uB82C \uBC30\uD3EC \uC218 (\uAE30\uBCF8 1)", "1").option("--next-when-fail", "\uC2DC\uB098\uB9AC\uC624 \uC2E4\uD328 \uC2DC\uC5D0\uB3C4 \uC9C4\uD589").option("--note <s>", "\uBC30\uD3EC \uBA54\uBAA8").option("--async", "\uC989\uC2DC \uBC18\uD658 (\uAE30\uBCF8\uC740 \uC644\uB8CC \uB300\uAE30)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
|
|
1624
|
+
const opts = cmd.optsWithGlobals();
|
|
1625
|
+
const target = await getDeployTarget(targetName);
|
|
1626
|
+
const appKey = opts.appKey ?? target.appKey;
|
|
1627
|
+
const artifactId = opts.artifactId ?? target.artifactId;
|
|
1628
|
+
const serverGroupId = opts.serverGroupId ?? target.serverGroupId;
|
|
1629
|
+
const scenarioIds = opts.scenarioIds ?? target.scenarioIds;
|
|
1630
|
+
const { client } = await createDeployClient(opts.profile);
|
|
1631
|
+
startSpinner("\uBC30\uD3EC \uC2E4\uD589 \uC911...");
|
|
1632
|
+
let result;
|
|
1633
|
+
try {
|
|
1634
|
+
result = await client.run({
|
|
1635
|
+
appKey,
|
|
1636
|
+
artifactId,
|
|
1637
|
+
serverGroupId,
|
|
1638
|
+
scenarioIds,
|
|
1639
|
+
targetHosts: opts.targetHosts,
|
|
1640
|
+
concurrentNum: parseInt(opts.concurrent ?? "1", 10),
|
|
1641
|
+
nextWhenFail: opts.nextWhenFail ?? false,
|
|
1642
|
+
deployNote: opts.note,
|
|
1643
|
+
async: opts.async ?? false
|
|
1644
|
+
});
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
stopSpinner(false);
|
|
1647
|
+
throw err;
|
|
1648
|
+
}
|
|
1649
|
+
stopSpinner(true);
|
|
1126
1650
|
output(opts, {
|
|
1127
1651
|
headers: ["key", "value"],
|
|
1128
1652
|
rows: Object.entries(result).map(([k, v]) => [k, String(v ?? "")]),
|
|
@@ -1132,8 +1656,8 @@ var runCommand = new import_commander3.Command("run").description("\uBC30\uD3EC\
|
|
|
1132
1656
|
});
|
|
1133
1657
|
|
|
1134
1658
|
// src/commands/deploy/artifacts.ts
|
|
1135
|
-
var
|
|
1136
|
-
var artifactsCommand = new
|
|
1659
|
+
var import_commander6 = require("commander");
|
|
1660
|
+
var artifactsCommand = new import_commander6.Command("artifacts").description("\uC544\uD2F0\uD329\uD2B8 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("[target]", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984 (--app-key \uB85C \uB300\uCCB4 \uAC00\uB2A5)").option("--app-key <k>", "appKey \uC9C1\uC811 \uC9C0\uC815 (target \uC5C6\uC774 \uC0AC\uC6A9 \uAC00\uB2A5)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
|
|
1137
1661
|
const opts = cmd.optsWithGlobals();
|
|
1138
1662
|
let appKey;
|
|
1139
1663
|
if (opts.appKey) {
|
|
@@ -1173,8 +1697,8 @@ var artifactsCommand = new import_commander4.Command("artifacts").description("\
|
|
|
1173
1697
|
});
|
|
1174
1698
|
|
|
1175
1699
|
// src/commands/deploy/server-groups.ts
|
|
1176
|
-
var
|
|
1177
|
-
var serverGroupsCommand = new
|
|
1700
|
+
var import_commander7 = require("commander");
|
|
1701
|
+
var serverGroupsCommand = new import_commander7.Command("server-groups").description("\uC11C\uBC84\uADF8\uB8F9 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
|
|
1178
1702
|
const opts = cmd.optsWithGlobals();
|
|
1179
1703
|
const target = await getDeployTarget(targetName);
|
|
1180
1704
|
const appKey = opts.appKey ?? target.appKey;
|
|
@@ -1198,8 +1722,8 @@ var serverGroupsCommand = new import_commander5.Command("server-groups").descrip
|
|
|
1198
1722
|
});
|
|
1199
1723
|
|
|
1200
1724
|
// src/commands/deploy/histories.ts
|
|
1201
|
-
var
|
|
1202
|
-
var historiesCommand = new
|
|
1725
|
+
var import_commander8 = require("commander");
|
|
1726
|
+
var historiesCommand = new import_commander8.Command("histories").description("\uBC30\uD3EC \uC774\uB825\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
|
|
1203
1727
|
const opts = cmd.optsWithGlobals();
|
|
1204
1728
|
const target = await getDeployTarget(targetName);
|
|
1205
1729
|
const appKey = opts.appKey ?? target.appKey;
|
|
@@ -1222,8 +1746,232 @@ var historiesCommand = new import_commander6.Command("histories").description("\
|
|
|
1222
1746
|
});
|
|
1223
1747
|
});
|
|
1224
1748
|
|
|
1749
|
+
// src/commands/deploy/binary-groups.ts
|
|
1750
|
+
var import_commander9 = require("commander");
|
|
1751
|
+
var binaryGroupsCommand = new import_commander9.Command("binary-groups").description("\uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
|
|
1752
|
+
const opts = cmd.optsWithGlobals();
|
|
1753
|
+
const target = await getDeployTarget(targetName);
|
|
1754
|
+
const appKey = opts.appKey ?? target.appKey;
|
|
1755
|
+
const artifactId = opts.artifactId ?? target.artifactId;
|
|
1756
|
+
const { client } = await createDeployClient(opts.profile);
|
|
1757
|
+
startSpinner("\uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
1758
|
+
let groups;
|
|
1759
|
+
try {
|
|
1760
|
+
groups = await client.binaryGroups(appKey, artifactId);
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
stopSpinner(false);
|
|
1763
|
+
throw err;
|
|
1764
|
+
}
|
|
1765
|
+
stopSpinner(true);
|
|
1766
|
+
output(opts, {
|
|
1767
|
+
headers: ["key", "name", "regionCode", "createDate", "description"],
|
|
1768
|
+
// 가드는 key·name 만 검증 — 나머지 필드는 누락 시 "undefined" 가 박히지 않게 ?? "" 방어.
|
|
1769
|
+
rows: groups.map((g) => [
|
|
1770
|
+
String(g.key),
|
|
1771
|
+
g.name ?? "",
|
|
1772
|
+
g.regionCode ?? "",
|
|
1773
|
+
g.createDate ?? "",
|
|
1774
|
+
g.description ?? ""
|
|
1775
|
+
]),
|
|
1776
|
+
raw: groups,
|
|
1777
|
+
// ids 에 key 를 넣어 --quiet 시 그룹 key 만 출력 → binaries --binary-group 에 파이프 가능
|
|
1778
|
+
ids: groups.map((g) => String(g.key))
|
|
1779
|
+
});
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// src/commands/deploy/binaries.ts
|
|
1783
|
+
var import_commander10 = require("commander");
|
|
1784
|
+
function parsePositiveInt(value, flag) {
|
|
1785
|
+
if (value === void 0) return void 0;
|
|
1786
|
+
if (!/^[1-9]\d*$/.test(value)) {
|
|
1787
|
+
throw new NhnCloudCliError(
|
|
1788
|
+
`${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${JSON.stringify(value)}).`,
|
|
1789
|
+
EXIT_PARAM_ERROR
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
return Number(value);
|
|
1793
|
+
}
|
|
1794
|
+
var binariesCommand = new import_commander10.Command("binaries").description("\uD2B9\uC815 \uBC14\uC774\uB108\uB9AC \uADF8\uB8F9\uC758 \uBC14\uC774\uB108\uB9AC \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").requiredOption("--binary-group <key>", "\uC870\uD68C\uD560 \uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 key (binary-groups \uB85C \uD655\uC778)").option("--page-num <n>", "\uD398\uC774\uC9C0 \uBC88\uD638 (1 \uC774\uC0C1)").option("--page-size <n>", "\uD398\uC774\uC9C0 \uD06C\uAE30 (1 \uC774\uC0C1)").option("--sort-key <k>", "\uC815\uB82C \uAE30\uC900 (\uC608: UPLOAD_DATE)").option("--sort-direction <d>", "\uC815\uB82C \uBC29\uD5A5 (\uC608: DESC)").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
|
|
1795
|
+
const opts = cmd.optsWithGlobals();
|
|
1796
|
+
const binaryGroupKey = parsePositiveInt(opts.binaryGroup, "--binary-group");
|
|
1797
|
+
if (binaryGroupKey === void 0) {
|
|
1798
|
+
throw new NhnCloudCliError("--binary-group \uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
|
|
1799
|
+
}
|
|
1800
|
+
const pageNum = parsePositiveInt(opts.pageNum, "--page-num");
|
|
1801
|
+
const pageSize = parsePositiveInt(opts.pageSize, "--page-size");
|
|
1802
|
+
const target = await getDeployTarget(targetName);
|
|
1803
|
+
const appKey = opts.appKey ?? target.appKey;
|
|
1804
|
+
const artifactId = opts.artifactId ?? target.artifactId;
|
|
1805
|
+
const { client } = await createDeployClient(opts.profile);
|
|
1806
|
+
startSpinner("\uBC14\uC774\uB108\uB9AC \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
1807
|
+
let result;
|
|
1808
|
+
try {
|
|
1809
|
+
result = await client.binaries(appKey, artifactId, binaryGroupKey, {
|
|
1810
|
+
pageNum,
|
|
1811
|
+
pageSize,
|
|
1812
|
+
sortKey: opts.sortKey,
|
|
1813
|
+
sortDirection: opts.sortDirection
|
|
1814
|
+
});
|
|
1815
|
+
} catch (err) {
|
|
1816
|
+
stopSpinner(false);
|
|
1817
|
+
throw err;
|
|
1818
|
+
}
|
|
1819
|
+
stopSpinner(true);
|
|
1820
|
+
output(opts, {
|
|
1821
|
+
headers: ["binaryKey", "version", "binaryName", "size(bytes)", "uploadDate", "uploader"],
|
|
1822
|
+
// 가드는 binaryKey·binarySize 만 검증 — 나머지 필드는 응답에서 누락 시 "undefined" 가
|
|
1823
|
+
// 표에 박히지 않게 ?? "" 로 방어한다 (타입 정합성 실측은 후속 이슈).
|
|
1824
|
+
rows: result.binaries.map((b) => [
|
|
1825
|
+
String(b.binaryKey),
|
|
1826
|
+
b.version ?? "",
|
|
1827
|
+
b.binaryName ?? "",
|
|
1828
|
+
String(b.binarySize),
|
|
1829
|
+
b.uploadDate ?? "",
|
|
1830
|
+
b.uploader ?? ""
|
|
1831
|
+
]),
|
|
1832
|
+
// raw 에 totalCount 포함 → --json 으로 페이지 정보 확인 가능
|
|
1833
|
+
raw: result,
|
|
1834
|
+
ids: result.binaries.map((b) => String(b.binaryKey))
|
|
1835
|
+
});
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
// src/commands/deploy/upload.ts
|
|
1839
|
+
var import_commander11 = require("commander");
|
|
1840
|
+
var import_node_fs3 = require("fs");
|
|
1841
|
+
var import_node_path3 = require("path");
|
|
1842
|
+
var MAX_UPLOAD_BYTES = 512 * 1024 * 1024;
|
|
1843
|
+
function parsePositiveInt2(value, flag) {
|
|
1844
|
+
if (value === void 0) return void 0;
|
|
1845
|
+
if (!/^[1-9]\d*$/.test(value)) {
|
|
1846
|
+
throw new NhnCloudCliError(
|
|
1847
|
+
`${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${JSON.stringify(value)}).`,
|
|
1848
|
+
EXIT_PARAM_ERROR
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
return Number(value);
|
|
1852
|
+
}
|
|
1853
|
+
var uploadCommand = new import_commander11.Command("upload").description("\uB85C\uCEEC \uD30C\uC77C\uC744 \uBC14\uC774\uB108\uB9AC \uADF8\uB8F9\uC5D0 \uC5C5\uB85C\uB4DC\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").requiredOption("--file <path>", "\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uACBD\uB85C").requiredOption("--binary-group <key>", "\uC5C5\uB85C\uB4DC \uB300\uC0C1 \uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 key (binary-groups \uB85C \uD655\uC778)").option("--application-type <type>", "applicationType (\uC608: server)", "server").option("--description <text>", "\uBC14\uC774\uB108\uB9AC \uC124\uBA85").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
|
|
1854
|
+
const opts = cmd.optsWithGlobals();
|
|
1855
|
+
const binaryGroupKey = parsePositiveInt2(opts.binaryGroup, "--binary-group");
|
|
1856
|
+
if (binaryGroupKey === void 0) {
|
|
1857
|
+
throw new NhnCloudCliError("--binary-group \uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
|
|
1858
|
+
}
|
|
1859
|
+
const filePath = opts.file;
|
|
1860
|
+
let stat;
|
|
1861
|
+
try {
|
|
1862
|
+
stat = (0, import_node_fs3.statSync)(filePath);
|
|
1863
|
+
} catch (e) {
|
|
1864
|
+
const reason = e.code ?? (e instanceof Error ? e.message : String(e));
|
|
1865
|
+
throw new NhnCloudCliError(
|
|
1866
|
+
`--file \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath} (${reason})`,
|
|
1867
|
+
EXIT_PARAM_ERROR
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
if (!stat.isFile()) {
|
|
1871
|
+
throw new NhnCloudCliError(`--file \uC774 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${filePath}`, EXIT_PARAM_ERROR);
|
|
1872
|
+
}
|
|
1873
|
+
if (stat.size > MAX_UPLOAD_BYTES) {
|
|
1874
|
+
throw new NhnCloudCliError(
|
|
1875
|
+
`--file \uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${stat.size} \uBC14\uC774\uD2B8). \uC5C5\uB85C\uB4DC \uD55C\uB3C4 ${MAX_UPLOAD_BYTES} \uBC14\uC774\uD2B8.`,
|
|
1876
|
+
EXIT_PARAM_ERROR
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
const fileBuffer = (0, import_node_fs3.readFileSync)(filePath);
|
|
1880
|
+
const fileName = (0, import_node_path3.basename)(filePath);
|
|
1881
|
+
const target = await getDeployTarget(targetName);
|
|
1882
|
+
const appKey = opts.appKey ?? target.appKey;
|
|
1883
|
+
const artifactId = opts.artifactId ?? target.artifactId;
|
|
1884
|
+
const { client } = await createDeployClient(opts.profile);
|
|
1885
|
+
startSpinner("\uBC14\uC774\uB108\uB9AC \uC5C5\uB85C\uB4DC \uC911...");
|
|
1886
|
+
let result;
|
|
1887
|
+
try {
|
|
1888
|
+
result = await client.uploadBinary({
|
|
1889
|
+
appKey,
|
|
1890
|
+
artifactId,
|
|
1891
|
+
binaryGroupKey,
|
|
1892
|
+
fileBuffer,
|
|
1893
|
+
fileName,
|
|
1894
|
+
applicationType: opts.applicationType,
|
|
1895
|
+
// Commander 옵션 기본값 "server" 가 SSOT — dead fallback 제거
|
|
1896
|
+
description: opts.description
|
|
1897
|
+
});
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
stopSpinner(false);
|
|
1900
|
+
throw err;
|
|
1901
|
+
}
|
|
1902
|
+
stopSpinner(true);
|
|
1903
|
+
output(opts, {
|
|
1904
|
+
headers: ["field", "value"],
|
|
1905
|
+
rows: [
|
|
1906
|
+
["binaryKey", String(result.binaryKey)],
|
|
1907
|
+
["downloadUrl", result.downloadUrl]
|
|
1908
|
+
],
|
|
1909
|
+
raw: result,
|
|
1910
|
+
ids: [String(result.binaryKey)]
|
|
1911
|
+
});
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
// src/commands/deploy/download.ts
|
|
1915
|
+
var import_commander12 = require("commander");
|
|
1916
|
+
var import_node_fs4 = require("fs");
|
|
1917
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
1918
|
+
function parsePositiveInt3(value, flag) {
|
|
1919
|
+
if (value === void 0) return void 0;
|
|
1920
|
+
if (!/^[1-9]\d*$/.test(value)) {
|
|
1921
|
+
throw new NhnCloudCliError(
|
|
1922
|
+
`${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${JSON.stringify(value)}).`,
|
|
1923
|
+
EXIT_PARAM_ERROR
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
return Number(value);
|
|
1927
|
+
}
|
|
1928
|
+
function assertWritable2(path, force) {
|
|
1929
|
+
if (force) return;
|
|
1930
|
+
try {
|
|
1931
|
+
(0, import_node_fs4.statSync)(path);
|
|
1932
|
+
} catch (e) {
|
|
1933
|
+
if (e.code === "ENOENT") return;
|
|
1934
|
+
const reason = e.code ?? (e instanceof Error ? e.message : String(e));
|
|
1935
|
+
throw new NhnCloudCliError(
|
|
1936
|
+
`-o \uACBD\uB85C\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${path} (${reason})`,
|
|
1937
|
+
EXIT_PARAM_ERROR
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
throw new NhnCloudCliError(
|
|
1941
|
+
`-o \uB300\uC0C1\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${path}. \uB36E\uC5B4\uC4F0\uB824\uBA74 --force \uB97C \uC4F0\uC138\uC694.`,
|
|
1942
|
+
EXIT_PARAM_ERROR
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
var downloadCommand = new import_commander12.Command("download").description("\uBC14\uC774\uB108\uB9AC\uB97C \uB85C\uCEEC \uD30C\uC77C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").requiredOption("--binary-group <key>", "\uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 key (binary-groups \uB85C \uD655\uC778)").requiredOption("--binary-key <key>", "\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uBC14\uC774\uB108\uB9AC key (binaries \uB610\uB294 upload \uB85C \uD655\uC778)").requiredOption("-o, --output <file>", "\uC800\uC7A5\uD560 \uD30C\uC77C \uACBD\uB85C").option("--force", "\uB300\uC0C1 \uD30C\uC77C\uC774 \uC788\uC73C\uBA74 \uB36E\uC5B4\uC4F4\uB2E4").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
|
|
1946
|
+
const opts = cmd.optsWithGlobals();
|
|
1947
|
+
const binaryGroupKey = parsePositiveInt3(opts.binaryGroup, "--binary-group");
|
|
1948
|
+
const binaryKey = parsePositiveInt3(opts.binaryKey, "--binary-key");
|
|
1949
|
+
if (binaryGroupKey === void 0 || binaryKey === void 0) {
|
|
1950
|
+
throw new NhnCloudCliError("--binary-group / --binary-key \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
|
|
1951
|
+
}
|
|
1952
|
+
const outPath = opts.output;
|
|
1953
|
+
assertWritable2(outPath, opts.force ?? false);
|
|
1954
|
+
const target = await getDeployTarget(targetName);
|
|
1955
|
+
const appKey = opts.appKey ?? target.appKey;
|
|
1956
|
+
const artifactId = opts.artifactId ?? target.artifactId;
|
|
1957
|
+
const { client } = await createDeployClient(opts.profile);
|
|
1958
|
+
startSpinner("\uBC14\uC774\uB108\uB9AC \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
|
|
1959
|
+
try {
|
|
1960
|
+
const buffer = await client.downloadBinary(appKey, artifactId, binaryGroupKey, binaryKey);
|
|
1961
|
+
(0, import_node_fs4.writeFileSync)(outPath, buffer);
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
stopSpinner(false);
|
|
1964
|
+
throw err;
|
|
1965
|
+
}
|
|
1966
|
+
stopSpinner(true);
|
|
1967
|
+
if (!opts.quiet) {
|
|
1968
|
+
process.stderr.write(import_chalk2.default.green(` \uC800\uC7A5\uB428: ${outPath}
|
|
1969
|
+
`));
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1225
1973
|
// src/commands/instance/list.ts
|
|
1226
|
-
var
|
|
1974
|
+
var import_commander13 = require("commander");
|
|
1227
1975
|
|
|
1228
1976
|
// src/services/instance/client.ts
|
|
1229
1977
|
var import_ky6 = __toESM(require("ky"));
|
|
@@ -1244,21 +1992,108 @@ function isServersResponse(val) {
|
|
|
1244
1992
|
const obj = val;
|
|
1245
1993
|
return Array.isArray(obj["servers"]);
|
|
1246
1994
|
}
|
|
1995
|
+
function isFlavor(val) {
|
|
1996
|
+
if (typeof val !== "object" || val === null) return false;
|
|
1997
|
+
const obj = val;
|
|
1998
|
+
return typeof obj["id"] === "string" && typeof obj["name"] === "string";
|
|
1999
|
+
}
|
|
2000
|
+
function isImage(val) {
|
|
2001
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2002
|
+
const obj = val;
|
|
2003
|
+
return typeof obj["id"] === "string" && // Glance v2 스펙상 name 은 nullable — null 인 private 이미지 하나가 페이지 전체를 거부하지 않게 허용.
|
|
2004
|
+
(typeof obj["name"] === "string" || obj["name"] === null) && typeof obj["status"] === "string" && typeof obj["visibility"] === "string";
|
|
2005
|
+
}
|
|
2006
|
+
function isImagesResponse(val) {
|
|
2007
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2008
|
+
const obj = val;
|
|
2009
|
+
const nextOk = obj["next"] === void 0 || typeof obj["next"] === "string";
|
|
2010
|
+
return Array.isArray(obj["images"]) && obj["images"].every(isImage) && nextOk;
|
|
2011
|
+
}
|
|
2012
|
+
function isFlavorsResponse(val) {
|
|
2013
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2014
|
+
const obj = val;
|
|
2015
|
+
return Array.isArray(obj["flavors"]) && obj["flavors"].every(isFlavor);
|
|
2016
|
+
}
|
|
2017
|
+
function isFlavorDetail(val) {
|
|
2018
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2019
|
+
const obj = val;
|
|
2020
|
+
return typeof obj["id"] === "string" && typeof obj["name"] === "string" && typeof obj["vcpus"] === "number" && typeof obj["ram"] === "number" && typeof obj["disk"] === "number";
|
|
2021
|
+
}
|
|
2022
|
+
function isFlavorDetailsResponse(val) {
|
|
2023
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2024
|
+
const obj = val;
|
|
2025
|
+
return Array.isArray(obj["flavors"]) && obj["flavors"].every(isFlavorDetail);
|
|
2026
|
+
}
|
|
1247
2027
|
function isCreateResponse(val) {
|
|
1248
2028
|
if (typeof val !== "object" || val === null) return false;
|
|
1249
2029
|
const server = val["server"];
|
|
1250
2030
|
if (typeof server !== "object" || server === null) return false;
|
|
1251
2031
|
return typeof server["id"] === "string";
|
|
1252
2032
|
}
|
|
2033
|
+
function isKeypair(val) {
|
|
2034
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2035
|
+
const obj = val;
|
|
2036
|
+
return typeof obj["name"] === "string" && typeof obj["public_key"] === "string" && typeof obj["fingerprint"] === "string";
|
|
2037
|
+
}
|
|
2038
|
+
function isKeypairsResponse(val) {
|
|
2039
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2040
|
+
const obj = val;
|
|
2041
|
+
return Array.isArray(obj["keypairs"]) && obj["keypairs"].every((e) => {
|
|
2042
|
+
if (typeof e !== "object" || e === null) return false;
|
|
2043
|
+
return isKeypair(e["keypair"]);
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
function isCreateKeypairResponse(val) {
|
|
2047
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2048
|
+
const obj = val;
|
|
2049
|
+
return isKeypair(obj["keypair"]);
|
|
2050
|
+
}
|
|
2051
|
+
function isKeypairDetail(val) {
|
|
2052
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2053
|
+
const obj = val;
|
|
2054
|
+
return typeof obj["name"] === "string" && typeof obj["public_key"] === "string" && typeof obj["fingerprint"] === "string" && typeof obj["user_id"] === "string" && typeof obj["id"] === "string" && typeof obj["created_at"] === "string";
|
|
2055
|
+
}
|
|
2056
|
+
function isKeypairDetailResponse(val) {
|
|
2057
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2058
|
+
return isKeypairDetail(val["keypair"]);
|
|
2059
|
+
}
|
|
2060
|
+
function isAvailabilityZone(val) {
|
|
2061
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2062
|
+
const obj = val;
|
|
2063
|
+
const state = obj["zoneState"];
|
|
2064
|
+
if (typeof state !== "object" || state === null) return false;
|
|
2065
|
+
return typeof obj["zoneName"] === "string" && typeof state["available"] === "boolean";
|
|
2066
|
+
}
|
|
2067
|
+
function isAvailabilityZonesResponse(val) {
|
|
2068
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2069
|
+
const obj = val;
|
|
2070
|
+
return Array.isArray(obj["availabilityZoneInfo"]) && obj["availabilityZoneInfo"].every(isAvailabilityZone);
|
|
2071
|
+
}
|
|
2072
|
+
function isServerVolumeAttachment(val) {
|
|
2073
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2074
|
+
const o = val;
|
|
2075
|
+
return typeof o["id"] === "string" && typeof o["volumeId"] === "string" && typeof o["serverId"] === "string" && typeof o["device"] === "string";
|
|
2076
|
+
}
|
|
2077
|
+
function isVolumeAttachmentsResponse(val) {
|
|
2078
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2079
|
+
const arr = val["volumeAttachments"];
|
|
2080
|
+
return Array.isArray(arr) && arr.every(isServerVolumeAttachment);
|
|
2081
|
+
}
|
|
2082
|
+
function isVolumeAttachmentResponse(val) {
|
|
2083
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2084
|
+
return isServerVolumeAttachment(val["volumeAttachment"]);
|
|
2085
|
+
}
|
|
1253
2086
|
function hasIpAddress(server) {
|
|
1254
2087
|
return Object.values(server.addresses).some((list) => list.length > 0);
|
|
1255
2088
|
}
|
|
1256
2089
|
var InstanceClient = class {
|
|
1257
2090
|
tokenId;
|
|
1258
2091
|
computeEndpoint;
|
|
1259
|
-
|
|
2092
|
+
imageEndpoint;
|
|
2093
|
+
constructor(tokenId, computeEndpoint, imageEndpoint) {
|
|
1260
2094
|
this.tokenId = tokenId;
|
|
1261
2095
|
this.computeEndpoint = computeEndpoint;
|
|
2096
|
+
this.imageEndpoint = imageEndpoint;
|
|
1262
2097
|
}
|
|
1263
2098
|
authHeaders() {
|
|
1264
2099
|
return { "X-Auth-Token": this.tokenId };
|
|
@@ -1310,6 +2145,7 @@ var InstanceClient = class {
|
|
|
1310
2145
|
/**
|
|
1311
2146
|
* 인스턴스를 생성한다 (POST /servers).
|
|
1312
2147
|
* NHN 확장 필드(ephemeralDiskSize / protect)는 정의됐을 때만 payload 에 포함한다.
|
|
2148
|
+
* userDataBase64 도 정의됐을 때만 `user_data` 로 포함한다 (인코딩은 command 단에서 완료).
|
|
1313
2149
|
*/
|
|
1314
2150
|
async create(params) {
|
|
1315
2151
|
const url = `${this.computeEndpoint}/servers`;
|
|
@@ -1344,6 +2180,9 @@ var InstanceClient = class {
|
|
|
1344
2180
|
if (params.protect !== void 0) {
|
|
1345
2181
|
serverBody["NHN-EXT-ATTR:protect"] = params.protect;
|
|
1346
2182
|
}
|
|
2183
|
+
if (params.userDataBase64 !== void 0) {
|
|
2184
|
+
serverBody["user_data"] = params.userDataBase64;
|
|
2185
|
+
}
|
|
1347
2186
|
let raw;
|
|
1348
2187
|
try {
|
|
1349
2188
|
raw = await import_ky6.default.post(url, {
|
|
@@ -1379,18 +2218,291 @@ var InstanceClient = class {
|
|
|
1379
2218
|
}
|
|
1380
2219
|
}
|
|
1381
2220
|
/**
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
2221
|
+
* 서버 action 을 실행한다 (POST /servers/{id}/action, 202 무본문).
|
|
2222
|
+
* NHN Cloud(OpenStack Nova)의 모든 전원·라이프사이클 action 의 공용 경로다.
|
|
2223
|
+
* payload 는 호출자가 action 별로 구성한다 (예: { "os-start": null }).
|
|
2224
|
+
* start/stop/reboot 가 이 helper 를 재사용하며, resize/shelve 등 향후 action 도 동일.
|
|
1386
2225
|
*/
|
|
1387
|
-
async
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
2226
|
+
async serverAction(id, payload) {
|
|
2227
|
+
const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}/action`;
|
|
2228
|
+
try {
|
|
2229
|
+
await import_ky6.default.post(url, {
|
|
2230
|
+
headers: this.authHeaders(),
|
|
2231
|
+
json: payload,
|
|
2232
|
+
retry: 0,
|
|
2233
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
2234
|
+
});
|
|
2235
|
+
} catch (err) {
|
|
2236
|
+
throw toNhnCloudCliError(err);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
/** 인스턴스를 시작한다 (SHUTOFF → ACTIVE). */
|
|
2240
|
+
async start(id) {
|
|
2241
|
+
return this.serverAction(id, { "os-start": null });
|
|
2242
|
+
}
|
|
2243
|
+
/** 인스턴스를 정지한다 (ACTIVE/ERROR → SHUTOFF). */
|
|
2244
|
+
async stop(id) {
|
|
2245
|
+
return this.serverAction(id, { "os-stop": null });
|
|
2246
|
+
}
|
|
2247
|
+
/** 인스턴스를 재부팅한다. type 기본 SOFT, HARD 는 강제 전원 cycle. */
|
|
2248
|
+
async reboot(id, type = "SOFT") {
|
|
2249
|
+
return this.serverAction(id, { reboot: { type } });
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* 인스턴스 타입(flavor)을 변경한다 (resize action).
|
|
2253
|
+
* POST /servers/{id}/action body { "resize": { "flavorRef": "<flavor-id>" } }, 202 무본문.
|
|
2254
|
+
* 사전 상태는 ACTIVE 또는 SHUTOFF (ACTIVE 면 NHN 측에서 중지 후 재시작).
|
|
2255
|
+
*/
|
|
2256
|
+
async resize(id, flavorRef) {
|
|
2257
|
+
return this.serverAction(id, { resize: { flavorRef } });
|
|
2258
|
+
}
|
|
2259
|
+
/** resize 를 확정한다 (VERIFY_RESIZE → ACTIVE, 새 flavor 로 고정). */
|
|
2260
|
+
async confirmResize(id) {
|
|
2261
|
+
return this.serverAction(id, { confirmResize: null });
|
|
2262
|
+
}
|
|
2263
|
+
/** resize 를 롤백한다 (VERIFY_RESIZE → ACTIVE, 이전 flavor 로 복귀). */
|
|
2264
|
+
async revertResize(id) {
|
|
2265
|
+
return this.serverAction(id, { revertResize: null });
|
|
2266
|
+
}
|
|
2267
|
+
async listFlavors(params = {}) {
|
|
2268
|
+
const path = params.detail ? "/flavors/detail" : "/flavors";
|
|
2269
|
+
const url = `${this.computeEndpoint}${path}`;
|
|
2270
|
+
const searchParams = {};
|
|
2271
|
+
if (params.minDisk !== void 0) searchParams["minDisk"] = params.minDisk;
|
|
2272
|
+
if (params.minRam !== void 0) searchParams["minRam"] = params.minRam;
|
|
2273
|
+
try {
|
|
2274
|
+
const raw = await import_ky6.default.get(url, {
|
|
2275
|
+
headers: this.authHeaders(),
|
|
2276
|
+
searchParams,
|
|
2277
|
+
retry: 0,
|
|
2278
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
2279
|
+
}).json();
|
|
2280
|
+
if (params.detail) {
|
|
2281
|
+
if (!isFlavorDetailsResponse(raw)) {
|
|
2282
|
+
throw new NhnCloudCliError(
|
|
2283
|
+
"instance flavors --detail \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 vcpus\xB7ram\xB7disk \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2284
|
+
EXIT_API_ERROR
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
return raw.flavors;
|
|
2288
|
+
}
|
|
2289
|
+
if (!isFlavorsResponse(raw)) {
|
|
2290
|
+
throw new NhnCloudCliError(
|
|
2291
|
+
"instance flavors \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 flavors \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2292
|
+
EXIT_API_ERROR
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
return raw.flavors;
|
|
2296
|
+
} catch (err) {
|
|
2297
|
+
throw toNhnCloudCliError(err);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* 가용성 영역(availability zone) 목록을 조회한다 (GET /os-availability-zone).
|
|
2302
|
+
* zoneName·가용 여부(available)를 반환하며 페이지네이션·필터 없음.
|
|
2303
|
+
*/
|
|
2304
|
+
async listAvailabilityZones() {
|
|
2305
|
+
const url = `${this.computeEndpoint}/os-availability-zone`;
|
|
2306
|
+
try {
|
|
2307
|
+
const raw = await import_ky6.default.get(url, {
|
|
2308
|
+
headers: this.authHeaders(),
|
|
2309
|
+
retry: 0,
|
|
2310
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
2311
|
+
}).json();
|
|
2312
|
+
if (!isAvailabilityZonesResponse(raw)) {
|
|
2313
|
+
throw new NhnCloudCliError(
|
|
2314
|
+
"instance availability-zones \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 availabilityZoneInfo \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2315
|
+
EXIT_API_ERROR
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
return raw.availabilityZoneInfo;
|
|
2319
|
+
} catch (err) {
|
|
2320
|
+
throw toNhnCloudCliError(err);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
/** 키페어 목록을 조회한다 (GET /os-keypairs). 응답 원소의 한겹(keypair)을 풀어 반환. */
|
|
2324
|
+
async listKeypairs() {
|
|
2325
|
+
const url = `${this.computeEndpoint}/os-keypairs`;
|
|
2326
|
+
try {
|
|
2327
|
+
const raw = await import_ky6.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 }).json();
|
|
2328
|
+
if (!isKeypairsResponse(raw)) {
|
|
2329
|
+
throw new NhnCloudCliError(
|
|
2330
|
+
"instance keypairs \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 keypairs \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2331
|
+
EXIT_API_ERROR
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
return raw.keypairs.map((e) => e.keypair);
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
throw toNhnCloudCliError(err);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
/** 단일 키페어를 조회한다 (GET /os-keypairs/{name}). */
|
|
2340
|
+
async getKeypair(name) {
|
|
2341
|
+
const url = `${this.computeEndpoint}/os-keypairs/${encodeURIComponent(name)}`;
|
|
2342
|
+
try {
|
|
2343
|
+
const raw = await import_ky6.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 }).json();
|
|
2344
|
+
if (!isKeypairDetailResponse(raw)) {
|
|
2345
|
+
throw new NhnCloudCliError(
|
|
2346
|
+
`instance keypair get(${name}) \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 keypair \uC0C1\uC138 \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
2347
|
+
EXIT_API_ERROR
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
return raw.keypair;
|
|
2351
|
+
} catch (err) {
|
|
2352
|
+
throw toNhnCloudCliError(err);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* 키페어를 생성한다 (POST /os-keypairs).
|
|
2357
|
+
* publicKey 미지정이면 NHN 이 키쌍을 생성하고 응답 keypair 에 private_key 가 1회성으로 포함된다.
|
|
2358
|
+
* publicKey 지정이면 기존 공개키를 등록하고 private_key 는 응답에 없다.
|
|
2359
|
+
*/
|
|
2360
|
+
async createKeypair(params) {
|
|
2361
|
+
const url = `${this.computeEndpoint}/os-keypairs`;
|
|
2362
|
+
const keypairBody = { name: params.name };
|
|
2363
|
+
if (params.publicKey !== void 0) {
|
|
2364
|
+
keypairBody["public_key"] = params.publicKey;
|
|
2365
|
+
}
|
|
2366
|
+
let raw;
|
|
2367
|
+
try {
|
|
2368
|
+
raw = await import_ky6.default.post(url, {
|
|
2369
|
+
headers: this.authHeaders(),
|
|
2370
|
+
json: { keypair: keypairBody },
|
|
2371
|
+
retry: 0,
|
|
2372
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
2373
|
+
}).json();
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
throw toNhnCloudCliError(err);
|
|
2376
|
+
}
|
|
2377
|
+
if (!isCreateKeypairResponse(raw)) {
|
|
2378
|
+
throw new NhnCloudCliError(
|
|
2379
|
+
"instance keypair create \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 keypair \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2380
|
+
EXIT_API_ERROR
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
const kp = raw.keypair;
|
|
2384
|
+
return {
|
|
2385
|
+
name: kp.name,
|
|
2386
|
+
public_key: kp.public_key,
|
|
2387
|
+
fingerprint: kp.fingerprint,
|
|
2388
|
+
user_id: kp.user_id ?? "",
|
|
2389
|
+
// 빈 문자열은 정의되지 않은 것과 동일 취급 — 빈 키 파일 저장/빈 줄 출력 방지.
|
|
2390
|
+
private_key: kp.private_key !== void 0 && kp.private_key.length > 0 ? kp.private_key : void 0
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
/** 키페어를 삭제한다 (DELETE /os-keypairs/{name}, 202/204 무응답). */
|
|
2394
|
+
async deleteKeypair(name) {
|
|
2395
|
+
const url = `${this.computeEndpoint}/os-keypairs/${encodeURIComponent(name)}`;
|
|
2396
|
+
try {
|
|
2397
|
+
await import_ky6.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 });
|
|
2398
|
+
} catch (err) {
|
|
2399
|
+
throw toNhnCloudCliError(err);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* 이미지 목록을 조회한다 (GET /v2/images, Glance v2).
|
|
2404
|
+
* compute 와 다른 host(imageEndpoint)지만 같은 Keystone 토큰을 쓴다.
|
|
2405
|
+
* 한 페이지(기본 limit 25)만 반환한다 — next 가 있으면 호출부가 marker 로 이어 받는다.
|
|
2406
|
+
*/
|
|
2407
|
+
async listImages(params = {}) {
|
|
2408
|
+
const url = `${this.imageEndpoint}/images`;
|
|
2409
|
+
const searchParams = {};
|
|
2410
|
+
if (params.limit !== void 0) searchParams["limit"] = params.limit;
|
|
2411
|
+
if (params.marker !== void 0) searchParams["marker"] = params.marker;
|
|
2412
|
+
if (params.name !== void 0) searchParams["name"] = params.name;
|
|
2413
|
+
if (params.visibility !== void 0) searchParams["visibility"] = params.visibility;
|
|
2414
|
+
if (params.owner !== void 0) searchParams["owner"] = params.owner;
|
|
2415
|
+
if (params.status !== void 0) searchParams["status"] = params.status;
|
|
2416
|
+
try {
|
|
2417
|
+
const raw = await import_ky6.default.get(url, {
|
|
2418
|
+
headers: this.authHeaders(),
|
|
2419
|
+
searchParams,
|
|
2420
|
+
retry: 0,
|
|
2421
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
2422
|
+
}).json();
|
|
2423
|
+
if (!isImagesResponse(raw)) {
|
|
2424
|
+
throw new NhnCloudCliError(
|
|
2425
|
+
"instance images \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 images \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2426
|
+
EXIT_API_ERROR
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
return { images: raw.images, next: raw.next };
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
throw toNhnCloudCliError(err);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* 인스턴스에 연결된 볼륨 목록을 조회한다 (GET .../os-volume_attachments).
|
|
2436
|
+
* Nova 표준 확장 — NHN Instance(Nova v2 호환, ADR-010). 실측 200 확인 (2026-06-11).
|
|
2437
|
+
*/
|
|
2438
|
+
async listVolumeAttachments(serverId) {
|
|
2439
|
+
const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments`;
|
|
2440
|
+
try {
|
|
2441
|
+
const raw = await import_ky6.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 }).json();
|
|
2442
|
+
if (!isVolumeAttachmentsResponse(raw)) {
|
|
2443
|
+
throw new NhnCloudCliError(
|
|
2444
|
+
"instance volumes \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumeAttachments \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2445
|
+
EXIT_API_ERROR
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
return raw.volumeAttachments;
|
|
2449
|
+
} catch (err) {
|
|
2450
|
+
throw toNhnCloudCliError(err);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* 볼륨을 인스턴스에 연결한다 (POST .../os-volume_attachments).
|
|
2455
|
+
* 요청 body: { volumeAttachment: { volumeId } }. 실제 연결은 수동 QA 확정 (1-26).
|
|
2456
|
+
*/
|
|
2457
|
+
async attachVolume(serverId, volumeId) {
|
|
2458
|
+
const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments`;
|
|
2459
|
+
try {
|
|
2460
|
+
const res = await import_ky6.default.post(url, {
|
|
2461
|
+
headers: this.authHeaders(),
|
|
2462
|
+
json: { volumeAttachment: { volumeId } },
|
|
2463
|
+
retry: 0,
|
|
2464
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
2465
|
+
});
|
|
2466
|
+
if (res.status === 202 || res.headers.get("content-length") === "0") {
|
|
2467
|
+
return { id: volumeId, volumeId, serverId, device: "" };
|
|
2468
|
+
}
|
|
2469
|
+
const raw = await res.json();
|
|
2470
|
+
if (!isVolumeAttachmentResponse(raw)) {
|
|
2471
|
+
throw new NhnCloudCliError(
|
|
2472
|
+
"instance volume attach \uC751\uB2F5\uC5D0 volumeAttachment \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2473
|
+
EXIT_API_ERROR
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
return raw.volumeAttachment;
|
|
2477
|
+
} catch (err) {
|
|
2478
|
+
throw toNhnCloudCliError(err);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* 볼륨 연결을 해제한다 (DELETE .../os-volume_attachments/{volumeId}, 202 무응답).
|
|
2483
|
+
* 실제 해제는 수동 QA 확정 (1-26).
|
|
2484
|
+
*/
|
|
2485
|
+
async detachVolume(serverId, volumeId) {
|
|
2486
|
+
const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments/${encodeURIComponent(volumeId)}`;
|
|
2487
|
+
try {
|
|
2488
|
+
await import_ky6.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 });
|
|
2489
|
+
} catch (err) {
|
|
2490
|
+
throw toNhnCloudCliError(err);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* 인스턴스가 ACTIVE 상태 + IP 1개 이상이 될 때까지 폴링한다.
|
|
2495
|
+
*
|
|
2496
|
+
* - status === "ACTIVE" + addresses 에 IP 1개 이상: 즉시 반환
|
|
2497
|
+
* - timeout 초과: 마지막 status 를 포함한 NhnCloudCliError(EXIT_API_ERROR)
|
|
2498
|
+
*/
|
|
2499
|
+
async waitForActive(id, opts) {
|
|
2500
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
2501
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
2502
|
+
let lastServer = null;
|
|
2503
|
+
while (Date.now() < deadline) {
|
|
2504
|
+
const server = await this.get(id);
|
|
2505
|
+
lastServer = server;
|
|
1394
2506
|
if (server.status === "ACTIVE" && hasIpAddress(server)) {
|
|
1395
2507
|
return server;
|
|
1396
2508
|
}
|
|
@@ -1404,50 +2516,691 @@ var InstanceClient = class {
|
|
|
1404
2516
|
EXIT_API_ERROR
|
|
1405
2517
|
);
|
|
1406
2518
|
}
|
|
1407
|
-
};
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
// src/commands/instance/helpers.ts
|
|
2522
|
+
async function resolveInstanceClient(opts) {
|
|
2523
|
+
const profileName = await resolveProfileName(opts.profile);
|
|
2524
|
+
const iaas = await getIaasCredential(profileName);
|
|
2525
|
+
const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
|
|
2526
|
+
const { tokenId, computeEndpoint, imageEndpoint } = await getIaasToken(profileName, effectiveIaas);
|
|
2527
|
+
return { client: new InstanceClient(tokenId, computeEndpoint, imageEndpoint), profileName };
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// src/commands/instance/list.ts
|
|
2531
|
+
function getIps(server) {
|
|
2532
|
+
return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
|
|
2533
|
+
}
|
|
2534
|
+
var listCommand = new import_commander13.Command("list").description("\uC778\uC2A4\uD134\uC2A4 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
2535
|
+
const opts = cmd.optsWithGlobals();
|
|
2536
|
+
const { client } = await resolveInstanceClient(opts);
|
|
2537
|
+
startSpinner("\uC778\uC2A4\uD134\uC2A4 \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
2538
|
+
let servers;
|
|
2539
|
+
try {
|
|
2540
|
+
servers = await client.list();
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
stopSpinner(false);
|
|
2543
|
+
throw err;
|
|
2544
|
+
}
|
|
2545
|
+
stopSpinner(true);
|
|
2546
|
+
output(opts, {
|
|
2547
|
+
headers: ["id", "name", "status", "IPs", "flavor"],
|
|
2548
|
+
rows: servers.map((s) => [s.id, s.name, s.status, getIps(s), s.flavor.id]),
|
|
2549
|
+
raw: servers,
|
|
2550
|
+
ids: servers.map((s) => s.id)
|
|
2551
|
+
});
|
|
2552
|
+
});
|
|
2553
|
+
|
|
2554
|
+
// src/commands/volume/list.ts
|
|
2555
|
+
var import_commander14 = require("commander");
|
|
2556
|
+
|
|
2557
|
+
// src/services/blockstorage/client.ts
|
|
2558
|
+
var import_ky7 = __toESM(require("ky"));
|
|
2559
|
+
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
2560
|
+
function isVolume(val) {
|
|
2561
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2562
|
+
const obj = val;
|
|
2563
|
+
return typeof obj["id"] === "string" && // Cinder 는 --name 미지정 시 null — null 인 볼륨 하나가 list 전체를 거부하지 않게 허용 (isImage 선례).
|
|
2564
|
+
(typeof obj["name"] === "string" || obj["name"] === null) && typeof obj["size"] === "number" && typeof obj["status"] === "string" && Array.isArray(obj["attachments"]);
|
|
2565
|
+
}
|
|
2566
|
+
function isVolumesResponse(val) {
|
|
2567
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2568
|
+
const obj = val;
|
|
2569
|
+
return Array.isArray(obj["volumes"]) && obj["volumes"].every(isVolume);
|
|
2570
|
+
}
|
|
2571
|
+
function isVolumeResponse(val) {
|
|
2572
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2573
|
+
const obj = val;
|
|
2574
|
+
return isVolume(obj["volume"]);
|
|
2575
|
+
}
|
|
2576
|
+
var BlockStorageClient = class {
|
|
2577
|
+
tokenId;
|
|
2578
|
+
endpoint;
|
|
2579
|
+
// blockStorageEndpoint (/v2/{tenantId} 까지 포함)
|
|
2580
|
+
constructor(tokenId, blockStorageEndpoint) {
|
|
2581
|
+
this.tokenId = tokenId;
|
|
2582
|
+
this.endpoint = blockStorageEndpoint;
|
|
2583
|
+
}
|
|
2584
|
+
authHeaders() {
|
|
2585
|
+
return { "X-Auth-Token": this.tokenId };
|
|
2586
|
+
}
|
|
2587
|
+
async list(params) {
|
|
2588
|
+
const url = `${this.endpoint}/volumes`;
|
|
2589
|
+
const searchParams = {};
|
|
2590
|
+
if (params?.sort !== void 0) searchParams["sort"] = params.sort;
|
|
2591
|
+
if (params?.limit !== void 0) searchParams["limit"] = params.limit;
|
|
2592
|
+
if (params?.offset !== void 0) searchParams["offset"] = params.offset;
|
|
2593
|
+
if (params?.marker !== void 0) searchParams["marker"] = params.marker;
|
|
2594
|
+
try {
|
|
2595
|
+
const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), searchParams, retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
|
|
2596
|
+
if (!isVolumesResponse(raw)) {
|
|
2597
|
+
throw new NhnCloudCliError(
|
|
2598
|
+
"volume list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumes \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2599
|
+
EXIT_API_ERROR
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
return raw.volumes;
|
|
2603
|
+
} catch (err) {
|
|
2604
|
+
throw toNhnCloudCliError(err);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
async get(id) {
|
|
2608
|
+
const url = `${this.endpoint}/volumes/${encodeURIComponent(id)}`;
|
|
2609
|
+
try {
|
|
2610
|
+
const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
|
|
2611
|
+
if (!isVolumeResponse(raw)) {
|
|
2612
|
+
throw new NhnCloudCliError(
|
|
2613
|
+
"volume get \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volume \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2614
|
+
EXIT_API_ERROR
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
return raw.volume;
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
throw toNhnCloudCliError(err);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
async create(params) {
|
|
2623
|
+
const url = `${this.endpoint}/volumes`;
|
|
2624
|
+
const volumeBody = { size: params.size };
|
|
2625
|
+
if (params.name !== void 0) volumeBody["name"] = params.name;
|
|
2626
|
+
if (params.description !== void 0) volumeBody["description"] = params.description;
|
|
2627
|
+
if (params.volume_type !== void 0) volumeBody["volume_type"] = params.volume_type;
|
|
2628
|
+
if (params.snapshot_id !== void 0) volumeBody["snapshot_id"] = params.snapshot_id;
|
|
2629
|
+
try {
|
|
2630
|
+
const raw = await import_ky7.default.post(url, {
|
|
2631
|
+
headers: this.authHeaders(),
|
|
2632
|
+
json: { volume: volumeBody },
|
|
2633
|
+
retry: 0,
|
|
2634
|
+
timeout: DEFAULT_TIMEOUT_MS3
|
|
2635
|
+
}).json();
|
|
2636
|
+
if (!isVolumeResponse(raw)) {
|
|
2637
|
+
throw new NhnCloudCliError(
|
|
2638
|
+
"volume create \uC751\uB2F5\uC5D0 volume \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2639
|
+
EXIT_API_ERROR
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
return raw.volume;
|
|
2643
|
+
} catch (err) {
|
|
2644
|
+
throw toNhnCloudCliError(err);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
|
|
2649
|
+
// src/commands/volume/helpers.ts
|
|
2650
|
+
async function resolveVolumeClient(opts) {
|
|
2651
|
+
const profileName = await resolveProfileName(opts.profile);
|
|
2652
|
+
const iaas = await getIaasCredential(profileName);
|
|
2653
|
+
const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
|
|
2654
|
+
const { tokenId, blockStorageEndpoint } = await getIaasToken(profileName, effectiveIaas);
|
|
2655
|
+
return { client: new BlockStorageClient(tokenId, blockStorageEndpoint), profileName };
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
// src/commands/volume/list.ts
|
|
2659
|
+
function parsePositiveInt4(value, flag) {
|
|
2660
|
+
if (!/^[1-9]\d*$/.test(value)) {
|
|
2661
|
+
throw new NhnCloudCliError(
|
|
2662
|
+
`${flag} \uB294 \uC591\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${JSON.stringify(value)}`,
|
|
2663
|
+
EXIT_PARAM_ERROR
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
return Number(value);
|
|
2667
|
+
}
|
|
2668
|
+
var listCommand2 = new import_commander14.Command("list").description("\uBCFC\uB968 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").option("--sort <key:dir>", "\uC815\uB82C \uAE30\uC900 (\uC608: created_at:desc)").option("--limit <n>", "\uCD5C\uB300 \uBC18\uD658 \uAC1C\uC218").option("--offset <n>", "\uD398\uC774\uC9C0 \uC2DC\uC791 \uC624\uD504\uC14B").option("--marker <id>", "\uD398\uC774\uC9C0\uB124\uC774\uC158 \uB9C8\uCEE4 \uBCFC\uB968 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
2669
|
+
const opts = cmd.optsWithGlobals();
|
|
2670
|
+
const limitNum = opts.limit !== void 0 ? parsePositiveInt4(opts.limit, "--limit") : void 0;
|
|
2671
|
+
const offsetNum = opts.offset !== void 0 ? parsePositiveInt4(opts.offset, "--offset") : void 0;
|
|
2672
|
+
const { client } = await resolveVolumeClient(opts);
|
|
2673
|
+
startSpinner("\uBCFC\uB968 \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
2674
|
+
let volumes;
|
|
2675
|
+
try {
|
|
2676
|
+
volumes = await client.list({
|
|
2677
|
+
sort: opts.sort,
|
|
2678
|
+
limit: limitNum,
|
|
2679
|
+
offset: offsetNum,
|
|
2680
|
+
marker: opts.marker
|
|
2681
|
+
});
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
stopSpinner(false);
|
|
2684
|
+
throw err;
|
|
2685
|
+
}
|
|
2686
|
+
stopSpinner(true);
|
|
2687
|
+
output(opts, {
|
|
2688
|
+
headers: ["id", "name", "size", "status", "type"],
|
|
2689
|
+
rows: volumes.map((v) => [
|
|
2690
|
+
v.id,
|
|
2691
|
+
v.name ?? "",
|
|
2692
|
+
String(v.size),
|
|
2693
|
+
v.status,
|
|
2694
|
+
v.volume_type ?? ""
|
|
2695
|
+
]),
|
|
2696
|
+
raw: volumes,
|
|
2697
|
+
ids: volumes.map((v) => v.id)
|
|
2698
|
+
});
|
|
2699
|
+
});
|
|
2700
|
+
|
|
2701
|
+
// src/commands/volume/get.ts
|
|
2702
|
+
var import_commander15 = require("commander");
|
|
2703
|
+
var getCommand = new import_commander15.Command("get").description("\uB2E8\uC77C \uBCFC\uB968 \uC0C1\uD0DC\uB97C \uC870\uD68C\uD55C\uB2E4").argument("<id>", "\uBCFC\uB968 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
2704
|
+
const opts = cmd.optsWithGlobals();
|
|
2705
|
+
const { client } = await resolveVolumeClient(opts);
|
|
2706
|
+
startSpinner("\uBCFC\uB968 \uC870\uD68C \uC911...");
|
|
2707
|
+
let volume;
|
|
2708
|
+
try {
|
|
2709
|
+
volume = await client.get(id);
|
|
2710
|
+
} catch (err) {
|
|
2711
|
+
stopSpinner(false);
|
|
2712
|
+
throw err;
|
|
2713
|
+
}
|
|
2714
|
+
stopSpinner(true);
|
|
2715
|
+
const attachmentSummary = Array.isArray(volume.attachments) ? volume.attachments.filter((a) => typeof a === "object" && a !== null).map((a) => String(a.server_id)).join(", ") : "";
|
|
2716
|
+
const rows = [
|
|
2717
|
+
["id", volume.id],
|
|
2718
|
+
["name", volume.name ?? ""],
|
|
2719
|
+
["size", String(volume.size)],
|
|
2720
|
+
["status", volume.status],
|
|
2721
|
+
["volume_type", volume.volume_type ?? ""],
|
|
2722
|
+
["created_at", volume.created_at],
|
|
2723
|
+
["attachments", attachmentSummary]
|
|
2724
|
+
];
|
|
2725
|
+
output(opts, {
|
|
2726
|
+
headers: ["field", "value"],
|
|
2727
|
+
rows,
|
|
2728
|
+
raw: volume,
|
|
2729
|
+
ids: [volume.id]
|
|
2730
|
+
});
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
// src/commands/volume/create.ts
|
|
2734
|
+
var import_commander16 = require("commander");
|
|
2735
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
2736
|
+
var createCommand = new import_commander16.Command("create").description("\uBCFC\uB968\uC744 \uBC1C\uAE09\uD55C\uB2E4").requiredOption("--size <gb>", "\uBCFC\uB968 \uD06C\uAE30(GB) \u2014 \uD544\uC218").option("--name <name>", "\uBCFC\uB968 \uC774\uB984").option("--description <text>", "\uBCFC\uB968 \uC124\uBA85").option("--volume-type <type>", "\uBCFC\uB968 \uD0C0\uC785").option("--snapshot-id <id>", "\uC2A4\uB0C5\uC0F7 ID (\uC2A4\uB0C5\uC0F7\uC5D0\uC11C \uBCFC\uB968 \uC0DD\uC131)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
2737
|
+
const opts = cmd.optsWithGlobals();
|
|
2738
|
+
if (!/^[1-9]\d*$/.test(opts.size)) {
|
|
2739
|
+
throw new NhnCloudCliError(
|
|
2740
|
+
`--size \uB294 \uC591\uC758 \uC815\uC218(GB)\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${JSON.stringify(opts.size)}`,
|
|
2741
|
+
EXIT_PARAM_ERROR
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
const sizeNum = Number(opts.size);
|
|
2745
|
+
const { client } = await resolveVolumeClient(opts);
|
|
2746
|
+
startSpinner("\uBCFC\uB968 \uBC1C\uAE09 \uC911...");
|
|
2747
|
+
let volume;
|
|
2748
|
+
try {
|
|
2749
|
+
volume = await client.create({
|
|
2750
|
+
size: sizeNum,
|
|
2751
|
+
name: opts.name,
|
|
2752
|
+
description: opts.description,
|
|
2753
|
+
volume_type: opts.volumeType,
|
|
2754
|
+
snapshot_id: opts.snapshotId
|
|
2755
|
+
});
|
|
2756
|
+
} catch (err) {
|
|
2757
|
+
stopSpinner(false);
|
|
2758
|
+
throw err;
|
|
2759
|
+
}
|
|
2760
|
+
stopSpinner(true);
|
|
2761
|
+
process.stderr.write(import_chalk3.default.green(`\uBCFC\uB968 \uBC1C\uAE09 \uC694\uCCAD \uC644\uB8CC (id: ${volume.id}, status: ${volume.status})
|
|
2762
|
+
`));
|
|
2763
|
+
const rows = [
|
|
2764
|
+
["id", volume.id],
|
|
2765
|
+
["name", volume.name ?? ""],
|
|
2766
|
+
["size", String(volume.size)],
|
|
2767
|
+
["status", volume.status],
|
|
2768
|
+
["volume_type", volume.volume_type ?? ""],
|
|
2769
|
+
["created_at", volume.created_at]
|
|
2770
|
+
];
|
|
2771
|
+
output(opts, {
|
|
2772
|
+
headers: ["field", "value"],
|
|
2773
|
+
rows,
|
|
2774
|
+
raw: volume,
|
|
2775
|
+
ids: [volume.id]
|
|
2776
|
+
});
|
|
2777
|
+
});
|
|
2778
|
+
|
|
2779
|
+
// src/commands/network/list.ts
|
|
2780
|
+
var import_commander17 = require("commander");
|
|
2781
|
+
|
|
2782
|
+
// src/services/network/client.ts
|
|
2783
|
+
var import_ky8 = __toESM(require("ky"));
|
|
2784
|
+
var DEFAULT_TIMEOUT_MS4 = 3e4;
|
|
2785
|
+
function isVpc(val) {
|
|
2786
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2787
|
+
const obj = val;
|
|
2788
|
+
return typeof obj["id"] === "string" && typeof obj["name"] === "string" && typeof obj["cidrv4"] === "string" && typeof obj["state"] === "string" && typeof obj["router:external"] === "boolean";
|
|
2789
|
+
}
|
|
2790
|
+
function isVpcsResponse(val) {
|
|
2791
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2792
|
+
const obj = val;
|
|
2793
|
+
return Array.isArray(obj["vpcs"]) && obj["vpcs"].every(isVpc);
|
|
2794
|
+
}
|
|
2795
|
+
function isSubnet(val) {
|
|
2796
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2797
|
+
const obj = val;
|
|
2798
|
+
return typeof obj["id"] === "string" && typeof obj["cidr"] === "string" && typeof obj["vpc_id"] === "string" && typeof obj["gateway"] === "string" && typeof obj["available_ip_count"] === "number";
|
|
2799
|
+
}
|
|
2800
|
+
function isSubnetsResponse(val) {
|
|
2801
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2802
|
+
const obj = val;
|
|
2803
|
+
return Array.isArray(obj["vpcsubnets"]) && obj["vpcsubnets"].every(isSubnet);
|
|
2804
|
+
}
|
|
2805
|
+
function isFloatingIp(val) {
|
|
2806
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2807
|
+
const obj = val;
|
|
2808
|
+
return typeof obj["id"] === "string" && typeof obj["floating_ip_address"] === "string" && typeof obj["status"] === "string" && typeof obj["floating_network_id"] === "string" && (obj["port_id"] === null || typeof obj["port_id"] === "string") && (obj["fixed_ip_address"] === null || typeof obj["fixed_ip_address"] === "string");
|
|
2809
|
+
}
|
|
2810
|
+
function isFloatingIpsResponse(val) {
|
|
2811
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2812
|
+
const obj = val;
|
|
2813
|
+
return Array.isArray(obj["floatingips"]) && obj["floatingips"].every(isFloatingIp);
|
|
2814
|
+
}
|
|
2815
|
+
function isFloatingIpResponse(val) {
|
|
2816
|
+
if (typeof val !== "object" || val === null) return false;
|
|
2817
|
+
const obj = val;
|
|
2818
|
+
return isFloatingIp(obj["floatingip"]);
|
|
2819
|
+
}
|
|
2820
|
+
var NetworkClient = class {
|
|
2821
|
+
tokenId;
|
|
2822
|
+
networkEndpoint;
|
|
2823
|
+
constructor(tokenId, networkEndpoint) {
|
|
2824
|
+
this.tokenId = tokenId;
|
|
2825
|
+
this.networkEndpoint = networkEndpoint;
|
|
2826
|
+
}
|
|
2827
|
+
authHeaders() {
|
|
2828
|
+
return { "X-Auth-Token": this.tokenId };
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* VPC 목록을 조회한다 (GET /v2.0/vpcs, NHN VPC).
|
|
2832
|
+
* instance 와 다른 host(networkEndpoint)지만 같은 Keystone 토큰을 쓴다.
|
|
2833
|
+
*/
|
|
2834
|
+
async listVpcs() {
|
|
2835
|
+
const url = `${this.networkEndpoint}/vpcs`;
|
|
2836
|
+
try {
|
|
2837
|
+
const raw = await import_ky8.default.get(url, {
|
|
2838
|
+
headers: this.authHeaders(),
|
|
2839
|
+
retry: 0,
|
|
2840
|
+
timeout: DEFAULT_TIMEOUT_MS4
|
|
2841
|
+
}).json();
|
|
2842
|
+
if (!isVpcsResponse(raw)) {
|
|
2843
|
+
throw new NhnCloudCliError(
|
|
2844
|
+
"network list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 vpcs \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2845
|
+
EXIT_API_ERROR
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
return raw.vpcs;
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
throw toNhnCloudCliError(err);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
/**
|
|
2854
|
+
* 서브넷 목록을 조회한다 (GET /v2.0/vpcsubnets, NHN VPC).
|
|
2855
|
+
*/
|
|
2856
|
+
async listSubnets() {
|
|
2857
|
+
const url = `${this.networkEndpoint}/vpcsubnets`;
|
|
2858
|
+
try {
|
|
2859
|
+
const raw = await import_ky8.default.get(url, {
|
|
2860
|
+
headers: this.authHeaders(),
|
|
2861
|
+
retry: 0,
|
|
2862
|
+
timeout: DEFAULT_TIMEOUT_MS4
|
|
2863
|
+
}).json();
|
|
2864
|
+
if (!isSubnetsResponse(raw)) {
|
|
2865
|
+
throw new NhnCloudCliError(
|
|
2866
|
+
"network subnet list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 vpcsubnets \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2867
|
+
EXIT_API_ERROR
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
return raw.vpcsubnets;
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
throw toNhnCloudCliError(err);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
/** Floating IP 목록을 조회한다 (GET /v2.0/floatingips). */
|
|
2876
|
+
async listFloatingIps() {
|
|
2877
|
+
const url = `${this.networkEndpoint}/floatingips`;
|
|
2878
|
+
try {
|
|
2879
|
+
const raw = await import_ky8.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS4 }).json();
|
|
2880
|
+
if (!isFloatingIpsResponse(raw)) {
|
|
2881
|
+
throw new NhnCloudCliError(
|
|
2882
|
+
"floatingip list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 floatingips \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2883
|
+
EXIT_API_ERROR
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
return raw.floatingips;
|
|
2887
|
+
} catch (err) {
|
|
2888
|
+
throw toNhnCloudCliError(err);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
/** Floating IP 를 발급한다 (POST /v2.0/floatingips). */
|
|
2892
|
+
async createFloatingIp(params) {
|
|
2893
|
+
const url = `${this.networkEndpoint}/floatingips`;
|
|
2894
|
+
try {
|
|
2895
|
+
const raw = await import_ky8.default.post(url, {
|
|
2896
|
+
headers: this.authHeaders(),
|
|
2897
|
+
json: { floatingip: { floating_network_id: params.floating_network_id } },
|
|
2898
|
+
retry: 0,
|
|
2899
|
+
timeout: DEFAULT_TIMEOUT_MS4
|
|
2900
|
+
}).json();
|
|
2901
|
+
if (!isFloatingIpResponse(raw)) {
|
|
2902
|
+
throw new NhnCloudCliError(
|
|
2903
|
+
"floatingip create \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 floatingip \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2904
|
+
EXIT_API_ERROR
|
|
2905
|
+
);
|
|
2906
|
+
}
|
|
2907
|
+
return raw.floatingip;
|
|
2908
|
+
} catch (err) {
|
|
2909
|
+
throw toNhnCloudCliError(err);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
/** Floating IP 를 삭제한다 (DELETE /v2.0/floatingips/{id}, 무본문). */
|
|
2913
|
+
async deleteFloatingIp(id) {
|
|
2914
|
+
const url = `${this.networkEndpoint}/floatingips/${encodeURIComponent(id)}`;
|
|
2915
|
+
try {
|
|
2916
|
+
await import_ky8.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS4 });
|
|
2917
|
+
} catch (err) {
|
|
2918
|
+
throw toNhnCloudCliError(err);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* 외부(external) VPC id 를 찾는다 — create 의 floating_network_id 기본 소스.
|
|
2923
|
+
* `router:external` 은 콜론 포함 리터럴 키 — bracket 접근 필수.
|
|
2924
|
+
* external VPC 가 둘 이상이면 첫 매칭을 반환한다.
|
|
2925
|
+
* 사용자는 `--network <id>` 로 명시 지정 가능하므로 create 의 stderr 에 그 사실을 안내한다.
|
|
2926
|
+
*/
|
|
2927
|
+
async findExternalNetworkId() {
|
|
2928
|
+
const url = `${this.networkEndpoint}/vpcs`;
|
|
2929
|
+
try {
|
|
2930
|
+
const raw = await import_ky8.default.get(url, {
|
|
2931
|
+
headers: this.authHeaders(),
|
|
2932
|
+
searchParams: { "router:external": "true" },
|
|
2933
|
+
retry: 0,
|
|
2934
|
+
timeout: DEFAULT_TIMEOUT_MS4
|
|
2935
|
+
}).json();
|
|
2936
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
2937
|
+
const vpcs = raw["vpcs"];
|
|
2938
|
+
if (!Array.isArray(vpcs)) return null;
|
|
2939
|
+
for (const v of vpcs) {
|
|
2940
|
+
if (typeof v !== "object" || v === null) continue;
|
|
2941
|
+
const obj = v;
|
|
2942
|
+
if (obj["router:external"] === true && typeof obj["id"] === "string") {
|
|
2943
|
+
return obj["id"];
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
return null;
|
|
2947
|
+
} catch (err) {
|
|
2948
|
+
throw toNhnCloudCliError(err);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
|
|
2953
|
+
// src/commands/network/helpers.ts
|
|
2954
|
+
async function resolveNetworkClient(opts) {
|
|
2955
|
+
const profileName = await resolveProfileName(opts.profile);
|
|
2956
|
+
const iaas = await getIaasCredential(profileName);
|
|
2957
|
+
const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
|
|
2958
|
+
const { tokenId, networkEndpoint } = await getIaasToken(profileName, effectiveIaas);
|
|
2959
|
+
return { client: new NetworkClient(tokenId, networkEndpoint), profileName };
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// src/commands/network/list.ts
|
|
2963
|
+
var listCommand3 = new import_commander17.Command("list").description("VPC \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (\uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
2964
|
+
const opts = cmd.optsWithGlobals();
|
|
2965
|
+
const { client } = await resolveNetworkClient(opts);
|
|
2966
|
+
startSpinner("VPC \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
2967
|
+
let vpcs;
|
|
2968
|
+
try {
|
|
2969
|
+
vpcs = await client.listVpcs();
|
|
2970
|
+
} catch (err) {
|
|
2971
|
+
stopSpinner(false);
|
|
2972
|
+
throw err;
|
|
2973
|
+
}
|
|
2974
|
+
stopSpinner(true);
|
|
2975
|
+
output(opts, {
|
|
2976
|
+
headers: ["id", "name", "cidrv4", "state", "external"],
|
|
2977
|
+
rows: vpcs.map((v) => [v.id, v.name, v.cidrv4, v.state, String(v["router:external"])]),
|
|
2978
|
+
raw: vpcs,
|
|
2979
|
+
ids: vpcs.map((v) => v.id)
|
|
2980
|
+
});
|
|
2981
|
+
});
|
|
2982
|
+
|
|
2983
|
+
// src/commands/network/subnet.ts
|
|
2984
|
+
var import_commander18 = require("commander");
|
|
2985
|
+
var subnetListCommand = new import_commander18.Command("list").description("\uC11C\uBE0C\uB137 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (\uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
2986
|
+
const opts = cmd.optsWithGlobals();
|
|
2987
|
+
const { client } = await resolveNetworkClient(opts);
|
|
2988
|
+
startSpinner("\uC11C\uBE0C\uB137 \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
2989
|
+
let subnets;
|
|
2990
|
+
try {
|
|
2991
|
+
subnets = await client.listSubnets();
|
|
2992
|
+
} catch (err) {
|
|
2993
|
+
stopSpinner(false);
|
|
2994
|
+
throw err;
|
|
2995
|
+
}
|
|
2996
|
+
stopSpinner(true);
|
|
2997
|
+
output(opts, {
|
|
2998
|
+
headers: ["id", "cidr", "vpc_id", "gateway", "available_ip"],
|
|
2999
|
+
rows: subnets.map((s) => [
|
|
3000
|
+
s.id,
|
|
3001
|
+
s.cidr,
|
|
3002
|
+
s.vpc_id,
|
|
3003
|
+
s.gateway,
|
|
3004
|
+
String(s.available_ip_count)
|
|
3005
|
+
]),
|
|
3006
|
+
raw: subnets,
|
|
3007
|
+
ids: subnets.map((s) => s.id)
|
|
3008
|
+
});
|
|
3009
|
+
});
|
|
3010
|
+
var subnetCommand = new import_commander18.Command("subnet").description("\uC11C\uBE0C\uB137 \uAD00\uB828 \uBA85\uB839").addCommand(subnetListCommand);
|
|
3011
|
+
|
|
3012
|
+
// src/commands/floatingip/list.ts
|
|
3013
|
+
var import_commander19 = require("commander");
|
|
3014
|
+
var listCommand4 = new import_commander19.Command("list").description("Floating IP \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
3015
|
+
const opts = cmd.optsWithGlobals();
|
|
3016
|
+
const { client } = await resolveNetworkClient(opts);
|
|
3017
|
+
startSpinner("Floating IP \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
3018
|
+
let fips;
|
|
3019
|
+
try {
|
|
3020
|
+
fips = await client.listFloatingIps();
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
stopSpinner(false);
|
|
3023
|
+
throw err;
|
|
3024
|
+
}
|
|
3025
|
+
stopSpinner(true);
|
|
3026
|
+
output(opts, {
|
|
3027
|
+
headers: ["id", "floating_ip_address", "status", "port_id", "fixed_ip_address"],
|
|
3028
|
+
rows: fips.map((f) => [
|
|
3029
|
+
f.id,
|
|
3030
|
+
f.floating_ip_address,
|
|
3031
|
+
f.status,
|
|
3032
|
+
f.port_id ?? "-",
|
|
3033
|
+
f.fixed_ip_address ?? "-"
|
|
3034
|
+
]),
|
|
3035
|
+
raw: fips,
|
|
3036
|
+
ids: fips.map((f) => f.id)
|
|
3037
|
+
});
|
|
3038
|
+
});
|
|
3039
|
+
|
|
3040
|
+
// src/commands/floatingip/create.ts
|
|
3041
|
+
var import_commander20 = require("commander");
|
|
3042
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
3043
|
+
var createCommand2 = new import_commander20.Command("create").description("Floating IP \uB97C \uBC1C\uAE09\uD55C\uB2E4 (--network \uBBF8\uC9C0\uC815 \uC2DC \uC678\uBD80 VPC \uC790\uB3D9 \uC870\uD68C)").option("--network <id>", "\uC678\uBD80 \uB124\uD2B8\uC6CC\uD06C(VPC) id (\uBBF8\uC9C0\uC815 \uC2DC router:external=true \uC790\uB3D9 \uC870\uD68C)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
3044
|
+
const opts = cmd.optsWithGlobals();
|
|
3045
|
+
const { client } = await resolveNetworkClient(opts);
|
|
3046
|
+
let networkId = opts.network;
|
|
3047
|
+
if (networkId === void 0) {
|
|
3048
|
+
startSpinner("\uC678\uBD80 \uB124\uD2B8\uC6CC\uD06C \uC870\uD68C \uC911...");
|
|
3049
|
+
try {
|
|
3050
|
+
const found = await client.findExternalNetworkId();
|
|
3051
|
+
if (found === null) {
|
|
3052
|
+
throw new NhnCloudCliError(
|
|
3053
|
+
"\uC678\uBD80 \uB124\uD2B8\uC6CC\uD06C(router:external=true)\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. --network <id> \uB85C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.",
|
|
3054
|
+
EXIT_PARAM_ERROR
|
|
3055
|
+
);
|
|
3056
|
+
}
|
|
3057
|
+
networkId = found;
|
|
3058
|
+
} catch (err) {
|
|
3059
|
+
stopSpinner(false);
|
|
3060
|
+
throw err;
|
|
3061
|
+
}
|
|
3062
|
+
stopSpinner(true);
|
|
3063
|
+
}
|
|
3064
|
+
startSpinner(`Floating IP \uBC1C\uAE09 \uC911... (network: ${networkId})`);
|
|
3065
|
+
let fip;
|
|
3066
|
+
try {
|
|
3067
|
+
fip = await client.createFloatingIp({ floating_network_id: networkId });
|
|
3068
|
+
} catch (err) {
|
|
3069
|
+
stopSpinner(false);
|
|
3070
|
+
throw err;
|
|
3071
|
+
}
|
|
3072
|
+
stopSpinner(true);
|
|
3073
|
+
process.stderr.write(
|
|
3074
|
+
import_chalk4.default.green(`\u2713 Floating IP "${fip.floating_ip_address}" \uB97C \uBC1C\uAE09\uD588\uC2B5\uB2C8\uB2E4 (id: ${fip.id}).
|
|
3075
|
+
`)
|
|
3076
|
+
);
|
|
3077
|
+
output(opts, {
|
|
3078
|
+
headers: ["id", "floating_ip_address", "status", "floating_network_id"],
|
|
3079
|
+
rows: [[fip.id, fip.floating_ip_address, fip.status, fip.floating_network_id]],
|
|
3080
|
+
raw: fip,
|
|
3081
|
+
ids: [fip.id]
|
|
3082
|
+
});
|
|
3083
|
+
});
|
|
3084
|
+
|
|
3085
|
+
// src/commands/floatingip/delete.ts
|
|
3086
|
+
var import_commander21 = require("commander");
|
|
3087
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
3088
|
+
var deleteCommand = new import_commander21.Command("delete").description("Floating IP \uB97C \uC0AD\uC81C\uD55C\uB2E4").argument("<id>", "Floating IP ID").option("--yes", "\uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uC0DD\uB7B5 (CI/\uBE44\uB300\uD654\uD615 \uD544\uC218)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
3089
|
+
const opts = cmd.optsWithGlobals();
|
|
3090
|
+
const isTTY = process.stdin.isTTY;
|
|
3091
|
+
if (!isTTY && !opts.yes) {
|
|
3092
|
+
throw new NhnCloudCliError(
|
|
3093
|
+
"\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C Floating IP \uC0AD\uC81C\uB294 --yes \uD50C\uB798\uADF8\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
3094
|
+
EXIT_PARAM_ERROR
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
if (isTTY && !opts.yes) {
|
|
3098
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
3099
|
+
const ok = await confirm({
|
|
3100
|
+
message: `Floating IP "${id}" \uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`,
|
|
3101
|
+
default: false
|
|
3102
|
+
});
|
|
3103
|
+
if (!ok) {
|
|
3104
|
+
process.stderr.write(import_chalk5.default.yellow("\uC0AD\uC81C\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
const { client } = await resolveNetworkClient(opts);
|
|
3109
|
+
startSpinner(`Floating IP \uC0AD\uC81C \uC911... (id: ${id})`);
|
|
3110
|
+
try {
|
|
3111
|
+
await client.deleteFloatingIp(id);
|
|
3112
|
+
} catch (err) {
|
|
3113
|
+
stopSpinner(false);
|
|
3114
|
+
throw err;
|
|
3115
|
+
}
|
|
3116
|
+
stopSpinner(true);
|
|
3117
|
+
process.stderr.write(import_chalk5.default.green(`\u2713 Floating IP "${id}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
|
|
3118
|
+
`));
|
|
3119
|
+
});
|
|
1408
3120
|
|
|
1409
|
-
// src/commands/instance/
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
3121
|
+
// src/commands/instance/flavors.ts
|
|
3122
|
+
var import_commander22 = require("commander");
|
|
3123
|
+
function parseNonNegInt(value, flag) {
|
|
3124
|
+
if (value === void 0) return void 0;
|
|
3125
|
+
const n = Number(value);
|
|
3126
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
3127
|
+
throw new NhnCloudCliError(`${flag} \uB294 0 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${value}).`, EXIT_PARAM_ERROR);
|
|
3128
|
+
}
|
|
3129
|
+
return n;
|
|
1416
3130
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
3131
|
+
var flavorsCommand = new import_commander22.Command("flavors").description("\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785(flavor)\uC744 \uC870\uD68C\uD55C\uB2E4 (\uAE30\uBCF8 id\xB7name, --detail \uB85C \uC2A4\uD399, \uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--detail", "vcpus\xB7ram\xB7disk \uB4F1 \uC2A4\uD399 \uD3EC\uD568 (GET /flavors/detail)").option("--min-disk <gb>", "\uCD5C\uC18C \uBE14\uB85D \uC2A4\uD1A0\uB9AC\uC9C0 \uD06C\uAE30(GB) \uC774\uC0C1\uB9CC \uD544\uD130").option("--min-ram <mb>", "\uCD5C\uC18C RAM \uD06C\uAE30(MB) \uC774\uC0C1\uB9CC \uD544\uD130").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
3132
|
+
const opts = cmd.optsWithGlobals();
|
|
3133
|
+
const minDisk = parseNonNegInt(opts.minDisk, "--min-disk");
|
|
3134
|
+
const minRam = parseNonNegInt(opts.minRam, "--min-ram");
|
|
3135
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3136
|
+
startSpinner("\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785 \uC870\uD68C \uC911...");
|
|
3137
|
+
try {
|
|
3138
|
+
if (opts.detail) {
|
|
3139
|
+
const flavors = await client.listFlavors({ detail: true, minDisk, minRam });
|
|
3140
|
+
stopSpinner(true);
|
|
3141
|
+
printFlavors(opts, flavors);
|
|
3142
|
+
} else {
|
|
3143
|
+
const flavors = await client.listFlavors({ minDisk, minRam });
|
|
3144
|
+
stopSpinner(true);
|
|
3145
|
+
printFlavors(opts, flavors);
|
|
3146
|
+
}
|
|
3147
|
+
} catch (err) {
|
|
3148
|
+
stopSpinner(false);
|
|
3149
|
+
throw err;
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
function printFlavors(opts, flavors) {
|
|
3153
|
+
if (isDetailList(flavors)) {
|
|
3154
|
+
output(opts, {
|
|
3155
|
+
headers: ["id", "name", "vcpus", "ram(MB)", "disk(GB)"],
|
|
3156
|
+
rows: flavors.map((f) => [f.id, f.name, String(f.vcpus), String(f.ram), String(f.disk)]),
|
|
3157
|
+
raw: flavors,
|
|
3158
|
+
ids: flavors.map((f) => f.id)
|
|
3159
|
+
});
|
|
3160
|
+
} else {
|
|
3161
|
+
output(opts, {
|
|
3162
|
+
headers: ["id", "name"],
|
|
3163
|
+
rows: flavors.map((f) => [f.id, f.name]),
|
|
3164
|
+
raw: flavors,
|
|
3165
|
+
ids: flavors.map((f) => f.id)
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
1421
3168
|
}
|
|
1422
|
-
|
|
3169
|
+
function isDetailList(flavors) {
|
|
3170
|
+
return flavors.length > 0 && "vcpus" in flavors[0];
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// src/commands/instance/availability-zones.ts
|
|
3174
|
+
var import_commander23 = require("commander");
|
|
3175
|
+
var availabilityZonesCommand = new import_commander23.Command("availability-zones").description("\uAC00\uC6A9\uC131 \uC601\uC5ED(availability zone) \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (zoneName\xB7available)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
1423
3176
|
const opts = cmd.optsWithGlobals();
|
|
1424
3177
|
const { client } = await resolveInstanceClient(opts);
|
|
1425
|
-
startSpinner("\
|
|
1426
|
-
let
|
|
3178
|
+
startSpinner("\uAC00\uC6A9\uC131 \uC601\uC5ED \uC870\uD68C \uC911...");
|
|
3179
|
+
let zones;
|
|
1427
3180
|
try {
|
|
1428
|
-
|
|
3181
|
+
zones = await client.listAvailabilityZones();
|
|
1429
3182
|
} catch (err) {
|
|
1430
3183
|
stopSpinner(false);
|
|
1431
3184
|
throw err;
|
|
1432
3185
|
}
|
|
1433
3186
|
stopSpinner(true);
|
|
1434
3187
|
output(opts, {
|
|
1435
|
-
headers: ["
|
|
1436
|
-
rows:
|
|
1437
|
-
raw:
|
|
1438
|
-
ids:
|
|
3188
|
+
headers: ["zoneName", "available"],
|
|
3189
|
+
rows: zones.map((z) => [z.zoneName, String(z.zoneState.available)]),
|
|
3190
|
+
raw: zones,
|
|
3191
|
+
ids: zones.map((z) => z.zoneName)
|
|
1439
3192
|
});
|
|
1440
3193
|
});
|
|
1441
3194
|
|
|
1442
3195
|
// src/commands/instance/get.ts
|
|
1443
|
-
var
|
|
3196
|
+
var import_commander24 = require("commander");
|
|
1444
3197
|
function getIps2(server) {
|
|
1445
3198
|
return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
|
|
1446
3199
|
}
|
|
1447
3200
|
function getImageId(server) {
|
|
1448
3201
|
return typeof server.image === "object" ? server.image.id : "";
|
|
1449
3202
|
}
|
|
1450
|
-
var
|
|
3203
|
+
var getCommand2 = new import_commander24.Command("get").description("\uB2E8\uC77C \uC778\uC2A4\uD134\uC2A4 \uC0C1\uD0DC\uB97C \uC870\uD68C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
1451
3204
|
const opts = cmd.optsWithGlobals();
|
|
1452
3205
|
const { client } = await resolveInstanceClient(opts);
|
|
1453
3206
|
startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC870\uD68C \uC911...");
|
|
@@ -1479,8 +3232,9 @@ var getCommand = new import_commander8.Command("get").description("\uB2E8\uC77C
|
|
|
1479
3232
|
});
|
|
1480
3233
|
|
|
1481
3234
|
// src/commands/instance/create.ts
|
|
1482
|
-
var
|
|
1483
|
-
var
|
|
3235
|
+
var import_commander25 = require("commander");
|
|
3236
|
+
var import_node_fs5 = require("fs");
|
|
3237
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
1484
3238
|
function getFirstIp(server) {
|
|
1485
3239
|
for (const list of Object.values(server.addresses)) {
|
|
1486
3240
|
for (const addr of list) {
|
|
@@ -1495,12 +3249,45 @@ function getIps3(server) {
|
|
|
1495
3249
|
function getImageId2(server) {
|
|
1496
3250
|
return typeof server.image === "object" ? server.image.id : "";
|
|
1497
3251
|
}
|
|
1498
|
-
var
|
|
3252
|
+
var createCommand3 = new import_commander25.Command("create").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD55C\uB2E4").requiredOption("--name <name>", "\uC778\uC2A4\uD134\uC2A4 \uC774\uB984").requiredOption("--flavor <id>", "flavor ID").requiredOption("--image <id>", "\uC774\uBBF8\uC9C0 ID").requiredOption("--network <uuid>", "\uB124\uD2B8\uC6CC\uD06C UUID (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--boot-volume-size <gb>", "boot-from-volume root \uBCFC\uB968 \uD06C\uAE30 (GB). GPU(g2) \uB4F1 boot-from-volume \uD544\uC218 flavor \uC5D0 \uC9C0\uC815").option("--key-name <name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--security-group <name>", "\uBCF4\uC548 \uADF8\uB8F9 \uC774\uB984 (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--ephemeral-disk-size <gb>", "\uC784\uC2DC \uB514\uC2A4\uD06C \uD06C\uAE30 (GB, NHN \uD655\uC7A5)").option("--protect", "\uC0AD\uC81C \uBC29\uC9C0 \uC124\uC815 (NHN \uD655\uC7A5)").option("--user-data <path>", "cloud-init user-data \uD30C\uC77C \uACBD\uB85C (base64 \uC778\uCF54\uB529\uD574 \uC8FC\uC785, \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8 \uD55C\uB3C4)").option("--wait", "ACTIVE \uC0C1\uD0DC\uAC00 \uB420 \uB54C\uAE4C\uC9C0 \uB300\uAE30").option("--timeout <sec>", "wait \uD0C0\uC784\uC544\uC6C3 (\uCD08, \uAE30\uBCF8 300)", "300").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
1499
3253
|
const opts = cmd.optsWithGlobals();
|
|
1500
3254
|
const networks = opts.network ?? [];
|
|
1501
3255
|
if (networks.length === 0) {
|
|
1502
3256
|
throw new NhnCloudCliError("--network \uB294 \uCD5C\uC18C 1\uAC1C \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
|
|
1503
3257
|
}
|
|
3258
|
+
let userDataBase64;
|
|
3259
|
+
if (opts.userData !== void 0) {
|
|
3260
|
+
let stat;
|
|
3261
|
+
try {
|
|
3262
|
+
stat = (0, import_node_fs5.statSync)(opts.userData);
|
|
3263
|
+
} catch (e) {
|
|
3264
|
+
const reason = e.code ?? (e instanceof Error ? e.message : String(e));
|
|
3265
|
+
throw new NhnCloudCliError(
|
|
3266
|
+
`--user-data \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.userData} (${reason})`,
|
|
3267
|
+
EXIT_PARAM_ERROR
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
if (!stat.isFile()) {
|
|
3271
|
+
throw new NhnCloudCliError(
|
|
3272
|
+
`--user-data \uAC00 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${opts.userData}`,
|
|
3273
|
+
EXIT_PARAM_ERROR
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
3276
|
+
if (stat.size > 49149) {
|
|
3277
|
+
throw new NhnCloudCliError(
|
|
3278
|
+
`--user-data \uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${stat.size} \uBC14\uC774\uD2B8). base64 \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8 \uD55C\uB3C4\uB77C raw 49149 \uBC14\uC774\uD2B8\uAE4C\uC9C0\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4.`,
|
|
3279
|
+
EXIT_PARAM_ERROR
|
|
3280
|
+
);
|
|
3281
|
+
}
|
|
3282
|
+
const raw = (0, import_node_fs5.readFileSync)(opts.userData);
|
|
3283
|
+
userDataBase64 = raw.toString("base64");
|
|
3284
|
+
if (userDataBase64.length > 65535) {
|
|
3285
|
+
throw new NhnCloudCliError(
|
|
3286
|
+
`--user-data \uAC00 base64 \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4 (${userDataBase64.length} \uBC14\uC774\uD2B8). cloud-init \uB0B4\uC6A9\uC744 \uC904\uC774\uC138\uC694.`,
|
|
3287
|
+
EXIT_PARAM_ERROR
|
|
3288
|
+
);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
1504
3291
|
const timeoutMs = parseInt(opts.timeout ?? "300", 10) * 1e3;
|
|
1505
3292
|
const { client } = await resolveInstanceClient(opts);
|
|
1506
3293
|
startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131 \uC911...");
|
|
@@ -1515,7 +3302,8 @@ var createCommand = new import_commander9.Command("create").description("\uC778\
|
|
|
1515
3302
|
keyName: opts.keyName,
|
|
1516
3303
|
securityGroups: opts.securityGroup && opts.securityGroup.length > 0 ? opts.securityGroup : void 0,
|
|
1517
3304
|
ephemeralDiskSize: opts.ephemeralDiskSize !== void 0 ? parseInt(opts.ephemeralDiskSize, 10) : void 0,
|
|
1518
|
-
protect: opts.protect
|
|
3305
|
+
protect: opts.protect,
|
|
3306
|
+
userDataBase64
|
|
1519
3307
|
});
|
|
1520
3308
|
} catch (err) {
|
|
1521
3309
|
stopSpinner(false);
|
|
@@ -1538,7 +3326,7 @@ var createCommand = new import_commander9.Command("create").description("\uC778\
|
|
|
1538
3326
|
return;
|
|
1539
3327
|
}
|
|
1540
3328
|
if (opts.wait) {
|
|
1541
|
-
process.stderr.write(
|
|
3329
|
+
process.stderr.write(import_chalk6.default.green(` IP: ${getIps3(server)}
|
|
1542
3330
|
`));
|
|
1543
3331
|
}
|
|
1544
3332
|
const rows = [
|
|
@@ -1558,9 +3346,9 @@ var createCommand = new import_commander9.Command("create").description("\uC778\
|
|
|
1558
3346
|
});
|
|
1559
3347
|
|
|
1560
3348
|
// src/commands/instance/delete.ts
|
|
1561
|
-
var
|
|
1562
|
-
var
|
|
1563
|
-
var
|
|
3349
|
+
var import_commander26 = require("commander");
|
|
3350
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
3351
|
+
var deleteCommand2 = new import_commander26.Command("delete").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC0AD\uC81C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--yes", "\uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uC0DD\uB7B5 (CI/\uBE44\uB300\uD654\uD615 \uD544\uC218)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
1564
3352
|
const opts = cmd.optsWithGlobals();
|
|
1565
3353
|
const isTTY = process.stdin.isTTY;
|
|
1566
3354
|
if (!isTTY && !opts.yes) {
|
|
@@ -1576,7 +3364,7 @@ var deleteCommand = new import_commander10.Command("delete").description("\uC778
|
|
|
1576
3364
|
default: false
|
|
1577
3365
|
});
|
|
1578
3366
|
if (!ok) {
|
|
1579
|
-
process.stderr.write(
|
|
3367
|
+
process.stderr.write(import_chalk7.default.yellow("\uC0AD\uC81C\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
|
|
1580
3368
|
return;
|
|
1581
3369
|
}
|
|
1582
3370
|
}
|
|
@@ -1589,41 +3377,474 @@ var deleteCommand = new import_commander10.Command("delete").description("\uC778
|
|
|
1589
3377
|
throw err;
|
|
1590
3378
|
}
|
|
1591
3379
|
stopSpinner(true);
|
|
1592
|
-
process.stderr.write(
|
|
3380
|
+
process.stderr.write(import_chalk7.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
|
|
3381
|
+
`));
|
|
3382
|
+
});
|
|
3383
|
+
|
|
3384
|
+
// src/commands/instance/power.ts
|
|
3385
|
+
var import_commander27 = require("commander");
|
|
3386
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
3387
|
+
var startCommand = new import_commander27.Command("start").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC2DC\uC791\uD55C\uB2E4 (SHUTOFF \u2192 ACTIVE)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
3388
|
+
const opts = cmd.optsWithGlobals();
|
|
3389
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3390
|
+
startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC2DC\uC791 \uC911... (id: ${id})`);
|
|
3391
|
+
try {
|
|
3392
|
+
await client.start(id);
|
|
3393
|
+
} catch (err) {
|
|
3394
|
+
stopSpinner(false);
|
|
3395
|
+
throw err;
|
|
3396
|
+
}
|
|
3397
|
+
stopSpinner(true);
|
|
3398
|
+
process.stderr.write(import_chalk8.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uC2DC\uC791\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 ACTIVE).
|
|
3399
|
+
`));
|
|
3400
|
+
});
|
|
3401
|
+
var stopCommand = new import_commander27.Command("stop").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC815\uC9C0\uD55C\uB2E4 (ACTIVE/ERROR \u2192 SHUTOFF)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
3402
|
+
const opts = cmd.optsWithGlobals();
|
|
3403
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3404
|
+
startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC815\uC9C0 \uC911... (id: ${id})`);
|
|
3405
|
+
try {
|
|
3406
|
+
await client.stop(id);
|
|
3407
|
+
} catch (err) {
|
|
3408
|
+
stopSpinner(false);
|
|
3409
|
+
throw err;
|
|
3410
|
+
}
|
|
3411
|
+
stopSpinner(true);
|
|
3412
|
+
process.stderr.write(import_chalk8.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uC815\uC9C0\uB97C \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 SHUTOFF).
|
|
3413
|
+
`));
|
|
3414
|
+
});
|
|
3415
|
+
var rebootCommand = new import_commander27.Command("reboot").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC7AC\uBD80\uD305\uD55C\uB2E4 (\uAE30\uBCF8 SOFT, --hard \uB85C HARD)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--hard", "HARD \uC7AC\uBD80\uD305 (\uAC15\uC81C \uC804\uC6D0 cycle, \uAE30\uBCF8\uC740 SOFT)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
3416
|
+
const opts = cmd.optsWithGlobals();
|
|
3417
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3418
|
+
const type = opts.hard ? "HARD" : "SOFT";
|
|
3419
|
+
startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC7AC\uBD80\uD305 \uC911... (id: ${id}, ${type})`);
|
|
3420
|
+
try {
|
|
3421
|
+
await client.reboot(id, type);
|
|
3422
|
+
} catch (err) {
|
|
3423
|
+
stopSpinner(false);
|
|
3424
|
+
throw err;
|
|
3425
|
+
}
|
|
3426
|
+
stopSpinner(true);
|
|
3427
|
+
process.stderr.write(import_chalk8.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" ${type} \uC7AC\uBD80\uD305\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4.
|
|
3428
|
+
`));
|
|
3429
|
+
});
|
|
3430
|
+
|
|
3431
|
+
// src/commands/instance/resize.ts
|
|
3432
|
+
var import_commander28 = require("commander");
|
|
3433
|
+
var import_chalk9 = __toESM(require("chalk"));
|
|
3434
|
+
var resizeCommand = new import_commander28.Command("resize").description("\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785(flavor)\uC744 \uBCC0\uACBD\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").requiredOption("--flavor <id>", "\uBCC0\uACBD\uD560 flavor ID (instance flavors \uB85C \uD6C4\uBCF4 \uC870\uD68C)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
3435
|
+
const opts = cmd.optsWithGlobals();
|
|
3436
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3437
|
+
startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785 \uBCC0\uACBD \uC911... (id: ${id})`);
|
|
3438
|
+
try {
|
|
3439
|
+
await client.resize(id, opts.flavor);
|
|
3440
|
+
} catch (err) {
|
|
3441
|
+
stopSpinner(false);
|
|
3442
|
+
throw err;
|
|
3443
|
+
}
|
|
3444
|
+
stopSpinner(true);
|
|
3445
|
+
process.stderr.write(
|
|
3446
|
+
import_chalk9.default.green(
|
|
3447
|
+
`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uD0C0\uC785 \uBCC0\uACBD(flavor: ${opts.flavor}) \uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 VERIFY_RESIZE, instance get \uC73C\uB85C \uD655\uC778 \uD6C4 resize-confirm/resize-revert).
|
|
3448
|
+
`
|
|
3449
|
+
)
|
|
3450
|
+
);
|
|
3451
|
+
});
|
|
3452
|
+
var resizeConfirmCommand = new import_commander28.Command("resize-confirm").description("resize \uB97C \uD655\uC815\uD55C\uB2E4 (VERIFY_RESIZE \u2192 ACTIVE, \uC0C8 flavor \uB85C \uACE0\uC815)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
3453
|
+
const opts = cmd.optsWithGlobals();
|
|
3454
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3455
|
+
startSpinner(`resize \uD655\uC815 \uC911... (id: ${id})`);
|
|
3456
|
+
try {
|
|
3457
|
+
await client.confirmResize(id);
|
|
3458
|
+
} catch (err) {
|
|
3459
|
+
stopSpinner(false);
|
|
3460
|
+
throw err;
|
|
3461
|
+
}
|
|
3462
|
+
stopSpinner(true);
|
|
3463
|
+
process.stderr.write(import_chalk9.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" resize \uD655\uC815\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 ACTIVE).
|
|
3464
|
+
`));
|
|
3465
|
+
});
|
|
3466
|
+
var resizeRevertCommand = new import_commander28.Command("resize-revert").description("resize \uB97C \uB864\uBC31\uD55C\uB2E4 (VERIFY_RESIZE \u2192 ACTIVE, \uC774\uC804 flavor \uB85C \uBCF5\uADC0)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
3467
|
+
const opts = cmd.optsWithGlobals();
|
|
3468
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3469
|
+
startSpinner(`resize \uB864\uBC31 \uC911... (id: ${id})`);
|
|
3470
|
+
try {
|
|
3471
|
+
await client.revertResize(id);
|
|
3472
|
+
} catch (err) {
|
|
3473
|
+
stopSpinner(false);
|
|
3474
|
+
throw err;
|
|
3475
|
+
}
|
|
3476
|
+
stopSpinner(true);
|
|
3477
|
+
process.stderr.write(
|
|
3478
|
+
import_chalk9.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" resize \uB864\uBC31\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\uC774\uC804 flavor \uB85C \uBCF5\uADC0).
|
|
3479
|
+
`)
|
|
3480
|
+
);
|
|
3481
|
+
});
|
|
3482
|
+
|
|
3483
|
+
// src/commands/instance/images.ts
|
|
3484
|
+
var import_commander29 = require("commander");
|
|
3485
|
+
var VISIBILITY_VALUES = ["public", "private", "shared"];
|
|
3486
|
+
function parsePositiveInt5(value, flag) {
|
|
3487
|
+
if (value === void 0) return void 0;
|
|
3488
|
+
const n = Number(value);
|
|
3489
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
3490
|
+
throw new NhnCloudCliError(`${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${value}).`, EXIT_PARAM_ERROR);
|
|
3491
|
+
}
|
|
3492
|
+
return n;
|
|
3493
|
+
}
|
|
3494
|
+
var imagesCommand = new import_commander29.Command("images").description("\uC774\uBBF8\uC9C0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (create --image <id> \uC18C\uC2A4, \uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--limit <n>", "\uD55C \uD398\uC774\uC9C0 \uCD5C\uB300 \uAC1C\uC218 (\uAE30\uBCF8: \uC11C\uBC84 \uAE30\uBCF8\uAC12 25)").option("--marker <id>", "\uC774 image id \uB2E4\uC74C\uBD80\uD130 \uC870\uD68C (\uD398\uC774\uC9C0\uB124\uC774\uC158)").option("--name <name>", "\uC774\uB984\uC73C\uB85C \uD544\uD130").option("--visibility <v>", `\uB178\uCD9C \uBC94\uC704 \uD544\uD130 (${VISIBILITY_VALUES.join("|")})`).option("--owner <id>", "\uC18C\uC720\uC790(\uD504\uB85C\uC81D\uD2B8 id)\uB85C \uD544\uD130").option("--status <status>", "\uC0C1\uD0DC\uB85C \uD544\uD130 (\uC608: active)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
3495
|
+
const opts = cmd.optsWithGlobals();
|
|
3496
|
+
const limit = parsePositiveInt5(opts.limit, "--limit");
|
|
3497
|
+
if (opts.visibility !== void 0 && !VISIBILITY_VALUES.includes(opts.visibility)) {
|
|
3498
|
+
throw new NhnCloudCliError(
|
|
3499
|
+
`--visibility \uB294 ${VISIBILITY_VALUES.join(" | ")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${opts.visibility}).`,
|
|
3500
|
+
EXIT_PARAM_ERROR
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3504
|
+
startSpinner("\uC774\uBBF8\uC9C0 \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
3505
|
+
let result;
|
|
3506
|
+
try {
|
|
3507
|
+
result = await client.listImages({
|
|
3508
|
+
limit,
|
|
3509
|
+
marker: opts.marker,
|
|
3510
|
+
name: opts.name,
|
|
3511
|
+
visibility: opts.visibility,
|
|
3512
|
+
owner: opts.owner,
|
|
3513
|
+
status: opts.status
|
|
3514
|
+
});
|
|
3515
|
+
} catch (err) {
|
|
3516
|
+
stopSpinner(false);
|
|
3517
|
+
throw err;
|
|
3518
|
+
}
|
|
3519
|
+
stopSpinner(true);
|
|
3520
|
+
output(opts, {
|
|
3521
|
+
headers: ["id", "name", "status", "visibility", "size"],
|
|
3522
|
+
rows: result.images.map((img) => [
|
|
3523
|
+
img.id,
|
|
3524
|
+
img.name ?? "-",
|
|
3525
|
+
img.status,
|
|
3526
|
+
img.visibility,
|
|
3527
|
+
img.size === void 0 ? "-" : String(img.size)
|
|
3528
|
+
]),
|
|
3529
|
+
raw: result.images,
|
|
3530
|
+
ids: result.images.map((img) => img.id)
|
|
3531
|
+
});
|
|
3532
|
+
if (result.next && !opts.json && !opts.quiet) {
|
|
3533
|
+
const lastId = result.images.at(-1)?.id;
|
|
3534
|
+
if (lastId) {
|
|
3535
|
+
process.stderr.write(`\uB2E4\uC74C \uD398\uC774\uC9C0: --marker ${lastId}
|
|
3536
|
+
`);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
});
|
|
3540
|
+
|
|
3541
|
+
// src/commands/instance/keypairs.ts
|
|
3542
|
+
var import_commander30 = require("commander");
|
|
3543
|
+
var keypairsCommand = new import_commander30.Command("keypairs").description("\uD0A4\uD398\uC5B4 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (name\xB7fingerprint, \uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
|
|
3544
|
+
const opts = cmd.optsWithGlobals();
|
|
3545
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3546
|
+
startSpinner("\uD0A4\uD398\uC5B4 \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
3547
|
+
let keypairs;
|
|
3548
|
+
try {
|
|
3549
|
+
keypairs = await client.listKeypairs();
|
|
3550
|
+
} catch (err) {
|
|
3551
|
+
stopSpinner(false);
|
|
3552
|
+
throw err;
|
|
3553
|
+
}
|
|
3554
|
+
stopSpinner(true);
|
|
3555
|
+
output(opts, {
|
|
3556
|
+
headers: ["name", "fingerprint"],
|
|
3557
|
+
rows: keypairs.map((k) => [k.name, k.fingerprint]),
|
|
3558
|
+
raw: keypairs,
|
|
3559
|
+
ids: keypairs.map((k) => k.name)
|
|
3560
|
+
});
|
|
3561
|
+
});
|
|
3562
|
+
|
|
3563
|
+
// src/commands/instance/keypair.ts
|
|
3564
|
+
var import_commander31 = require("commander");
|
|
3565
|
+
var import_node_fs6 = require("fs");
|
|
3566
|
+
var import_node_crypto3 = require("crypto");
|
|
3567
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
3568
|
+
function resolvePublicKey(value) {
|
|
3569
|
+
let stat;
|
|
3570
|
+
try {
|
|
3571
|
+
stat = (0, import_node_fs6.statSync)(value);
|
|
3572
|
+
} catch (e) {
|
|
3573
|
+
const err = e;
|
|
3574
|
+
if (err.code && err.code !== "ENOENT") {
|
|
3575
|
+
throw new NhnCloudCliError(
|
|
3576
|
+
`--public-key \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${value} (${err.code})`,
|
|
3577
|
+
EXIT_PARAM_ERROR
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
return value;
|
|
3581
|
+
}
|
|
3582
|
+
if (!stat.isFile()) {
|
|
3583
|
+
throw new NhnCloudCliError(`--public-key \uAC00 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${value}`, EXIT_PARAM_ERROR);
|
|
3584
|
+
}
|
|
3585
|
+
return (0, import_node_fs6.readFileSync)(value, "utf-8").trim();
|
|
3586
|
+
}
|
|
3587
|
+
function savePrivateKey(filePath, privateKey) {
|
|
3588
|
+
const tmp = `${filePath}.${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.tmp`;
|
|
3589
|
+
const content = privateKey.endsWith("\n") ? privateKey : privateKey + "\n";
|
|
3590
|
+
try {
|
|
3591
|
+
(0, import_node_fs6.writeFileSync)(tmp, content, { encoding: "utf-8", mode: 384 });
|
|
3592
|
+
(0, import_node_fs6.renameSync)(tmp, filePath);
|
|
3593
|
+
} catch (e) {
|
|
3594
|
+
const reason = e.code ?? (e instanceof Error ? e.message : String(e));
|
|
3595
|
+
let tmpNote = "";
|
|
3596
|
+
try {
|
|
3597
|
+
(0, import_node_fs6.unlinkSync)(tmp);
|
|
3598
|
+
} catch {
|
|
3599
|
+
tmpNote = ` \u2014 0600 \uC784\uC2DC \uD30C\uC77C\uC774 \uB0A8\uC558\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4: ${tmp}`;
|
|
3600
|
+
}
|
|
3601
|
+
throw new NhnCloudCliError(
|
|
3602
|
+
`private_key \uD30C\uC77C\uC744 \uC800\uC7A5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath} (${reason})${tmpNote}`,
|
|
3603
|
+
EXIT_PARAM_ERROR
|
|
3604
|
+
);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
var getKeypairCmd = new import_commander31.Command("get").description("\uB2E8\uC77C \uD0A4\uD398\uC5B4\uB97C \uC870\uD68C\uD55C\uB2E4").argument("<name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--region <region>", "region override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (name, _opts, cmd) => {
|
|
3608
|
+
const opts = cmd.optsWithGlobals();
|
|
3609
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3610
|
+
startSpinner("\uD0A4\uD398\uC5B4 \uC870\uD68C \uC911...");
|
|
3611
|
+
let kp;
|
|
3612
|
+
try {
|
|
3613
|
+
kp = await client.getKeypair(name);
|
|
3614
|
+
} catch (err) {
|
|
3615
|
+
stopSpinner(false);
|
|
3616
|
+
throw err;
|
|
3617
|
+
}
|
|
3618
|
+
stopSpinner(true);
|
|
3619
|
+
output(opts, {
|
|
3620
|
+
headers: ["field", "value"],
|
|
3621
|
+
rows: [
|
|
3622
|
+
["name", kp.name],
|
|
3623
|
+
["fingerprint", kp.fingerprint],
|
|
3624
|
+
["user_id", kp.user_id],
|
|
3625
|
+
["created_at", kp.created_at],
|
|
3626
|
+
["public_key", kp.public_key]
|
|
3627
|
+
],
|
|
3628
|
+
raw: kp,
|
|
3629
|
+
ids: [kp.name]
|
|
3630
|
+
});
|
|
3631
|
+
});
|
|
3632
|
+
var createKeypairCmd = new import_commander31.Command("create").description("\uD0A4\uD398\uC5B4\uB97C \uC0DD\uC131\uD55C\uB2E4 (--public-key \uBBF8\uC9C0\uC815 \uC2DC NHN \uC774 \uD0A4\uC30D \uC0DD\uC131 \u2014 private_key 1\uD68C\uC131)").argument("<name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--public-key <path|key>", "\uAE30\uC874 \uACF5\uAC1C\uD0A4 (\uD30C\uC77C \uACBD\uB85C \uB610\uB294 \uD0A4 \uBB38\uC790\uC5F4). \uC9C0\uC815 \uC2DC private_key \uBBF8\uBC18\uD658").option("-o, --output <keyfile>", "\uC0DD\uC131\uB41C private_key \uB97C \uD30C\uC77C(mode 0600)\uB85C \uC800\uC7A5").option("--region <region>", "region override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (name, _opts, cmd) => {
|
|
3633
|
+
const opts = cmd.optsWithGlobals();
|
|
3634
|
+
const publicKey = opts.publicKey !== void 0 ? resolvePublicKey(opts.publicKey) : void 0;
|
|
3635
|
+
if (opts.output !== void 0 && publicKey !== void 0) {
|
|
3636
|
+
throw new NhnCloudCliError(
|
|
3637
|
+
"--output \uC740 NHN \uC774 \uD0A4\uB97C \uC0DD\uC131\uD560 \uB54C\uB9CC \uC758\uBBF8\uAC00 \uC788\uC2B5\uB2C8\uB2E4. --public-key \uC640 \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
3638
|
+
EXIT_PARAM_ERROR
|
|
3639
|
+
);
|
|
3640
|
+
}
|
|
3641
|
+
if (publicKey === void 0 && opts.quiet && opts.output === void 0) {
|
|
3642
|
+
throw new NhnCloudCliError(
|
|
3643
|
+
"NHN \uC774 \uC0DD\uC131\uD558\uB294 private_key \uB294 1\uD68C\uB9CC \uBC18\uD658\uB429\uB2C8\uB2E4. --quiet \uB85C \uC0DD\uC131\uD560 \uB54C\uB294 --output <keyfile> \uB85C \uC800\uC7A5 \uACBD\uB85C\uB97C \uC9C0\uC815\uD558\uC138\uC694 (\uBBF8\uC9C0\uC815 \uC2DC \uD0A4 \uC720\uC2E4).",
|
|
3644
|
+
EXIT_PARAM_ERROR
|
|
3645
|
+
);
|
|
3646
|
+
}
|
|
3647
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3648
|
+
startSpinner("\uD0A4\uD398\uC5B4 \uC0DD\uC131 \uC911...");
|
|
3649
|
+
let result;
|
|
3650
|
+
try {
|
|
3651
|
+
result = await client.createKeypair({ name, publicKey });
|
|
3652
|
+
} catch (err) {
|
|
3653
|
+
stopSpinner(false);
|
|
3654
|
+
throw err;
|
|
3655
|
+
}
|
|
3656
|
+
stopSpinner(true);
|
|
3657
|
+
if (result.private_key !== void 0) {
|
|
3658
|
+
if (opts.output !== void 0) {
|
|
3659
|
+
try {
|
|
3660
|
+
savePrivateKey(opts.output, result.private_key);
|
|
3661
|
+
process.stderr.write(import_chalk10.default.green(` private_key \uB97C ${opts.output} \uC5D0 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (mode 0600).
|
|
3662
|
+
`));
|
|
3663
|
+
} catch (saveErr) {
|
|
3664
|
+
const reason = saveErr instanceof Error ? saveErr.message : String(saveErr);
|
|
3665
|
+
process.stderr.write(
|
|
3666
|
+
import_chalk10.default.red(` \u26A0 private_key \uD30C\uC77C \uC800\uC7A5 \uC2E4\uD328 (${reason}). \uC720\uC2E4 \uBC29\uC9C0\uB97C \uC704\uD574 \uC544\uB798\uC5D0 \uCD9C\uB825\uD569\uB2C8\uB2E4 \u2014 \uC989\uC2DC \uC548\uC804\uD55C \uACF3\uC5D0 \uBCF4\uAD00\uD558\uC138\uC694.
|
|
3667
|
+
`)
|
|
3668
|
+
);
|
|
3669
|
+
process.stdout.write(result.private_key + "\n");
|
|
3670
|
+
}
|
|
3671
|
+
} else {
|
|
3672
|
+
process.stderr.write(
|
|
3673
|
+
import_chalk10.default.yellow(" \u26A0 private_key \uB294 \uC9C0\uAE08 \uD55C \uBC88\uB9CC \uD45C\uC2DC\uB429\uB2C8\uB2E4. \uBD84\uC2E4 \uC2DC \uBCF5\uAD6C \uBD88\uAC00 \u2014 \uC548\uC804\uD55C \uACF3\uC5D0 \uBCF4\uAD00\uD558\uC138\uC694.\n")
|
|
3674
|
+
);
|
|
3675
|
+
process.stdout.write(result.private_key + "\n");
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
const { private_key, ...meta } = result;
|
|
3679
|
+
void private_key;
|
|
3680
|
+
output(opts, {
|
|
3681
|
+
headers: ["field", "value"],
|
|
3682
|
+
rows: [
|
|
3683
|
+
["name", meta.name],
|
|
3684
|
+
["fingerprint", meta.fingerprint],
|
|
3685
|
+
["user_id", meta.user_id]
|
|
3686
|
+
],
|
|
3687
|
+
raw: meta,
|
|
3688
|
+
ids: [meta.name]
|
|
3689
|
+
});
|
|
3690
|
+
});
|
|
3691
|
+
var deleteKeypairCmd = new import_commander31.Command("delete").description("\uD0A4\uD398\uC5B4\uB97C \uC0AD\uC81C\uD55C\uB2E4").argument("<name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--region <region>", "region override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (name, _opts, cmd) => {
|
|
3692
|
+
const opts = cmd.optsWithGlobals();
|
|
3693
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3694
|
+
startSpinner("\uD0A4\uD398\uC5B4 \uC0AD\uC81C \uC911...");
|
|
3695
|
+
try {
|
|
3696
|
+
await client.deleteKeypair(name);
|
|
3697
|
+
} catch (err) {
|
|
3698
|
+
stopSpinner(false);
|
|
3699
|
+
throw err;
|
|
3700
|
+
}
|
|
3701
|
+
stopSpinner(true);
|
|
3702
|
+
process.stderr.write(import_chalk10.default.green(`\u2713 \uD0A4\uD398\uC5B4 "${name}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
|
|
1593
3703
|
`));
|
|
1594
3704
|
});
|
|
3705
|
+
var keypairCommand = new import_commander31.Command("keypair").description("\uD0A4\uD398\uC5B4 \uB2E8\uAC74 \uAD00\uB9AC (get / create / delete)").addCommand(getKeypairCmd).addCommand(createKeypairCmd).addCommand(deleteKeypairCmd);
|
|
3706
|
+
|
|
3707
|
+
// src/commands/instance/volume.ts
|
|
3708
|
+
var import_commander32 = require("commander");
|
|
3709
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
3710
|
+
var attachCommand = new import_commander32.Command("attach").description("\uBCFC\uB968\uC744 \uC778\uC2A4\uD134\uC2A4\uC5D0 \uC5F0\uACB0\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").requiredOption("--volume <volumeId>", "\uC5F0\uACB0\uD560 \uBCFC\uB968 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
3711
|
+
const opts = cmd.optsWithGlobals();
|
|
3712
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3713
|
+
startSpinner("\uBCFC\uB968 \uC5F0\uACB0 \uC911...");
|
|
3714
|
+
let att;
|
|
3715
|
+
try {
|
|
3716
|
+
att = await client.attachVolume(id, opts.volume);
|
|
3717
|
+
} catch (err) {
|
|
3718
|
+
stopSpinner(false);
|
|
3719
|
+
throw err;
|
|
3720
|
+
}
|
|
3721
|
+
stopSpinner(true);
|
|
3722
|
+
process.stderr.write(
|
|
3723
|
+
import_chalk11.default.green(`\uBCFC\uB968 \uC5F0\uACB0 \uC644\uB8CC (volumeId: ${att.volumeId}, device: ${att.device})
|
|
3724
|
+
`)
|
|
3725
|
+
);
|
|
3726
|
+
output(opts, {
|
|
3727
|
+
headers: ["field", "value"],
|
|
3728
|
+
rows: [
|
|
3729
|
+
["id", att.id],
|
|
3730
|
+
["volumeId", att.volumeId],
|
|
3731
|
+
["serverId", att.serverId],
|
|
3732
|
+
["device", att.device]
|
|
3733
|
+
],
|
|
3734
|
+
raw: att,
|
|
3735
|
+
ids: [att.id]
|
|
3736
|
+
});
|
|
3737
|
+
});
|
|
3738
|
+
var detachCommand = new import_commander32.Command("detach").description("\uBCFC\uB968 \uC5F0\uACB0\uC744 \uD574\uC81C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").argument("<volumeId>", "\uD574\uC81C\uD560 \uBCFC\uB968 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, volumeId, _opts, cmd) => {
|
|
3739
|
+
const opts = cmd.optsWithGlobals();
|
|
3740
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3741
|
+
startSpinner("\uBCFC\uB968 \uC5F0\uACB0 \uD574\uC81C \uC911...");
|
|
3742
|
+
try {
|
|
3743
|
+
await client.detachVolume(id, volumeId);
|
|
3744
|
+
} catch (err) {
|
|
3745
|
+
stopSpinner(false);
|
|
3746
|
+
throw err;
|
|
3747
|
+
}
|
|
3748
|
+
stopSpinner(true);
|
|
3749
|
+
process.stderr.write(
|
|
3750
|
+
import_chalk11.default.green(`\uBCFC\uB968 \uC5F0\uACB0 \uD574\uC81C \uC694\uCCAD \uC644\uB8CC (volumeId: ${volumeId})
|
|
3751
|
+
`)
|
|
3752
|
+
);
|
|
3753
|
+
});
|
|
3754
|
+
var volumeCommand = new import_commander32.Command("volume").description(
|
|
3755
|
+
"\uC778\uC2A4\uD134\uC2A4 \uBCFC\uB968 \uC5F0\uACB0/\uD574\uC81C"
|
|
3756
|
+
);
|
|
3757
|
+
volumeCommand.addCommand(attachCommand);
|
|
3758
|
+
volumeCommand.addCommand(detachCommand);
|
|
3759
|
+
|
|
3760
|
+
// src/commands/instance/volumes.ts
|
|
3761
|
+
var import_commander33 = require("commander");
|
|
3762
|
+
var volumesCommand = new import_commander33.Command("volumes").description("\uC778\uC2A4\uD134\uC2A4\uC5D0 \uC5F0\uACB0\uB41C \uBCFC\uB968 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
|
|
3763
|
+
const opts = cmd.optsWithGlobals();
|
|
3764
|
+
const { client } = await resolveInstanceClient(opts);
|
|
3765
|
+
startSpinner("\uBCFC\uB968 \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
3766
|
+
let attachments;
|
|
3767
|
+
try {
|
|
3768
|
+
attachments = await client.listVolumeAttachments(id);
|
|
3769
|
+
} catch (err) {
|
|
3770
|
+
stopSpinner(false);
|
|
3771
|
+
throw err;
|
|
3772
|
+
}
|
|
3773
|
+
stopSpinner(true);
|
|
3774
|
+
output(opts, {
|
|
3775
|
+
headers: ["id", "volumeId", "device"],
|
|
3776
|
+
rows: attachments.map((a) => [a.id, a.volumeId, a.device]),
|
|
3777
|
+
raw: attachments,
|
|
3778
|
+
ids: attachments.map((a) => a.id)
|
|
3779
|
+
});
|
|
3780
|
+
});
|
|
1595
3781
|
|
|
1596
3782
|
// src/index.ts
|
|
1597
|
-
var program = new
|
|
1598
|
-
program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.
|
|
3783
|
+
var program = new import_commander34.Command();
|
|
3784
|
+
program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.4.0").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");
|
|
1599
3785
|
program.hook("preAction", () => {
|
|
1600
3786
|
const opts = program.opts();
|
|
1601
3787
|
if (!opts.color || process.env["NO_COLOR"]) {
|
|
1602
|
-
|
|
3788
|
+
import_chalk12.default.level = 0;
|
|
1603
3789
|
}
|
|
1604
3790
|
if (opts.json || opts.quiet) {
|
|
1605
3791
|
setQuiet(true);
|
|
1606
3792
|
}
|
|
1607
3793
|
});
|
|
1608
3794
|
program.addCommand(configureCommand);
|
|
1609
|
-
var logncrashCommand = new
|
|
3795
|
+
var logncrashCommand = new import_commander34.Command("logncrash").description("Log & Crash \uAD00\uB828 \uBA85\uB839");
|
|
1610
3796
|
logncrashCommand.addCommand(searchCommand);
|
|
3797
|
+
logncrashCommand.addCommand(sendCommand);
|
|
3798
|
+
logncrashCommand.addCommand(exportCommand);
|
|
1611
3799
|
program.addCommand(logncrashCommand);
|
|
1612
|
-
var deployCommand = new
|
|
3800
|
+
var deployCommand = new import_commander34.Command("deploy").description("NHN Cloud Deploy \uAD00\uB828 \uBA85\uB839");
|
|
1613
3801
|
deployCommand.addCommand(runCommand);
|
|
1614
3802
|
deployCommand.addCommand(artifactsCommand);
|
|
1615
3803
|
deployCommand.addCommand(serverGroupsCommand);
|
|
1616
3804
|
deployCommand.addCommand(historiesCommand);
|
|
3805
|
+
deployCommand.addCommand(binaryGroupsCommand);
|
|
3806
|
+
deployCommand.addCommand(binariesCommand);
|
|
3807
|
+
deployCommand.addCommand(uploadCommand);
|
|
3808
|
+
deployCommand.addCommand(downloadCommand);
|
|
1617
3809
|
program.addCommand(deployCommand);
|
|
1618
|
-
var instanceCommand = new
|
|
3810
|
+
var instanceCommand = new import_commander34.Command("instance").description("Compute \uC778\uC2A4\uD134\uC2A4 \uAD00\uB828 \uBA85\uB839");
|
|
1619
3811
|
instanceCommand.addCommand(listCommand);
|
|
1620
|
-
instanceCommand.addCommand(
|
|
1621
|
-
instanceCommand.addCommand(
|
|
1622
|
-
instanceCommand.addCommand(
|
|
3812
|
+
instanceCommand.addCommand(flavorsCommand);
|
|
3813
|
+
instanceCommand.addCommand(availabilityZonesCommand);
|
|
3814
|
+
instanceCommand.addCommand(getCommand2);
|
|
3815
|
+
instanceCommand.addCommand(createCommand3);
|
|
3816
|
+
instanceCommand.addCommand(deleteCommand2);
|
|
3817
|
+
instanceCommand.addCommand(startCommand);
|
|
3818
|
+
instanceCommand.addCommand(stopCommand);
|
|
3819
|
+
instanceCommand.addCommand(rebootCommand);
|
|
3820
|
+
instanceCommand.addCommand(resizeCommand);
|
|
3821
|
+
instanceCommand.addCommand(resizeConfirmCommand);
|
|
3822
|
+
instanceCommand.addCommand(resizeRevertCommand);
|
|
3823
|
+
instanceCommand.addCommand(imagesCommand);
|
|
3824
|
+
instanceCommand.addCommand(keypairsCommand);
|
|
3825
|
+
instanceCommand.addCommand(keypairCommand);
|
|
3826
|
+
instanceCommand.addCommand(volumeCommand);
|
|
3827
|
+
instanceCommand.addCommand(volumesCommand);
|
|
1623
3828
|
program.addCommand(instanceCommand);
|
|
3829
|
+
var networkCommand = new import_commander34.Command("network").description("VPC\xB7\uC11C\uBE0C\uB137 \uC870\uD68C");
|
|
3830
|
+
networkCommand.addCommand(listCommand3);
|
|
3831
|
+
networkCommand.addCommand(subnetCommand);
|
|
3832
|
+
program.addCommand(networkCommand);
|
|
3833
|
+
var volumeCommand2 = new import_commander34.Command("volume").description("Block Storage \uBCFC\uB968 \uAD00\uB828 \uBA85\uB839");
|
|
3834
|
+
volumeCommand2.addCommand(listCommand2);
|
|
3835
|
+
volumeCommand2.addCommand(getCommand);
|
|
3836
|
+
volumeCommand2.addCommand(createCommand);
|
|
3837
|
+
program.addCommand(volumeCommand2);
|
|
3838
|
+
var floatingipCommand = new import_commander34.Command("floatingip").description(
|
|
3839
|
+
"Floating IP(\uC778\uC2A4\uD134\uC2A4 \uACF5\uC778 IP) \uAD00\uB9AC"
|
|
3840
|
+
);
|
|
3841
|
+
floatingipCommand.addCommand(listCommand4);
|
|
3842
|
+
floatingipCommand.addCommand(createCommand2);
|
|
3843
|
+
floatingipCommand.addCommand(deleteCommand);
|
|
3844
|
+
program.addCommand(floatingipCommand);
|
|
1624
3845
|
program.parseAsync().catch((err) => {
|
|
1625
3846
|
const message = err instanceof Error ? err.message : String(err);
|
|
1626
3847
|
const exitCode = err instanceof NhnCloudCliError ? err.exitCode : 1;
|
|
1627
|
-
process.stderr.write(
|
|
3848
|
+
process.stderr.write(import_chalk12.default.red(`\uC624\uB958: ${message}`) + "\n");
|
|
1628
3849
|
process.exit(exitCode);
|
|
1629
3850
|
});
|