@occam-scaly/scaly-cli 0.2.7 → 0.2.8
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/bin/scaly.js +97 -26
- package/lib/scaly-api.js +98 -47
- package/lib/scaly-apply.js +11 -4
- package/package.json +1 -1
package/bin/scaly.js
CHANGED
|
@@ -487,28 +487,14 @@ async function sourceLocalEnv() {
|
|
|
487
487
|
}
|
|
488
488
|
|
|
489
489
|
async function importHelpers() {
|
|
490
|
-
const {
|
|
491
|
-
SecretsManagerClient,
|
|
492
|
-
GetSecretValueCommand
|
|
493
|
-
} = require('@aws-sdk/client-secrets-manager');
|
|
494
|
-
const {
|
|
495
|
-
CloudWatchLogsClient,
|
|
496
|
-
DescribeLogStreamsCommand
|
|
497
|
-
} = require('@aws-sdk/client-cloudwatch-logs');
|
|
498
|
-
const {
|
|
499
|
-
ECSClient,
|
|
500
|
-
ListClustersCommand,
|
|
501
|
-
ListServicesCommand,
|
|
502
|
-
DescribeServicesCommand,
|
|
503
|
-
ListTasksCommand,
|
|
504
|
-
DescribeTasksCommand
|
|
505
|
-
} = require('@aws-sdk/client-ecs');
|
|
506
|
-
const {
|
|
507
|
-
ElasticLoadBalancingV2Client,
|
|
508
|
-
DescribeTargetGroupsCommand,
|
|
509
|
-
DescribeTargetHealthCommand
|
|
510
|
-
} = require('@aws-sdk/client-elastic-load-balancing-v2');
|
|
511
490
|
const axios = require('axios');
|
|
491
|
+
const { resolveApiEndpoint } = require('../lib/scaly-api');
|
|
492
|
+
const { normalizeStage, readAuthStore } = require('../lib/scaly-auth');
|
|
493
|
+
|
|
494
|
+
// Ensure API endpoint is set for common commands (db/secrets/etc).
|
|
495
|
+
if (!process.env.API_ENDPOINT && !process.env.SCALY_API_URL) {
|
|
496
|
+
process.env.API_ENDPOINT = resolveApiEndpoint();
|
|
497
|
+
}
|
|
512
498
|
|
|
513
499
|
const API = () => process.env.API_ENDPOINT;
|
|
514
500
|
const deriveDomain = () => {
|
|
@@ -528,12 +514,48 @@ async function importHelpers() {
|
|
|
528
514
|
})[String(r || '').toUpperCase()] || null;
|
|
529
515
|
|
|
530
516
|
async function getToken() {
|
|
531
|
-
// Prefer stored bearer token or env override
|
|
517
|
+
// Prefer stored bearer token or env override.
|
|
532
518
|
const t = getStoredBearer();
|
|
533
519
|
if (t) return t;
|
|
534
520
|
if (process.env.SCALY_API_BEARER) return process.env.SCALY_API_BEARER;
|
|
535
|
-
|
|
521
|
+
|
|
522
|
+
// Prefer a Scaly session token from `scaly auth login`.
|
|
523
|
+
if (process.env.SCALY_OIDC_TOKEN) return process.env.SCALY_OIDC_TOKEN;
|
|
524
|
+
const found = readAuthStore();
|
|
525
|
+
if (found && found.session && found.session.access_token) {
|
|
526
|
+
const s = found.session;
|
|
527
|
+
const expMs = typeof s.expires_at === 'number' ? s.expires_at : null;
|
|
528
|
+
const stage = normalizeStage(
|
|
529
|
+
s.stage || process.env.SCALY_STAGE || 'prod'
|
|
530
|
+
);
|
|
531
|
+
if (!process.env.SCALY_STAGE) process.env.SCALY_STAGE = stage;
|
|
532
|
+
if (expMs && Date.now() >= expMs) {
|
|
533
|
+
const e = new Error(
|
|
534
|
+
`Your Scaly session token expired. Run: scaly auth login --stage ${stage}`
|
|
535
|
+
);
|
|
536
|
+
e.code = 'TOKEN_EXPIRED';
|
|
537
|
+
throw e;
|
|
538
|
+
}
|
|
539
|
+
return s.access_token;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Internal-only fallback (dev tooling): Cognito client credentials via AWS Secrets Manager.
|
|
543
|
+
if (!process.env.COGNITO_SECRET) {
|
|
544
|
+
const stage = normalizeStage(process.env.SCALY_STAGE || 'prod');
|
|
545
|
+
const e = new Error(
|
|
546
|
+
`Not authenticated. Run: scaly auth login --stage ${stage}`
|
|
547
|
+
);
|
|
548
|
+
e.code = 'SCALY_AUTH_REQUIRED';
|
|
549
|
+
throw e;
|
|
550
|
+
}
|
|
536
551
|
const region = process.env.COGNITO_SECRET_REGION || 'ca-central-1';
|
|
552
|
+
console.error(
|
|
553
|
+
'[warn] Using COGNITO_SECRET flow (internal dev only). Prefer: scaly auth login'
|
|
554
|
+
);
|
|
555
|
+
const {
|
|
556
|
+
SecretsManagerClient,
|
|
557
|
+
GetSecretValueCommand
|
|
558
|
+
} = require('@aws-sdk/client-secrets-manager');
|
|
537
559
|
const sm = new SecretsManagerClient({ region });
|
|
538
560
|
const res = await sm.send(
|
|
539
561
|
new GetSecretValueCommand({ SecretId: process.env.COGNITO_SECRET })
|
|
@@ -571,9 +593,13 @@ async function importHelpers() {
|
|
|
571
593
|
}
|
|
572
594
|
|
|
573
595
|
function elbClient(region, creds) {
|
|
596
|
+
const {
|
|
597
|
+
ElasticLoadBalancingV2Client
|
|
598
|
+
} = require('@aws-sdk/client-elastic-load-balancing-v2');
|
|
574
599
|
return new ElasticLoadBalancingV2Client({ region, credentials: creds });
|
|
575
600
|
}
|
|
576
601
|
function ecsClient(region, creds) {
|
|
602
|
+
const { ECSClient } = require('@aws-sdk/client-ecs');
|
|
577
603
|
return new ECSClient({ region, credentials: creds });
|
|
578
604
|
}
|
|
579
605
|
|
|
@@ -599,6 +625,10 @@ async function importHelpers() {
|
|
|
599
625
|
|
|
600
626
|
async function checkCloudwatchLogs(accountId, appId, region) {
|
|
601
627
|
try {
|
|
628
|
+
const {
|
|
629
|
+
CloudWatchLogsClient,
|
|
630
|
+
DescribeLogStreamsCommand
|
|
631
|
+
} = require('@aws-sdk/client-cloudwatch-logs');
|
|
602
632
|
const cwl = new CloudWatchLogsClient({ region });
|
|
603
633
|
const group = `/scaly/account/${accountId}`;
|
|
604
634
|
const cmd = new DescribeLogStreamsCommand({
|
|
@@ -2284,6 +2314,36 @@ async function runSecrets(sub, rest) {
|
|
|
2284
2314
|
const json = parseBool(f.json, false);
|
|
2285
2315
|
|
|
2286
2316
|
// OIDC-only for build secrets.
|
|
2317
|
+
try {
|
|
2318
|
+
const { resolveApiEndpoint } = require('../lib/scaly-api');
|
|
2319
|
+
const { normalizeStage, readAuthStore } = require('../lib/scaly-auth');
|
|
2320
|
+
if (!process.env.API_ENDPOINT && !process.env.SCALY_API_URL) {
|
|
2321
|
+
process.env.API_ENDPOINT = resolveApiEndpoint();
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
const found = readAuthStore();
|
|
2325
|
+
if (
|
|
2326
|
+
found &&
|
|
2327
|
+
found.session &&
|
|
2328
|
+
found.session.access_token &&
|
|
2329
|
+
!process.env.SCALY_API_BEARER
|
|
2330
|
+
) {
|
|
2331
|
+
const expMs =
|
|
2332
|
+
typeof found.session.expires_at === 'number'
|
|
2333
|
+
? found.session.expires_at
|
|
2334
|
+
: null;
|
|
2335
|
+
const stage = normalizeStage(
|
|
2336
|
+
found.session.stage || process.env.SCALY_STAGE || 'prod'
|
|
2337
|
+
);
|
|
2338
|
+
if (!process.env.SCALY_STAGE) process.env.SCALY_STAGE = stage;
|
|
2339
|
+
if (expMs && Date.now() >= expMs) {
|
|
2340
|
+
// Fall through to error message below.
|
|
2341
|
+
} else {
|
|
2342
|
+
process.env.SCALY_API_BEARER = found.session.access_token;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
} catch {}
|
|
2346
|
+
|
|
2287
2347
|
try {
|
|
2288
2348
|
const t = getStoredBearer();
|
|
2289
2349
|
if (t && !process.env.SCALY_API_BEARER) process.env.SCALY_API_BEARER = t;
|
|
@@ -2295,8 +2355,8 @@ async function runSecrets(sub, rest) {
|
|
|
2295
2355
|
process.env.API_BEARER;
|
|
2296
2356
|
if (!bearer || !isProbablyJwt(bearer)) {
|
|
2297
2357
|
const msg =
|
|
2298
|
-
'secrets commands require a
|
|
2299
|
-
'Run `scaly login
|
|
2358
|
+
'secrets commands require a Scaly session token (JWT). ' +
|
|
2359
|
+
'Run `scaly auth login` first.';
|
|
2300
2360
|
if (json)
|
|
2301
2361
|
console.log(JSON.stringify({ ok: false, error: { message: msg } }));
|
|
2302
2362
|
else console.error(msg);
|
|
@@ -2566,7 +2626,7 @@ async function mintDbProxyInfo(rest) {
|
|
|
2566
2626
|
const bearer = await getToken();
|
|
2567
2627
|
if (!isProbablyJwt(bearer)) {
|
|
2568
2628
|
const e = new Error(
|
|
2569
|
-
'db commands require a
|
|
2629
|
+
'db commands require a Scaly session token (JWT). Run `scaly auth login` first.'
|
|
2570
2630
|
);
|
|
2571
2631
|
e.code = 'SCALY_DB_JWT_REQUIRED';
|
|
2572
2632
|
throw e;
|
|
@@ -3059,6 +3119,17 @@ async function runDiagnoseApp(rest) {
|
|
|
3059
3119
|
});
|
|
3060
3120
|
const c = credsRes?.issueAwsCredentials;
|
|
3061
3121
|
if (c?.accessKeyId) {
|
|
3122
|
+
const {
|
|
3123
|
+
DescribeTargetGroupsCommand,
|
|
3124
|
+
DescribeTargetHealthCommand
|
|
3125
|
+
} = require('@aws-sdk/client-elastic-load-balancing-v2');
|
|
3126
|
+
const {
|
|
3127
|
+
ListClustersCommand,
|
|
3128
|
+
ListServicesCommand,
|
|
3129
|
+
DescribeServicesCommand,
|
|
3130
|
+
ListTasksCommand,
|
|
3131
|
+
DescribeTasksCommand
|
|
3132
|
+
} = require('@aws-sdk/client-ecs');
|
|
3062
3133
|
const creds = makeCreds(c);
|
|
3063
3134
|
// Target group heuristic: scaly-<first8-of-stackId>
|
|
3064
3135
|
const shortId = String(stackId).split('-')[0];
|
package/lib/scaly-api.js
CHANGED
|
@@ -140,7 +140,7 @@ function buildHeaders(auth) {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
async function graphqlRequest(query, variables) {
|
|
143
|
+
async function graphqlRequest(query, variables, extraHeaders) {
|
|
144
144
|
const endpoint = resolveApiEndpoint();
|
|
145
145
|
const auth = resolveAuth();
|
|
146
146
|
if (!auth) {
|
|
@@ -156,7 +156,10 @@ async function graphqlRequest(query, variables) {
|
|
|
156
156
|
{
|
|
157
157
|
headers: {
|
|
158
158
|
'content-type': 'application/json',
|
|
159
|
-
...buildHeaders(auth)
|
|
159
|
+
...buildHeaders(auth),
|
|
160
|
+
...(extraHeaders && typeof extraHeaders === 'object'
|
|
161
|
+
? extraHeaders
|
|
162
|
+
: {})
|
|
160
163
|
},
|
|
161
164
|
timeout: 60_000
|
|
162
165
|
}
|
|
@@ -381,63 +384,111 @@ async function listAddOnsByType({ accountId, type, take = 10 }) {
|
|
|
381
384
|
return data?.listAddOns || [];
|
|
382
385
|
}
|
|
383
386
|
|
|
384
|
-
async function createStack({
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
387
|
+
async function createStack({
|
|
388
|
+
accountId,
|
|
389
|
+
name,
|
|
390
|
+
size,
|
|
391
|
+
minIdle,
|
|
392
|
+
executionGroupId
|
|
393
|
+
}) {
|
|
394
|
+
const data = await graphqlRequest(
|
|
395
|
+
MUTATIONS.createStack,
|
|
396
|
+
{
|
|
397
|
+
data: {
|
|
398
|
+
name,
|
|
399
|
+
size,
|
|
400
|
+
minIdle: typeof minIdle === 'number' ? minIdle : undefined,
|
|
401
|
+
account: { connect: { id: accountId } }
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
executionGroupId
|
|
405
|
+
? { 'x-scaly-execution-group-id': executionGroupId }
|
|
406
|
+
: undefined
|
|
407
|
+
);
|
|
393
408
|
return data?.createStack;
|
|
394
409
|
}
|
|
395
410
|
|
|
396
|
-
async function updateStack({ id, name, size, minIdle }) {
|
|
397
|
-
const data = await graphqlRequest(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
411
|
+
async function updateStack({ id, name, size, minIdle, executionGroupId }) {
|
|
412
|
+
const data = await graphqlRequest(
|
|
413
|
+
MUTATIONS.updateStack,
|
|
414
|
+
{
|
|
415
|
+
where: { id },
|
|
416
|
+
data: {
|
|
417
|
+
name: name || undefined,
|
|
418
|
+
size: size || undefined,
|
|
419
|
+
minIdle: typeof minIdle === 'number' ? minIdle : undefined
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
executionGroupId
|
|
423
|
+
? { 'x-scaly-execution-group-id': executionGroupId }
|
|
424
|
+
: undefined
|
|
425
|
+
);
|
|
405
426
|
return data?.updateStack;
|
|
406
427
|
}
|
|
407
428
|
|
|
408
|
-
async function createApp({
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
429
|
+
async function createApp({
|
|
430
|
+
accountId,
|
|
431
|
+
stackId,
|
|
432
|
+
name,
|
|
433
|
+
minIdle,
|
|
434
|
+
executionGroupId
|
|
435
|
+
}) {
|
|
436
|
+
const data = await graphqlRequest(
|
|
437
|
+
MUTATIONS.createApp,
|
|
438
|
+
{
|
|
439
|
+
data: {
|
|
440
|
+
name,
|
|
441
|
+
minIdle: typeof minIdle === 'number' ? minIdle : undefined,
|
|
442
|
+
account: { connect: { id: accountId } },
|
|
443
|
+
stack: stackId ? { connect: { id: stackId } } : undefined
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
executionGroupId
|
|
447
|
+
? { 'x-scaly-execution-group-id': executionGroupId }
|
|
448
|
+
: undefined
|
|
449
|
+
);
|
|
417
450
|
return data?.createApp;
|
|
418
451
|
}
|
|
419
452
|
|
|
420
|
-
async function updateApp({ id, stackId, name, minIdle }) {
|
|
421
|
-
const data = await graphqlRequest(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
453
|
+
async function updateApp({ id, stackId, name, minIdle, executionGroupId }) {
|
|
454
|
+
const data = await graphqlRequest(
|
|
455
|
+
MUTATIONS.updateApp,
|
|
456
|
+
{
|
|
457
|
+
where: { id },
|
|
458
|
+
data: {
|
|
459
|
+
name: name || undefined,
|
|
460
|
+
minIdle: typeof minIdle === 'number' ? minIdle : undefined,
|
|
461
|
+
stack: stackId ? { connect: { id: stackId } } : undefined
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
executionGroupId
|
|
465
|
+
? { 'x-scaly-execution-group-id': executionGroupId }
|
|
466
|
+
: undefined
|
|
467
|
+
);
|
|
429
468
|
return data?.updateApp;
|
|
430
469
|
}
|
|
431
470
|
|
|
432
|
-
async function createAddOn({
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
471
|
+
async function createAddOn({
|
|
472
|
+
accountId,
|
|
473
|
+
name,
|
|
474
|
+
type,
|
|
475
|
+
database,
|
|
476
|
+
executionGroupId
|
|
477
|
+
}) {
|
|
478
|
+
const data = await graphqlRequest(
|
|
479
|
+
MUTATIONS.createAddOn,
|
|
480
|
+
{
|
|
481
|
+
data: {
|
|
482
|
+
name,
|
|
483
|
+
type,
|
|
484
|
+
account: { connect: { id: accountId } },
|
|
485
|
+
addOnDatabase: database ? { create: database } : undefined
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
executionGroupId
|
|
489
|
+
? { 'x-scaly-execution-group-id': executionGroupId }
|
|
490
|
+
: undefined
|
|
491
|
+
);
|
|
441
492
|
return data?.createAddOn;
|
|
442
493
|
}
|
|
443
494
|
|
package/lib/scaly-apply.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const api = require('./scaly-api');
|
|
4
|
+
const { randomUUID } = require('crypto');
|
|
4
5
|
|
|
5
6
|
async function applyPlan({ config, plan, autoApprove = false }) {
|
|
6
7
|
const actionable = (plan.operations || []).filter(
|
|
@@ -8,6 +9,7 @@ async function applyPlan({ config, plan, autoApprove = false }) {
|
|
|
8
9
|
);
|
|
9
10
|
|
|
10
11
|
const results = [];
|
|
12
|
+
const executionGroupId = randomUUID();
|
|
11
13
|
|
|
12
14
|
let createdStack = null;
|
|
13
15
|
let stackId = null;
|
|
@@ -20,7 +22,8 @@ async function applyPlan({ config, plan, autoApprove = false }) {
|
|
|
20
22
|
accountId: op.desired.accountId,
|
|
21
23
|
name: op.desired.name,
|
|
22
24
|
size: op.desired.size,
|
|
23
|
-
minIdle: op.desired.minIdle
|
|
25
|
+
minIdle: op.desired.minIdle,
|
|
26
|
+
executionGroupId
|
|
24
27
|
});
|
|
25
28
|
createdStack = res;
|
|
26
29
|
stackId = res?.id || stackId;
|
|
@@ -33,7 +36,8 @@ async function applyPlan({ config, plan, autoApprove = false }) {
|
|
|
33
36
|
id: op.resource.id,
|
|
34
37
|
name: op.desired.name,
|
|
35
38
|
size: op.desired.size,
|
|
36
|
-
minIdle: op.desired.minIdle
|
|
39
|
+
minIdle: op.desired.minIdle,
|
|
40
|
+
executionGroupId
|
|
37
41
|
});
|
|
38
42
|
stackId = res?.id || stackId;
|
|
39
43
|
results.push({ op_id: op.id, ok: true, result: res });
|
|
@@ -48,7 +52,8 @@ async function applyPlan({ config, plan, autoApprove = false }) {
|
|
|
48
52
|
const res = await api.createApp({
|
|
49
53
|
accountId: op.desired.accountId,
|
|
50
54
|
stackId,
|
|
51
|
-
name: op.desired.name
|
|
55
|
+
name: op.desired.name,
|
|
56
|
+
executionGroupId
|
|
52
57
|
});
|
|
53
58
|
results.push({ op_id: op.id, ok: true, result: res });
|
|
54
59
|
continue;
|
|
@@ -59,7 +64,8 @@ async function applyPlan({ config, plan, autoApprove = false }) {
|
|
|
59
64
|
accountId: op.desired.accountId,
|
|
60
65
|
name: op.desired.name,
|
|
61
66
|
type: op.desired.type,
|
|
62
|
-
database: op.desired.database || undefined
|
|
67
|
+
database: op.desired.database || undefined,
|
|
68
|
+
executionGroupId
|
|
63
69
|
});
|
|
64
70
|
results.push({ op_id: op.id, ok: true, result: res });
|
|
65
71
|
continue;
|
|
@@ -76,6 +82,7 @@ async function applyPlan({ config, plan, autoApprove = false }) {
|
|
|
76
82
|
return {
|
|
77
83
|
ok,
|
|
78
84
|
auto_approve: !!autoApprove,
|
|
85
|
+
execution_group_id: executionGroupId,
|
|
79
86
|
applied: actionable.length,
|
|
80
87
|
results,
|
|
81
88
|
created_stack_id: createdStack?.id || null
|