@bifos/nhncloud-cli 0.1.0 → 0.2.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 +79 -1
- package/dist/index.js +608 -26
- package/package.json +1 -1
- package/skills/nhncloud-cli/SKILL.md +117 -0
package/README.md
CHANGED
|
@@ -17,17 +17,32 @@ npm install -g @bifos/nhncloud-cli
|
|
|
17
17
|
nhncloud configure
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
- profile → UAK(id/secret) → logncrash appkey/secret 순으로 입력한다.
|
|
20
|
+
- profile → UAK(id/secret) → logncrash appkey/secret → iaas 자격증명 순으로 입력한다.
|
|
21
21
|
- 저장 전 연결 테스트를 자동으로 수행한다 (`--no-verify` 로 생략 가능).
|
|
22
22
|
- CI/자동화는 flag 로 비대화형 설정이 가능하다.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
+
# UAK + logncrash 비대화형 설정
|
|
25
26
|
nhncloud configure \
|
|
26
27
|
--uak-id <id> --uak-secret <secret> \
|
|
27
28
|
--logncrash-appkey <key> --logncrash-secret <secret> \
|
|
28
29
|
--no-verify
|
|
30
|
+
|
|
31
|
+
# iaas (Compute) 비대화형 설정 — API 비밀번호는 env 권장
|
|
32
|
+
NHNCLOUD_IAAS_PASSWORD=<api-password> nhncloud configure \
|
|
33
|
+
--iaas-tenant-id <tenant-id> \
|
|
34
|
+
--iaas-username <iam-username> \
|
|
35
|
+
--iaas-region kr1 \
|
|
36
|
+
--no-verify
|
|
29
37
|
```
|
|
30
38
|
|
|
39
|
+
> **iaas password 안내**: `--iaas-password` 에 입력하는 값은 NHN Cloud 콘솔 IAM 의 **API 비밀번호**입니다.
|
|
40
|
+
> 로그인 비밀번호와 다릅니다.
|
|
41
|
+
> IAM 사용자 상세 페이지 → "API 비밀번호 설정"에서 별도로 발급하세요.
|
|
42
|
+
>
|
|
43
|
+
> **iaas username 안내**: `--iaas-username` 은 NHN Cloud 계정 이메일 또는 IAM 계정 ID(사번)입니다.
|
|
44
|
+
> tenantId 와 비슷한 "API 사용자 ID"(UUID)가 아닙니다.
|
|
45
|
+
|
|
31
46
|
저장 경로: `~/.nhncloud/credentials.json` (mode 0600), `~/.nhncloud/config.json`.
|
|
32
47
|
|
|
33
48
|
profile 해석 우선순위: `--profile` 옵션 > `NHNCLOUD_PROFILE` 환경변수 > `config.defaultProfile` > `"default"`.
|
|
@@ -129,11 +144,74 @@ nhncloud logncrash search --query '*' --from 1d --to now --json | jq '.totalItem
|
|
|
129
144
|
| 3 | 입력 오류 (파라미터·시간 범위) |
|
|
130
145
|
| 4 | 설정 오류 (자격증명 누락) |
|
|
131
146
|
|
|
147
|
+
### 인스턴스 (Instance)
|
|
148
|
+
|
|
149
|
+
`~/.nhncloud/credentials.json` 에 `iaas` 블록을 추가하거나 `nhncloud configure` 로 설정한다.
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"version": 1,
|
|
154
|
+
"profiles": {
|
|
155
|
+
"default": {
|
|
156
|
+
"iaas": {
|
|
157
|
+
"tenantId": "<tenant-id>",
|
|
158
|
+
"username": "<iam-username>",
|
|
159
|
+
"password": "<api-password>",
|
|
160
|
+
"region": "kr1"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# 인스턴스 목록 조회
|
|
169
|
+
nhncloud instance list
|
|
170
|
+
|
|
171
|
+
# 단일 인스턴스 상태 조회
|
|
172
|
+
nhncloud instance get <instance-id>
|
|
173
|
+
|
|
174
|
+
# 인스턴스 생성 (즉시 반환, BUILD 상태)
|
|
175
|
+
nhncloud instance create \
|
|
176
|
+
--name my-server \
|
|
177
|
+
--flavor <flavor-id> \
|
|
178
|
+
--image <image-id> \
|
|
179
|
+
--network <network-uuid>
|
|
180
|
+
|
|
181
|
+
# 인스턴스 생성 + ACTIVE 대기 (IP 할당까지 폴링)
|
|
182
|
+
nhncloud instance create \
|
|
183
|
+
--name my-server \
|
|
184
|
+
--flavor <flavor-id> \
|
|
185
|
+
--image <image-id> \
|
|
186
|
+
--network <network-uuid> \
|
|
187
|
+
--wait
|
|
188
|
+
|
|
189
|
+
# GPU(g2) 등 boot-from-volume 필수 flavor — --boot-volume-size 지정
|
|
190
|
+
nhncloud instance create \
|
|
191
|
+
--name gpu-server \
|
|
192
|
+
--flavor <gpu-flavor-id> \
|
|
193
|
+
--image <image-id> \
|
|
194
|
+
--network <network-uuid> \
|
|
195
|
+
--boot-volume-size 30 \
|
|
196
|
+
--wait
|
|
197
|
+
|
|
198
|
+
# --quiet --wait: 첫 IP 한 줄만 stdout (CI 파이프용)
|
|
199
|
+
IP=$(nhncloud instance create --name ci-runner \
|
|
200
|
+
--flavor <flavor-id> --image <image-id> --network <network-uuid> \
|
|
201
|
+
--wait --quiet)
|
|
202
|
+
|
|
203
|
+
# 인스턴스 삭제 (confirm 생략)
|
|
204
|
+
nhncloud instance delete <instance-id> --yes
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
지원 region: `kr1` / `kr2` / `kr3` / `jp1` (`--region` 으로 override 가능).
|
|
208
|
+
|
|
132
209
|
## 개발
|
|
133
210
|
|
|
134
211
|
```bash
|
|
135
212
|
pnpm install
|
|
136
213
|
pnpm run build # tsup 단일 번들 (dist/index.js)
|
|
137
214
|
pnpm tsc --noEmit # 타입 체크
|
|
215
|
+
node dist/index.js instance --help
|
|
138
216
|
node dist/index.js logncrash search --help
|
|
139
217
|
```
|
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_commander11 = require("commander");
|
|
28
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
29
29
|
|
|
30
30
|
// src/utils/spinner.ts
|
|
31
31
|
var import_ora = __toESM(require("ora"));
|
|
@@ -191,9 +191,9 @@ nhncloud configure \uB97C \uC2E4\uD589\uD574 UAK id/secret \uC744 \uC124\uC815\u
|
|
|
191
191
|
return uak;
|
|
192
192
|
}
|
|
193
193
|
async function getServiceCredential(service, profileName) {
|
|
194
|
-
if (service === "userAccessKey") {
|
|
194
|
+
if (service === "userAccessKey" || service === "iaas") {
|
|
195
195
|
throw new NhnCloudCliError(
|
|
196
|
-
"
|
|
196
|
+
`"${service}" \uB294 \uC11C\uBE44\uC2A4 \uC790\uACA9\uC99D\uBA85\uC774 \uC544\uB2D9\uB2C8\uB2E4 \u2014 \uC804\uC6A9 getter \uB97C \uC0AC\uC6A9\uD558\uC138\uC694.`,
|
|
197
197
|
EXIT_PARAM_ERROR
|
|
198
198
|
);
|
|
199
199
|
}
|
|
@@ -255,6 +255,38 @@ async function setServiceCredential(profileName, service, cred) {
|
|
|
255
255
|
creds.profiles[profileName] = { ...profile, [service]: cred };
|
|
256
256
|
await saveCredentials(creds);
|
|
257
257
|
}
|
|
258
|
+
function isIaasCredential(value) {
|
|
259
|
+
if (typeof value !== "object" || value === null) return false;
|
|
260
|
+
const obj = value;
|
|
261
|
+
return typeof obj["tenantId"] === "string" && obj["tenantId"].length > 0 && typeof obj["username"] === "string" && obj["username"].length > 0 && typeof obj["password"] === "string" && obj["password"].length > 0 && typeof obj["region"] === "string" && obj["region"].length > 0;
|
|
262
|
+
}
|
|
263
|
+
async function getIaasCredential(profileName) {
|
|
264
|
+
const credentials = await loadCredentials();
|
|
265
|
+
const profile = credentials.profiles[profileName];
|
|
266
|
+
if (!profile) {
|
|
267
|
+
throw new NhnCloudCliError(
|
|
268
|
+
`profile "${profileName}" \uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
|
|
269
|
+
${CREDENTIALS_PATH} \uC5D0\uC11C profiles.${profileName} \uBE14\uB85D\uC744 \uCD94\uAC00\uD558\uAC70\uB098 nhncloud configure \uB97C \uC2E4\uD589\uD558\uC138\uC694.`,
|
|
270
|
+
EXIT_CONFIG_ERROR
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const iaas = profile["iaas"];
|
|
274
|
+
if (!isIaasCredential(iaas)) {
|
|
275
|
+
throw new NhnCloudCliError(
|
|
276
|
+
`profile "${profileName}" \uC5D0 iaas \uC790\uACA9\uC99D\uBA85\uC774 \uC5C6\uAC70\uB098 \uBD88\uC644\uC804\uD569\uB2C8\uB2E4.
|
|
277
|
+
nhncloud configure \uB97C \uC2E4\uD589\uD574 tenantId / username / password / region \uC744 \uC124\uC815\uD558\uC138\uC694.
|
|
278
|
+
password \uB294 NHN Cloud \uCF58\uC194 IAM \uC758 API \uBE44\uBC00\uBC88\uD638\uC785\uB2C8\uB2E4 (\uB85C\uADF8\uC778 \uBE44\uBC00\uBC88\uD638\uAC00 \uC544\uB2D9\uB2C8\uB2E4).`,
|
|
279
|
+
EXIT_CONFIG_ERROR
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return iaas;
|
|
283
|
+
}
|
|
284
|
+
async function setIaasCredential(profileName, iaas) {
|
|
285
|
+
const creds = await loadCredentialsOrEmpty();
|
|
286
|
+
const profile = creds.profiles[profileName] ?? {};
|
|
287
|
+
creds.profiles[profileName] = { ...profile, iaas };
|
|
288
|
+
await saveCredentials(creds);
|
|
289
|
+
}
|
|
258
290
|
async function getDeployTarget(name) {
|
|
259
291
|
const config = await loadConfig();
|
|
260
292
|
const targets = config?.deploy?.targets;
|
|
@@ -279,6 +311,44 @@ var import_node_path2 = require("path");
|
|
|
279
311
|
var import_node_os2 = require("os");
|
|
280
312
|
var import_node_crypto = require("crypto");
|
|
281
313
|
var CACHE_DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".nhncloud", "cache");
|
|
314
|
+
function iaasCachePath(profile, region) {
|
|
315
|
+
return (0, import_node_path2.join)(CACHE_DIR, `iaas-token-${profile}-${region}.json`);
|
|
316
|
+
}
|
|
317
|
+
function isIaasTokenCache(val) {
|
|
318
|
+
if (typeof val !== "object" || val === null) return false;
|
|
319
|
+
const obj = val;
|
|
320
|
+
return typeof obj["tokenId"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["computeEndpoint"] === "string";
|
|
321
|
+
}
|
|
322
|
+
async function readIaasToken(profile, region) {
|
|
323
|
+
const filePath = iaasCachePath(profile, region);
|
|
324
|
+
try {
|
|
325
|
+
const raw = await (0, import_promises2.readFile)(filePath, "utf-8");
|
|
326
|
+
const parsed = JSON.parse(raw);
|
|
327
|
+
if (!isIaasTokenCache(parsed)) return null;
|
|
328
|
+
const expiresAt = new Date(parsed.expiresAt).getTime();
|
|
329
|
+
const BUFFER_MS = 6e4;
|
|
330
|
+
if (expiresAt - Date.now() < BUFFER_MS) return null;
|
|
331
|
+
return {
|
|
332
|
+
tokenId: parsed.tokenId,
|
|
333
|
+
expiresAt: parsed.expiresAt,
|
|
334
|
+
computeEndpoint: parsed.computeEndpoint
|
|
335
|
+
};
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function writeIaasToken(profile, region, data) {
|
|
341
|
+
const filePath = iaasCachePath(profile, region);
|
|
342
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(filePath), { recursive: true });
|
|
343
|
+
const cache = {
|
|
344
|
+
tokenId: data.tokenId,
|
|
345
|
+
expiresAt: data.expiresAt,
|
|
346
|
+
computeEndpoint: data.computeEndpoint
|
|
347
|
+
};
|
|
348
|
+
const tmp = filePath + "." + (0, import_node_crypto.randomBytes)(4).toString("hex") + ".tmp";
|
|
349
|
+
await (0, import_promises2.writeFile)(tmp, JSON.stringify(cache, null, 2), { encoding: "utf-8", mode: 384 });
|
|
350
|
+
await (0, import_promises2.rename)(tmp, filePath);
|
|
351
|
+
}
|
|
282
352
|
function tokenCachePath(profile) {
|
|
283
353
|
return (0, import_node_path2.join)(CACHE_DIR, `deploy-token-${profile}.json`);
|
|
284
354
|
}
|
|
@@ -372,7 +442,7 @@ async function getAccessToken(profile, uakId, uakSecret, forceRefresh = false) {
|
|
|
372
442
|
return raw.access_token;
|
|
373
443
|
}
|
|
374
444
|
|
|
375
|
-
// src/
|
|
445
|
+
// src/api/keystone.ts
|
|
376
446
|
var import_ky3 = __toESM(require("ky"));
|
|
377
447
|
|
|
378
448
|
// src/api/endpoints.ts
|
|
@@ -390,6 +460,78 @@ function endpointFor(service) {
|
|
|
390
460
|
}
|
|
391
461
|
return endpoint;
|
|
392
462
|
}
|
|
463
|
+
function keystoneIdentityUrl() {
|
|
464
|
+
return "https://api-identity-infrastructure.nhncloudservice.com/v2.0/tokens";
|
|
465
|
+
}
|
|
466
|
+
var INSTANCE_HOST = {
|
|
467
|
+
kr1: "kr1-api-instance-infrastructure.nhncloudservice.com",
|
|
468
|
+
kr2: "kr2-api-instance-infrastructure.nhncloudservice.com",
|
|
469
|
+
kr3: "kr3-api-instance-infrastructure.nhncloudservice.com",
|
|
470
|
+
jp1: "jp1-api-instance-infrastructure.nhncloudservice.com"
|
|
471
|
+
};
|
|
472
|
+
function instanceHost(region) {
|
|
473
|
+
const host = INSTANCE_HOST[region];
|
|
474
|
+
if (!host) {
|
|
475
|
+
throw new NhnCloudCliError(
|
|
476
|
+
`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${Object.keys(INSTANCE_HOST).join(", ")}`,
|
|
477
|
+
EXIT_PARAM_ERROR
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return host;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/api/keystone.ts
|
|
484
|
+
function isKeystoneTokenResponse(val) {
|
|
485
|
+
if (typeof val !== "object" || val === null) return false;
|
|
486
|
+
const obj = val;
|
|
487
|
+
if (typeof obj["access"] !== "object" || obj["access"] === null) return false;
|
|
488
|
+
const access = obj["access"];
|
|
489
|
+
if (typeof access["token"] !== "object" || access["token"] === null) return false;
|
|
490
|
+
const token = access["token"];
|
|
491
|
+
return typeof token["id"] === "string" && typeof token["expires"] === "string";
|
|
492
|
+
}
|
|
493
|
+
async function getIaasToken(profile, iaas, forceRefresh = false) {
|
|
494
|
+
if (!forceRefresh) {
|
|
495
|
+
const cached = await readIaasToken(profile, iaas.region);
|
|
496
|
+
if (cached !== null) {
|
|
497
|
+
return { tokenId: cached.tokenId, computeEndpoint: cached.computeEndpoint };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const host = instanceHost(iaas.region);
|
|
501
|
+
const computeEndpoint = `https://${host}/v2/${encodeURIComponent(iaas.tenantId)}`;
|
|
502
|
+
let raw;
|
|
503
|
+
try {
|
|
504
|
+
raw = await import_ky3.default.post(keystoneIdentityUrl(), {
|
|
505
|
+
json: {
|
|
506
|
+
auth: {
|
|
507
|
+
tenantId: iaas.tenantId,
|
|
508
|
+
passwordCredentials: {
|
|
509
|
+
username: iaas.username,
|
|
510
|
+
password: iaas.password
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
retry: 0
|
|
515
|
+
}).json();
|
|
516
|
+
} catch (err) {
|
|
517
|
+
throw toNhnCloudCliError(err);
|
|
518
|
+
}
|
|
519
|
+
if (!isKeystoneTokenResponse(raw)) {
|
|
520
|
+
throw new NhnCloudCliError(
|
|
521
|
+
"Keystone \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 access.token.id / expires \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
522
|
+
EXIT_API_ERROR
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const tokenId = raw.access.token.id;
|
|
526
|
+
const expiresAt = raw.access.token.expires;
|
|
527
|
+
if (!forceRefresh) {
|
|
528
|
+
await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, computeEndpoint });
|
|
529
|
+
}
|
|
530
|
+
return { tokenId, computeEndpoint };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/services/logncrash/client.ts
|
|
534
|
+
var import_ky4 = __toESM(require("ky"));
|
|
393
535
|
|
|
394
536
|
// src/api/envelope.ts
|
|
395
537
|
function unwrap(res) {
|
|
@@ -417,7 +559,7 @@ var LogncrashClient = class {
|
|
|
417
559
|
const endpoint = endpointFor("logncrash");
|
|
418
560
|
const url = `${endpoint}/api/v2/search/${encodeURIComponent(this.appkey)}`;
|
|
419
561
|
try {
|
|
420
|
-
const res = await
|
|
562
|
+
const res = await import_ky4.default.post(url, {
|
|
421
563
|
headers: {
|
|
422
564
|
"X-LNCS-SECRET": this.secret,
|
|
423
565
|
"Content-Type": "application/json"
|
|
@@ -449,6 +591,17 @@ async function verifyUserAccessKey(uak) {
|
|
|
449
591
|
throw err;
|
|
450
592
|
}
|
|
451
593
|
}
|
|
594
|
+
async function verifyIaas(iaas) {
|
|
595
|
+
try {
|
|
596
|
+
await getIaasToken("__verify__", iaas, true);
|
|
597
|
+
return true;
|
|
598
|
+
} catch (err) {
|
|
599
|
+
if (err instanceof NhnCloudCliError && err.exitCode === EXIT_AUTH_ERROR) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
throw err;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
452
605
|
async function verifyLogncrash(cred) {
|
|
453
606
|
if (!cred.appkey || !cred.secret) return false;
|
|
454
607
|
const client = new LogncrashClient(cred.appkey, cred.secret);
|
|
@@ -472,7 +625,7 @@ async function verifyLogncrash(cred) {
|
|
|
472
625
|
}
|
|
473
626
|
|
|
474
627
|
// src/commands/configure.ts
|
|
475
|
-
async function saveAndVerify(profileName, uak, logncrash, doVerify) {
|
|
628
|
+
async function saveAndVerify(profileName, uak, logncrash, iaas, doVerify) {
|
|
476
629
|
if (doVerify) {
|
|
477
630
|
if (uak) {
|
|
478
631
|
const ok = await verifyUserAccessKey(uak);
|
|
@@ -496,6 +649,17 @@ async function saveAndVerify(profileName, uak, logncrash, doVerify) {
|
|
|
496
649
|
);
|
|
497
650
|
}
|
|
498
651
|
}
|
|
652
|
+
if (iaas) {
|
|
653
|
+
const ok = await verifyIaas(iaas);
|
|
654
|
+
if (ok) {
|
|
655
|
+
process.stderr.write(import_chalk.default.green(" \u2713 iaas \uC5F0\uACB0 \uC131\uACF5\n"));
|
|
656
|
+
} else {
|
|
657
|
+
throw new NhnCloudCliError(
|
|
658
|
+
"iaas \uC778\uC99D \uC2E4\uD328 \u2014 tenantId / username / API \uBE44\uBC00\uBC88\uD638\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
659
|
+
EXIT_AUTH_ERROR
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
499
663
|
}
|
|
500
664
|
if (uak) {
|
|
501
665
|
await setUserAccessKey(profileName, uak);
|
|
@@ -503,6 +667,9 @@ async function saveAndVerify(profileName, uak, logncrash, doVerify) {
|
|
|
503
667
|
if (logncrash) {
|
|
504
668
|
await setServiceCredential(profileName, "logncrash", logncrash);
|
|
505
669
|
}
|
|
670
|
+
if (iaas) {
|
|
671
|
+
await setIaasCredential(profileName, iaas);
|
|
672
|
+
}
|
|
506
673
|
process.stderr.write(import_chalk.default.green(`
|
|
507
674
|
\u2713 profile "${profileName}" \uC124\uC815\uC774 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
|
|
508
675
|
`));
|
|
@@ -534,12 +701,40 @@ async function runInteractive(opts) {
|
|
|
534
701
|
const secret = await password({ message: "logncrash secret", mask: "*" });
|
|
535
702
|
logncrash = { appkey, secret };
|
|
536
703
|
}
|
|
704
|
+
let iaas;
|
|
705
|
+
const setupIaas = await confirm({
|
|
706
|
+
message: "iaas (Compute) \uC790\uACA9\uC99D\uBA85\uB3C4 \uC124\uC815\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
|
|
707
|
+
default: false
|
|
708
|
+
});
|
|
709
|
+
if (setupIaas) {
|
|
710
|
+
process.stderr.write(import_chalk.default.gray("\n\u2014 iaas (Compute) \uC790\uACA9\uC99D\uBA85 \u2014\n"));
|
|
711
|
+
process.stderr.write(
|
|
712
|
+
import_chalk.default.yellow(
|
|
713
|
+
" \u203B password \uB294 NHN Cloud \uCF58\uC194 IAM \uC758 API \uBE44\uBC00\uBC88\uD638\uC785\uB2C8\uB2E4 (\uB85C\uADF8\uC778 \uBE44\uBC00\uBC88\uD638\uAC00 \uC544\uB2D9\uB2C8\uB2E4).\n"
|
|
714
|
+
)
|
|
715
|
+
);
|
|
716
|
+
const { select } = await import("@inquirer/prompts");
|
|
717
|
+
const tenantId = await input({ message: "tenantId (\uD504\uB85C\uC81D\uD2B8 ID)" });
|
|
718
|
+
const iaasUsername = await input({ message: "IAM username" });
|
|
719
|
+
const iaasPassword = await password({ message: "API \uBE44\uBC00\uBC88\uD638", mask: "*" });
|
|
720
|
+
const region = await select({
|
|
721
|
+
message: "region",
|
|
722
|
+
choices: [
|
|
723
|
+
{ value: "kr1", name: "kr1 (\uD55C\uAD6D \uD310\uAD50)" },
|
|
724
|
+
{ value: "kr2", name: "kr2 (\uD55C\uAD6D \uD3C9\uCD0C)" },
|
|
725
|
+
{ value: "kr3", name: "kr3 (\uD55C\uAD6D \uAD11\uC8FC)" },
|
|
726
|
+
{ value: "jp1", name: "jp1 (\uC77C\uBCF8 \uB3C4\uCFC4)" }
|
|
727
|
+
],
|
|
728
|
+
default: "kr1"
|
|
729
|
+
});
|
|
730
|
+
iaas = { tenantId, username: iaasUsername, password: iaasPassword, region };
|
|
731
|
+
}
|
|
537
732
|
if (opts.verify) {
|
|
538
733
|
process.stderr.write(import_chalk.default.gray("\n\u2014 \uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC911\u2026 \u2014\n"));
|
|
539
734
|
}
|
|
540
735
|
if (opts.verify) {
|
|
541
736
|
try {
|
|
542
|
-
await saveAndVerify(profileName, uak, logncrash, true);
|
|
737
|
+
await saveAndVerify(profileName, uak, logncrash, iaas, true);
|
|
543
738
|
} catch (err) {
|
|
544
739
|
if (err instanceof NhnCloudCliError && err.exitCode === EXIT_AUTH_ERROR) {
|
|
545
740
|
process.stderr.write(import_chalk.default.red(` \u2717 ${err.message}
|
|
@@ -552,34 +747,44 @@ async function runInteractive(opts) {
|
|
|
552
747
|
process.stderr.write(import_chalk.default.yellow("\uC800\uC7A5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
|
|
553
748
|
return;
|
|
554
749
|
}
|
|
555
|
-
await saveAndVerify(profileName, uak, logncrash, false);
|
|
750
|
+
await saveAndVerify(profileName, uak, logncrash, iaas, false);
|
|
556
751
|
} else {
|
|
557
752
|
throw err;
|
|
558
753
|
}
|
|
559
754
|
}
|
|
560
755
|
} else {
|
|
561
|
-
await saveAndVerify(profileName, uak, logncrash, false);
|
|
756
|
+
await saveAndVerify(profileName, uak, logncrash, iaas, false);
|
|
562
757
|
}
|
|
563
758
|
}
|
|
564
759
|
async function runNonInteractive(opts) {
|
|
565
760
|
const profileName = await resolveProfileName(opts.profile);
|
|
566
761
|
const uakSecret = opts.uakSecret ?? process.env["NHNCLOUD_UAK_SECRET"];
|
|
567
762
|
const logncrashSecret = opts.logncrashSecret ?? process.env["NHNCLOUD_LOGNCRASH_SECRET"];
|
|
763
|
+
const iaasPassword = opts.iaasPassword ?? process.env["NHNCLOUD_IAAS_PASSWORD"];
|
|
568
764
|
const uak = opts.uakId && uakSecret ? { id: opts.uakId, secret: uakSecret } : void 0;
|
|
569
765
|
const logncrash = opts.logncrashAppkey && logncrashSecret ? { appkey: opts.logncrashAppkey, secret: logncrashSecret } : void 0;
|
|
570
|
-
|
|
766
|
+
const iaas = opts.iaasTenantId && opts.iaasUsername && iaasPassword ? {
|
|
767
|
+
tenantId: opts.iaasTenantId,
|
|
768
|
+
username: opts.iaasUsername,
|
|
769
|
+
password: iaasPassword,
|
|
770
|
+
region: opts.iaasRegion ?? "kr1"
|
|
771
|
+
} : void 0;
|
|
772
|
+
if (!uak && !logncrash && !iaas) {
|
|
571
773
|
throw new NhnCloudCliError(
|
|
572
|
-
"\uBE44\uB300\uD654\uD615 \uBAA8\uB4DC: --uak-id + UAK secret \uB610\uB294 --
|
|
774
|
+
"\uBE44\uB300\uD654\uD615 \uBAA8\uB4DC: --uak-id + UAK secret, --logncrash-appkey + logncrash secret,\n\uB610\uB294 --iaas-tenant-id + --iaas-username + iaas password \uC911 \uD558\uB098\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.\nsecret/password \uB294 \uB178\uCD9C \uBC29\uC9C0\uB97C \uC704\uD574 \uD658\uACBD\uBCC0\uC218 \uAD8C\uC7A5:\nNHNCLOUD_UAK_SECRET / NHNCLOUD_LOGNCRASH_SECRET / NHNCLOUD_IAAS_PASSWORD.",
|
|
573
775
|
EXIT_PARAM_ERROR
|
|
574
776
|
);
|
|
575
777
|
}
|
|
576
778
|
if (opts.verify) {
|
|
577
779
|
process.stderr.write(import_chalk.default.gray("\uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC911\u2026\n"));
|
|
578
780
|
}
|
|
579
|
-
await saveAndVerify(profileName, uak, logncrash, opts.verify);
|
|
781
|
+
await saveAndVerify(profileName, uak, logncrash, iaas, opts.verify);
|
|
580
782
|
}
|
|
581
|
-
var configureCommand = new import_commander.Command("configure").description("\uC790\uACA9\uC99D\uBA85 \uC124\uC815 \uB9C8\uBC95\uC0AC (\uB300\uD654\uD615 + flag)").option("--profile <name>", "\uB300\uC0C1 profile \uC774\uB984 (\uAE30\uBCF8: default)").option("--uak-id <id>", "\uAC1C\uC778 UAK ID (\uBE44\uB300\uD654\uD615)").option("--uak-secret <secret>", "\uAC1C\uC778 UAK Secret (\uBE44\uB300\uD654\uD615, \uB178\uCD9C \uBC29\uC9C0\uB85C env NHNCLOUD_UAK_SECRET \uAD8C\uC7A5)").option("--logncrash-appkey <key>", "logncrash appkey (\uBE44\uB300\uD654\uD615)").option("--logncrash-secret <secret>", "logncrash secret (\uBE44\uB300\uD654\uD615, env NHNCLOUD_LOGNCRASH_SECRET \uAD8C\uC7A5)").option("--
|
|
582
|
-
|
|
783
|
+
var configureCommand = new import_commander.Command("configure").description("\uC790\uACA9\uC99D\uBA85 \uC124\uC815 \uB9C8\uBC95\uC0AC (\uB300\uD654\uD615 + flag)").option("--profile <name>", "\uB300\uC0C1 profile \uC774\uB984 (\uAE30\uBCF8: default)").option("--uak-id <id>", "\uAC1C\uC778 UAK ID (\uBE44\uB300\uD654\uD615)").option("--uak-secret <secret>", "\uAC1C\uC778 UAK Secret (\uBE44\uB300\uD654\uD615, \uB178\uCD9C \uBC29\uC9C0\uB85C env NHNCLOUD_UAK_SECRET \uAD8C\uC7A5)").option("--logncrash-appkey <key>", "logncrash appkey (\uBE44\uB300\uD654\uD615)").option("--logncrash-secret <secret>", "logncrash secret (\uBE44\uB300\uD654\uD615, env NHNCLOUD_LOGNCRASH_SECRET \uAD8C\uC7A5)").option("--iaas-tenant-id <id>", "iaas tenantId / \uD504\uB85C\uC81D\uD2B8 ID (\uBE44\uB300\uD654\uD615)").option("--iaas-username <user>", "iaas IAM username (\uBE44\uB300\uD654\uD615)").option(
|
|
784
|
+
"--iaas-password <pass>",
|
|
785
|
+
"iaas API \uBE44\uBC00\uBC88\uD638 (\uBE44\uB300\uD654\uD615, \uB178\uCD9C \uBC29\uC9C0\uB85C env NHNCLOUD_IAAS_PASSWORD \uAD8C\uC7A5)"
|
|
786
|
+
).option("--iaas-region <region>", "iaas region (\uAE30\uBCF8: kr1)", "kr1").option("--no-verify", "\uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC0DD\uB7B5").action(async (opts) => {
|
|
787
|
+
const hasFlag = opts.uakId || opts.uakSecret || opts.logncrashAppkey || opts.logncrashSecret || opts.iaasTenantId || opts.iaasUsername || opts.iaasPassword;
|
|
583
788
|
try {
|
|
584
789
|
if (hasFlag) {
|
|
585
790
|
await runNonInteractive(opts);
|
|
@@ -784,7 +989,7 @@ credentials.json \uC5D0 "secret": "<secretkey>" \uB97C \uCD94\uAC00\uD558\uC138\
|
|
|
784
989
|
var import_commander3 = require("commander");
|
|
785
990
|
|
|
786
991
|
// src/services/deploy/client.ts
|
|
787
|
-
var
|
|
992
|
+
var import_ky5 = __toESM(require("ky"));
|
|
788
993
|
var SYNC_TIMEOUT_MS = 6e5;
|
|
789
994
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
790
995
|
var DeployClient = class {
|
|
@@ -818,7 +1023,7 @@ var DeployClient = class {
|
|
|
818
1023
|
payload["targetServerHostnames"] = params.targetHosts;
|
|
819
1024
|
}
|
|
820
1025
|
try {
|
|
821
|
-
const res = await
|
|
1026
|
+
const res = await import_ky5.default.post(url, {
|
|
822
1027
|
headers: {
|
|
823
1028
|
...this.authHeaders(),
|
|
824
1029
|
"Content-Type": "application/json"
|
|
@@ -838,7 +1043,7 @@ var DeployClient = class {
|
|
|
838
1043
|
async artifacts(appKey) {
|
|
839
1044
|
const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts`;
|
|
840
1045
|
try {
|
|
841
|
-
const res = await
|
|
1046
|
+
const res = await import_ky5.default.get(url, {
|
|
842
1047
|
headers: this.authHeaders(),
|
|
843
1048
|
retry: 0,
|
|
844
1049
|
timeout: DEFAULT_TIMEOUT_MS
|
|
@@ -854,7 +1059,7 @@ var DeployClient = class {
|
|
|
854
1059
|
async serverGroups(appKey, artifactId) {
|
|
855
1060
|
const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts/${artifactId}/server-groups`;
|
|
856
1061
|
try {
|
|
857
|
-
const res = await
|
|
1062
|
+
const res = await import_ky5.default.get(url, {
|
|
858
1063
|
headers: this.authHeaders(),
|
|
859
1064
|
retry: 0,
|
|
860
1065
|
timeout: DEFAULT_TIMEOUT_MS
|
|
@@ -870,7 +1075,7 @@ var DeployClient = class {
|
|
|
870
1075
|
async histories(appKey, artifactId) {
|
|
871
1076
|
const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts/${artifactId}/deploy-histories`;
|
|
872
1077
|
try {
|
|
873
|
-
const res = await
|
|
1078
|
+
const res = await import_ky5.default.get(url, {
|
|
874
1079
|
headers: this.authHeaders(),
|
|
875
1080
|
retry: 0,
|
|
876
1081
|
timeout: DEFAULT_TIMEOUT_MS
|
|
@@ -1017,31 +1222,408 @@ var historiesCommand = new import_commander6.Command("histories").description("\
|
|
|
1017
1222
|
});
|
|
1018
1223
|
});
|
|
1019
1224
|
|
|
1225
|
+
// src/commands/instance/list.ts
|
|
1226
|
+
var import_commander7 = require("commander");
|
|
1227
|
+
|
|
1228
|
+
// src/services/instance/client.ts
|
|
1229
|
+
var import_ky6 = __toESM(require("ky"));
|
|
1230
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
1231
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
1232
|
+
function isServer(val) {
|
|
1233
|
+
if (typeof val !== "object" || val === null) return false;
|
|
1234
|
+
const obj = val;
|
|
1235
|
+
return typeof obj["id"] === "string" && typeof obj["name"] === "string" && typeof obj["status"] === "string" && typeof obj["addresses"] === "object" && obj["addresses"] !== null;
|
|
1236
|
+
}
|
|
1237
|
+
function isServerResponse(val) {
|
|
1238
|
+
if (typeof val !== "object" || val === null) return false;
|
|
1239
|
+
const obj = val;
|
|
1240
|
+
return isServer(obj["server"]);
|
|
1241
|
+
}
|
|
1242
|
+
function isServersResponse(val) {
|
|
1243
|
+
if (typeof val !== "object" || val === null) return false;
|
|
1244
|
+
const obj = val;
|
|
1245
|
+
return Array.isArray(obj["servers"]);
|
|
1246
|
+
}
|
|
1247
|
+
function isCreateResponse(val) {
|
|
1248
|
+
if (typeof val !== "object" || val === null) return false;
|
|
1249
|
+
const server = val["server"];
|
|
1250
|
+
if (typeof server !== "object" || server === null) return false;
|
|
1251
|
+
return typeof server["id"] === "string";
|
|
1252
|
+
}
|
|
1253
|
+
function hasIpAddress(server) {
|
|
1254
|
+
return Object.values(server.addresses).some((list) => list.length > 0);
|
|
1255
|
+
}
|
|
1256
|
+
var InstanceClient = class {
|
|
1257
|
+
tokenId;
|
|
1258
|
+
computeEndpoint;
|
|
1259
|
+
constructor(tokenId, computeEndpoint) {
|
|
1260
|
+
this.tokenId = tokenId;
|
|
1261
|
+
this.computeEndpoint = computeEndpoint;
|
|
1262
|
+
}
|
|
1263
|
+
authHeaders() {
|
|
1264
|
+
return { "X-Auth-Token": this.tokenId };
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* 인스턴스 목록을 조회한다 (GET /servers/detail).
|
|
1268
|
+
*/
|
|
1269
|
+
async list() {
|
|
1270
|
+
const url = `${this.computeEndpoint}/servers/detail`;
|
|
1271
|
+
try {
|
|
1272
|
+
const raw = await import_ky6.default.get(url, {
|
|
1273
|
+
headers: this.authHeaders(),
|
|
1274
|
+
retry: 0,
|
|
1275
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
1276
|
+
}).json();
|
|
1277
|
+
if (!isServersResponse(raw)) {
|
|
1278
|
+
throw new NhnCloudCliError(
|
|
1279
|
+
"instance list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 servers \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1280
|
+
EXIT_API_ERROR
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
return raw.servers;
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
throw toNhnCloudCliError(err);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* 단일 인스턴스를 조회한다 (GET /servers/{id}).
|
|
1290
|
+
*/
|
|
1291
|
+
async get(id) {
|
|
1292
|
+
const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}`;
|
|
1293
|
+
try {
|
|
1294
|
+
const raw = await import_ky6.default.get(url, {
|
|
1295
|
+
headers: this.authHeaders(),
|
|
1296
|
+
retry: 0,
|
|
1297
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
1298
|
+
}).json();
|
|
1299
|
+
if (!isServerResponse(raw)) {
|
|
1300
|
+
throw new NhnCloudCliError(
|
|
1301
|
+
`instance get(${id}) \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 server \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
|
|
1302
|
+
EXIT_API_ERROR
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
return raw.server;
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
throw toNhnCloudCliError(err);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* 인스턴스를 생성한다 (POST /servers).
|
|
1312
|
+
* NHN 확장 필드(ephemeralDiskSize / protect)는 정의됐을 때만 payload 에 포함한다.
|
|
1313
|
+
*/
|
|
1314
|
+
async create(params) {
|
|
1315
|
+
const url = `${this.computeEndpoint}/servers`;
|
|
1316
|
+
const serverBody = {
|
|
1317
|
+
name: params.name,
|
|
1318
|
+
flavorRef: params.flavorRef,
|
|
1319
|
+
networks: params.networks.map((uuid) => ({ uuid }))
|
|
1320
|
+
};
|
|
1321
|
+
if (params.bootVolumeSize !== void 0) {
|
|
1322
|
+
serverBody["block_device_mapping_v2"] = [
|
|
1323
|
+
{
|
|
1324
|
+
boot_index: 0,
|
|
1325
|
+
uuid: params.imageRef,
|
|
1326
|
+
source_type: "image",
|
|
1327
|
+
destination_type: "volume",
|
|
1328
|
+
volume_size: params.bootVolumeSize,
|
|
1329
|
+
delete_on_termination: true
|
|
1330
|
+
}
|
|
1331
|
+
];
|
|
1332
|
+
} else {
|
|
1333
|
+
serverBody["imageRef"] = params.imageRef;
|
|
1334
|
+
}
|
|
1335
|
+
if (params.keyName !== void 0) {
|
|
1336
|
+
serverBody["key_name"] = params.keyName;
|
|
1337
|
+
}
|
|
1338
|
+
if (params.securityGroups !== void 0) {
|
|
1339
|
+
serverBody["security_groups"] = params.securityGroups.map((name) => ({ name }));
|
|
1340
|
+
}
|
|
1341
|
+
if (params.ephemeralDiskSize !== void 0) {
|
|
1342
|
+
serverBody["NHN-EXT-ATTR:ephemeral_disk_size"] = params.ephemeralDiskSize;
|
|
1343
|
+
}
|
|
1344
|
+
if (params.protect !== void 0) {
|
|
1345
|
+
serverBody["NHN-EXT-ATTR:protect"] = params.protect;
|
|
1346
|
+
}
|
|
1347
|
+
let raw;
|
|
1348
|
+
try {
|
|
1349
|
+
raw = await import_ky6.default.post(url, {
|
|
1350
|
+
headers: this.authHeaders(),
|
|
1351
|
+
json: { server: serverBody },
|
|
1352
|
+
retry: 0,
|
|
1353
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
1354
|
+
}).json();
|
|
1355
|
+
} catch (err) {
|
|
1356
|
+
throw toNhnCloudCliError(err);
|
|
1357
|
+
}
|
|
1358
|
+
if (!isCreateResponse(raw)) {
|
|
1359
|
+
throw new NhnCloudCliError(
|
|
1360
|
+
"instance create \uC751\uB2F5\uC5D0 server.id \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1361
|
+
EXIT_API_ERROR
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
return this.get(raw.server.id);
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* 인스턴스를 삭제한다 (DELETE /servers/{id}, 204 No Content).
|
|
1368
|
+
*/
|
|
1369
|
+
async delete(id) {
|
|
1370
|
+
const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}`;
|
|
1371
|
+
try {
|
|
1372
|
+
await import_ky6.default.delete(url, {
|
|
1373
|
+
headers: this.authHeaders(),
|
|
1374
|
+
retry: 0,
|
|
1375
|
+
timeout: DEFAULT_TIMEOUT_MS2
|
|
1376
|
+
});
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
throw toNhnCloudCliError(err);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* 인스턴스가 ACTIVE 상태 + IP 1개 이상이 될 때까지 폴링한다.
|
|
1383
|
+
*
|
|
1384
|
+
* - status === "ACTIVE" + addresses 에 IP 1개 이상: 즉시 반환
|
|
1385
|
+
* - timeout 초과: 마지막 status 를 포함한 NhnCloudCliError(EXIT_API_ERROR)
|
|
1386
|
+
*/
|
|
1387
|
+
async waitForActive(id, opts) {
|
|
1388
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1389
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
1390
|
+
let lastServer = null;
|
|
1391
|
+
while (Date.now() < deadline) {
|
|
1392
|
+
const server = await this.get(id);
|
|
1393
|
+
lastServer = server;
|
|
1394
|
+
if (server.status === "ACTIVE" && hasIpAddress(server)) {
|
|
1395
|
+
return server;
|
|
1396
|
+
}
|
|
1397
|
+
const remaining = deadline - Date.now();
|
|
1398
|
+
if (remaining <= 0) break;
|
|
1399
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remaining)));
|
|
1400
|
+
}
|
|
1401
|
+
const lastStatus = lastServer ? lastServer.status : "unknown";
|
|
1402
|
+
throw new NhnCloudCliError(
|
|
1403
|
+
`\uC778\uC2A4\uD134\uC2A4 ${id} \uAC00 ACTIVE \uAC00 \uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 (\uB9C8\uC9C0\uB9C9 \uC0C1\uD0DC: ${lastStatus}). --wait \uD0C0\uC784\uC544\uC6C3(${Math.round(opts.timeoutMs / 1e3)}\uCD08) \uCD08\uACFC.`,
|
|
1404
|
+
EXIT_API_ERROR
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
// src/commands/instance/helpers.ts
|
|
1410
|
+
async function resolveInstanceClient(opts) {
|
|
1411
|
+
const profileName = await resolveProfileName(opts.profile);
|
|
1412
|
+
const iaas = await getIaasCredential(profileName);
|
|
1413
|
+
const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
|
|
1414
|
+
const { tokenId, computeEndpoint } = await getIaasToken(profileName, effectiveIaas);
|
|
1415
|
+
return { client: new InstanceClient(tokenId, computeEndpoint), profileName };
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/commands/instance/list.ts
|
|
1419
|
+
function getIps(server) {
|
|
1420
|
+
return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
|
|
1421
|
+
}
|
|
1422
|
+
var listCommand = new import_commander7.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) => {
|
|
1423
|
+
const opts = cmd.optsWithGlobals();
|
|
1424
|
+
const { client } = await resolveInstanceClient(opts);
|
|
1425
|
+
startSpinner("\uC778\uC2A4\uD134\uC2A4 \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
1426
|
+
let servers;
|
|
1427
|
+
try {
|
|
1428
|
+
servers = await client.list();
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
stopSpinner(false);
|
|
1431
|
+
throw err;
|
|
1432
|
+
}
|
|
1433
|
+
stopSpinner(true);
|
|
1434
|
+
output(opts, {
|
|
1435
|
+
headers: ["id", "name", "status", "IPs", "flavor"],
|
|
1436
|
+
rows: servers.map((s) => [s.id, s.name, s.status, getIps(s), s.flavor.id]),
|
|
1437
|
+
raw: servers,
|
|
1438
|
+
ids: servers.map((s) => s.id)
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// src/commands/instance/get.ts
|
|
1443
|
+
var import_commander8 = require("commander");
|
|
1444
|
+
function getIps2(server) {
|
|
1445
|
+
return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
|
|
1446
|
+
}
|
|
1447
|
+
function getImageId(server) {
|
|
1448
|
+
return typeof server.image === "object" ? server.image.id : "";
|
|
1449
|
+
}
|
|
1450
|
+
var getCommand = new import_commander8.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
|
+
const opts = cmd.optsWithGlobals();
|
|
1452
|
+
const { client } = await resolveInstanceClient(opts);
|
|
1453
|
+
startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC870\uD68C \uC911...");
|
|
1454
|
+
let server;
|
|
1455
|
+
try {
|
|
1456
|
+
server = await client.get(id);
|
|
1457
|
+
} catch (err) {
|
|
1458
|
+
stopSpinner(false);
|
|
1459
|
+
throw err;
|
|
1460
|
+
}
|
|
1461
|
+
stopSpinner(true);
|
|
1462
|
+
const rows = [
|
|
1463
|
+
["id", server.id],
|
|
1464
|
+
["name", server.name],
|
|
1465
|
+
["status", server.status],
|
|
1466
|
+
["IPs", getIps2(server)],
|
|
1467
|
+
["flavor", server.flavor.id],
|
|
1468
|
+
["image", getImageId(server)],
|
|
1469
|
+
["key_name", server.key_name ?? ""],
|
|
1470
|
+
["created", server.created],
|
|
1471
|
+
["updated", server.updated]
|
|
1472
|
+
];
|
|
1473
|
+
output(opts, {
|
|
1474
|
+
headers: ["field", "value"],
|
|
1475
|
+
rows,
|
|
1476
|
+
raw: server,
|
|
1477
|
+
ids: [server.id]
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
// src/commands/instance/create.ts
|
|
1482
|
+
var import_commander9 = require("commander");
|
|
1483
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
1484
|
+
function getFirstIp(server) {
|
|
1485
|
+
for (const list of Object.values(server.addresses)) {
|
|
1486
|
+
for (const addr of list) {
|
|
1487
|
+
return addr.addr;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return "";
|
|
1491
|
+
}
|
|
1492
|
+
function getIps3(server) {
|
|
1493
|
+
return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
|
|
1494
|
+
}
|
|
1495
|
+
function getImageId2(server) {
|
|
1496
|
+
return typeof server.image === "object" ? server.image.id : "";
|
|
1497
|
+
}
|
|
1498
|
+
var createCommand = new import_commander9.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("--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
|
+
const opts = cmd.optsWithGlobals();
|
|
1500
|
+
const networks = opts.network ?? [];
|
|
1501
|
+
if (networks.length === 0) {
|
|
1502
|
+
throw new NhnCloudCliError("--network \uB294 \uCD5C\uC18C 1\uAC1C \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
|
|
1503
|
+
}
|
|
1504
|
+
const timeoutMs = parseInt(opts.timeout ?? "300", 10) * 1e3;
|
|
1505
|
+
const { client } = await resolveInstanceClient(opts);
|
|
1506
|
+
startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131 \uC911...");
|
|
1507
|
+
let server;
|
|
1508
|
+
try {
|
|
1509
|
+
server = await client.create({
|
|
1510
|
+
name: opts.name,
|
|
1511
|
+
flavorRef: opts.flavor,
|
|
1512
|
+
imageRef: opts.image,
|
|
1513
|
+
networks,
|
|
1514
|
+
bootVolumeSize: opts.bootVolumeSize !== void 0 ? parseInt(opts.bootVolumeSize, 10) : void 0,
|
|
1515
|
+
keyName: opts.keyName,
|
|
1516
|
+
securityGroups: opts.securityGroup && opts.securityGroup.length > 0 ? opts.securityGroup : void 0,
|
|
1517
|
+
ephemeralDiskSize: opts.ephemeralDiskSize !== void 0 ? parseInt(opts.ephemeralDiskSize, 10) : void 0,
|
|
1518
|
+
protect: opts.protect
|
|
1519
|
+
});
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
stopSpinner(false);
|
|
1522
|
+
throw err;
|
|
1523
|
+
}
|
|
1524
|
+
stopSpinner(true);
|
|
1525
|
+
if (opts.wait) {
|
|
1526
|
+
startSpinner(`ACTIVE \uB300\uAE30 \uC911... (id: ${server.id})`);
|
|
1527
|
+
try {
|
|
1528
|
+
server = await client.waitForActive(server.id, { timeoutMs });
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
stopSpinner(false);
|
|
1531
|
+
throw err;
|
|
1532
|
+
}
|
|
1533
|
+
stopSpinner(true, `ACTIVE \uD655\uC778 (id: ${server.id})`);
|
|
1534
|
+
}
|
|
1535
|
+
if (opts.quiet && opts.wait) {
|
|
1536
|
+
const ip = getFirstIp(server);
|
|
1537
|
+
if (ip) process.stdout.write(ip + "\n");
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
if (opts.wait) {
|
|
1541
|
+
process.stderr.write(import_chalk2.default.green(` IP: ${getIps3(server)}
|
|
1542
|
+
`));
|
|
1543
|
+
}
|
|
1544
|
+
const rows = [
|
|
1545
|
+
["id", server.id],
|
|
1546
|
+
["name", server.name],
|
|
1547
|
+
["status", server.status],
|
|
1548
|
+
["IPs", getIps3(server)],
|
|
1549
|
+
["flavor", server.flavor.id],
|
|
1550
|
+
["image", getImageId2(server)]
|
|
1551
|
+
];
|
|
1552
|
+
output(opts, {
|
|
1553
|
+
headers: ["field", "value"],
|
|
1554
|
+
rows,
|
|
1555
|
+
raw: server,
|
|
1556
|
+
ids: [server.id]
|
|
1557
|
+
});
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
// src/commands/instance/delete.ts
|
|
1561
|
+
var import_commander10 = require("commander");
|
|
1562
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
1563
|
+
var deleteCommand = new import_commander10.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
|
+
const opts = cmd.optsWithGlobals();
|
|
1565
|
+
const isTTY = process.stdin.isTTY;
|
|
1566
|
+
if (!isTTY && !opts.yes) {
|
|
1567
|
+
throw new NhnCloudCliError(
|
|
1568
|
+
"\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C \uC778\uC2A4\uD134\uC2A4 \uC0AD\uC81C\uB294 --yes \uD50C\uB798\uADF8\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
1569
|
+
EXIT_PARAM_ERROR
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
if (isTTY && !opts.yes) {
|
|
1573
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
1574
|
+
const ok = await confirm({
|
|
1575
|
+
message: `\uC778\uC2A4\uD134\uC2A4 "${id}" \uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`,
|
|
1576
|
+
default: false
|
|
1577
|
+
});
|
|
1578
|
+
if (!ok) {
|
|
1579
|
+
process.stderr.write(import_chalk3.default.yellow("\uC0AD\uC81C\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
const { client } = await resolveInstanceClient(opts);
|
|
1584
|
+
startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC0AD\uC81C \uC911... (id: ${id})`);
|
|
1585
|
+
try {
|
|
1586
|
+
await client.delete(id);
|
|
1587
|
+
} catch (err) {
|
|
1588
|
+
stopSpinner(false);
|
|
1589
|
+
throw err;
|
|
1590
|
+
}
|
|
1591
|
+
stopSpinner(true);
|
|
1592
|
+
process.stderr.write(import_chalk3.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
|
|
1593
|
+
`));
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1020
1596
|
// src/index.ts
|
|
1021
|
-
var program = new
|
|
1022
|
-
program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.
|
|
1597
|
+
var program = new import_commander11.Command();
|
|
1598
|
+
program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.2.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");
|
|
1023
1599
|
program.hook("preAction", () => {
|
|
1024
1600
|
const opts = program.opts();
|
|
1025
1601
|
if (!opts.color || process.env["NO_COLOR"]) {
|
|
1026
|
-
|
|
1602
|
+
import_chalk4.default.level = 0;
|
|
1027
1603
|
}
|
|
1028
1604
|
if (opts.json || opts.quiet) {
|
|
1029
1605
|
setQuiet(true);
|
|
1030
1606
|
}
|
|
1031
1607
|
});
|
|
1032
1608
|
program.addCommand(configureCommand);
|
|
1033
|
-
var logncrashCommand = new
|
|
1609
|
+
var logncrashCommand = new import_commander11.Command("logncrash").description("Log & Crash \uAD00\uB828 \uBA85\uB839");
|
|
1034
1610
|
logncrashCommand.addCommand(searchCommand);
|
|
1035
1611
|
program.addCommand(logncrashCommand);
|
|
1036
|
-
var deployCommand = new
|
|
1612
|
+
var deployCommand = new import_commander11.Command("deploy").description("NHN Cloud Deploy \uAD00\uB828 \uBA85\uB839");
|
|
1037
1613
|
deployCommand.addCommand(runCommand);
|
|
1038
1614
|
deployCommand.addCommand(artifactsCommand);
|
|
1039
1615
|
deployCommand.addCommand(serverGroupsCommand);
|
|
1040
1616
|
deployCommand.addCommand(historiesCommand);
|
|
1041
1617
|
program.addCommand(deployCommand);
|
|
1618
|
+
var instanceCommand = new import_commander11.Command("instance").description("Compute \uC778\uC2A4\uD134\uC2A4 \uAD00\uB828 \uBA85\uB839");
|
|
1619
|
+
instanceCommand.addCommand(listCommand);
|
|
1620
|
+
instanceCommand.addCommand(getCommand);
|
|
1621
|
+
instanceCommand.addCommand(createCommand);
|
|
1622
|
+
instanceCommand.addCommand(deleteCommand);
|
|
1623
|
+
program.addCommand(instanceCommand);
|
|
1042
1624
|
program.parseAsync().catch((err) => {
|
|
1043
1625
|
const message = err instanceof Error ? err.message : String(err);
|
|
1044
1626
|
const exitCode = err instanceof NhnCloudCliError ? err.exitCode : 1;
|
|
1045
|
-
process.stderr.write(
|
|
1627
|
+
process.stderr.write(import_chalk4.default.red(`\uC624\uB958: ${message}`) + "\n");
|
|
1046
1628
|
process.exit(exitCode);
|
|
1047
1629
|
});
|
package/package.json
CHANGED
|
@@ -250,3 +250,120 @@ nhncloud deploy histories my-service --json | jq '.[0] | {deployKey, deployStatu
|
|
|
250
250
|
| UAK 누락 / config target 없음 | 4 (CONFIG_ERROR) 또는 3 (PARAM_ERROR) |
|
|
251
251
|
| OAuth 인증 실패 (401/403) | 2 (AUTH_ERROR) |
|
|
252
252
|
| Deploy API 오류 / 봉투 실패 | 1 (API_ERROR) |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## instance — Compute 인스턴스 발급·조회·삭제
|
|
257
|
+
|
|
258
|
+
`instance` 명령군은 OpenStack Nova v2 호환 Compute API 를 호출한다.
|
|
259
|
+
Keystone v2 토큰 인증을 사용하며 토큰은 만료 전까지 region 별로 캐시한다.
|
|
260
|
+
|
|
261
|
+
### instance 설정
|
|
262
|
+
|
|
263
|
+
`nhncloud configure` 대화형 마법사에서 "iaas 자격증명도 설정하시겠습니까?" 에 응답하거나,
|
|
264
|
+
flag 로 직접 입력한다.
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# 대화형 (권장)
|
|
268
|
+
nhncloud configure
|
|
269
|
+
|
|
270
|
+
# 비대화형 — API 비밀번호는 env 로 전달 (cmdline 노출 방지)
|
|
271
|
+
NHNCLOUD_IAAS_PASSWORD=<api-password> nhncloud configure \
|
|
272
|
+
--iaas-tenant-id <tenant-id> \
|
|
273
|
+
--iaas-username <iam-username> \
|
|
274
|
+
--iaas-region kr1 \
|
|
275
|
+
--no-verify
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
> **주의 (password)**: `--iaas-password` 에 입력하는 값은 NHN Cloud 콘솔 IAM 의 **API 비밀번호**입니다.
|
|
279
|
+
> NHN Cloud 로그인 비밀번호와 다릅니다.
|
|
280
|
+
> IAM 사용자 상세 페이지 → "API 비밀번호 설정"에서 별도로 발급합니다.
|
|
281
|
+
>
|
|
282
|
+
> **주의 (username)**: `--iaas-username` 은 NHN Cloud 계정 이메일 **또는 IAM 계정 ID(사번)** 입니다.
|
|
283
|
+
> tenantId 와 비슷한 32자리 hex "API 사용자 ID"(UUID)가 아닙니다 — 잘못 넣으면 `Could not find user` 인증 실패가 납니다.
|
|
284
|
+
|
|
285
|
+
저장 위치: `~/.nhncloud/credentials.json` 의 `profiles.<profile>.iaas` 블록.
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"version": 1,
|
|
290
|
+
"profiles": {
|
|
291
|
+
"default": {
|
|
292
|
+
"iaas": {
|
|
293
|
+
"tenantId": "<tenant-id>",
|
|
294
|
+
"username": "<iam-username>",
|
|
295
|
+
"password": "<api-password>",
|
|
296
|
+
"region": "kr1"
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 의도 → 커맨드 매핑
|
|
304
|
+
|
|
305
|
+
| 의도 | 커맨드 |
|
|
306
|
+
|------|--------|
|
|
307
|
+
| 인스턴스 목록 조회 | `nhncloud instance list` |
|
|
308
|
+
| 특정 인스턴스 상태 조회 | `nhncloud instance get <id>` |
|
|
309
|
+
| 인스턴스 생성 (즉시 반환) | `nhncloud instance create --name <name> --flavor <id> --image <id> --network <uuid>` |
|
|
310
|
+
| 인스턴스 생성 + ACTIVE 대기 | `nhncloud instance create ... --wait` |
|
|
311
|
+
| GPU 인스턴스 생성 | `nhncloud instance create --flavor <gpu-flavor-id> --boot-volume-size <gb> ...` (GPU 는 boot-from-volume 필수) |
|
|
312
|
+
| 인스턴스 삭제 (confirm 없이) | `nhncloud instance delete <id> --yes` |
|
|
313
|
+
| 다른 region 사용 | `nhncloud instance list --region kr2` |
|
|
314
|
+
| 다른 profile 사용 | `nhncloud instance list --profile staging` |
|
|
315
|
+
|
|
316
|
+
### instance create 옵션
|
|
317
|
+
|
|
318
|
+
| 옵션 | 필수 | 설명 |
|
|
319
|
+
|------|:---:|------|
|
|
320
|
+
| `--name <name>` | 예 | 인스턴스 이름 |
|
|
321
|
+
| `--flavor <id>` | 예 | flavor ID (CPU/메모리 사양. GPU 발급 시 GPU flavor ID 지정) |
|
|
322
|
+
| `--image <id>` | 예 | 이미지 ID |
|
|
323
|
+
| `--network <uuid>` | 예 | 네트워크 UUID (반복 지정으로 여러 개 가능) |
|
|
324
|
+
| `--boot-volume-size <gb>` | 조건부 | boot-from-volume root 볼륨 크기(GB). **GPU(g2) 등 일부 flavor 는 필수** (없으면 `Missing Block Device Mapping` 발급 실패). 미지정 시 로컬 디스크 부팅 |
|
|
325
|
+
| `--key-name <name>` | 아니오 | 키페어 이름 |
|
|
326
|
+
| `--security-group <name>` | 아니오 | 보안 그룹 이름 (반복 지정) |
|
|
327
|
+
| `--ephemeral-disk-size <gb>` | 아니오 | 임시 디스크 크기(GB, NHN 확장) |
|
|
328
|
+
| `--protect` | 아니오 | 삭제 방지 설정 (NHN 확장) |
|
|
329
|
+
| `--wait` | 아니오 | ACTIVE 상태 + IP 할당까지 대기 |
|
|
330
|
+
| `--timeout <sec>` | 아니오 | wait 타임아웃 (초, 기본 300) |
|
|
331
|
+
| `--region <region>` | 아니오 | region override (kr1/kr2/kr3/jp1) |
|
|
332
|
+
| `--profile <name>` | 아니오 | 사용할 profile 이름 |
|
|
333
|
+
|
|
334
|
+
### 체이닝 예시
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# 1. 인스턴스 생성 후 ACTIVE + IP 대기 → IP 추출 → SSH 접속
|
|
338
|
+
IP=$(nhncloud instance create \
|
|
339
|
+
--name ci-runner \
|
|
340
|
+
--flavor <flavor-id> \
|
|
341
|
+
--image <image-id> \
|
|
342
|
+
--network <network-uuid> \
|
|
343
|
+
--wait --quiet)
|
|
344
|
+
ssh ubuntu@"$IP" "echo ready"
|
|
345
|
+
|
|
346
|
+
# 2. --wait --json 으로 전체 필드 취득 후 jq 로 IP 파싱
|
|
347
|
+
nhncloud instance create \
|
|
348
|
+
--name ci-runner \
|
|
349
|
+
--flavor <flavor-id> \
|
|
350
|
+
--image <image-id> \
|
|
351
|
+
--network <network-uuid> \
|
|
352
|
+
--wait --json | jq -r '.addresses | to_entries[0].value[0].addr'
|
|
353
|
+
|
|
354
|
+
# 3. 사용 후 삭제 (ephemeral CI 패턴)
|
|
355
|
+
nhncloud instance delete "$INSTANCE_ID" --yes
|
|
356
|
+
|
|
357
|
+
# 4. 목록에서 id 만 추출
|
|
358
|
+
nhncloud instance list --quiet
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### instance 에러 코드
|
|
362
|
+
|
|
363
|
+
| 상황 | exit code |
|
|
364
|
+
|------|-----------|
|
|
365
|
+
| iaas 자격증명 누락·불완전 | 4 (CONFIG_ERROR) |
|
|
366
|
+
| Keystone 인증 실패 (401/403) | 2 (AUTH_ERROR) |
|
|
367
|
+
| 미등록 region / 필수 옵션 누락 | 3 (PARAM_ERROR) |
|
|
368
|
+
| API 오류 / waitForActive 타임아웃 | 1 (API_ERROR) |
|
|
369
|
+
| 비대화형 delete 에서 --yes 미지정 | 3 (PARAM_ERROR) |
|