@joshualiamzn/open-stack 0.0.1 → 0.0.2
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/.kiro/memory-session-id +1 -0
- package/package.json +3 -1
- package/src/aws.mjs +680 -61
- package/src/cli.mjs +57 -7
- package/src/commands/create.mjs +200 -18
- package/src/commands/demo.mjs +193 -0
- package/src/commands/describe.mjs +53 -43
- package/src/commands/help.mjs +5 -6
- package/src/commands/index.mjs +14 -17
- package/src/commands/list.mjs +14 -13
- package/src/config.mjs +11 -1
- package/src/eks.mjs +356 -0
- package/src/interactive.mjs +290 -198
- package/src/main.mjs +111 -16
- package/src/render.mjs +1 -2
- package/src/repl.mjs +19 -43
- package/src/ui.mjs +123 -12
- package/src/commands/update.mjs +0 -309
package/src/cli.mjs
CHANGED
|
@@ -6,11 +6,17 @@ import { DEFAULTS } from './config.mjs';
|
|
|
6
6
|
* Returns null when no flags were given (triggers interactive mode).
|
|
7
7
|
*/
|
|
8
8
|
export function parseCli(argv) {
|
|
9
|
+
// No args → interactive REPL
|
|
10
|
+
if (argv.length <= 2) return null;
|
|
11
|
+
|
|
12
|
+
// Demo subcommand — separate parser to avoid option conflicts
|
|
13
|
+
if (argv[2] === 'demo') return parseDemoArgs(argv);
|
|
14
|
+
|
|
9
15
|
const program = new Command()
|
|
10
16
|
.name('open-stack')
|
|
11
17
|
.description(
|
|
12
|
-
'Create
|
|
13
|
-
'
|
|
18
|
+
'Create and manage your observability stack on AWS:\n' +
|
|
19
|
+
'OpenSearch, Prometheus, IAM roles, and ingestion pipelines.'
|
|
14
20
|
)
|
|
15
21
|
.version('1.0.0');
|
|
16
22
|
|
|
@@ -47,6 +53,10 @@ export function parseCli(argv) {
|
|
|
47
53
|
.option('--prometheus-url <url>', 'Reuse an existing APS remote-write URL')
|
|
48
54
|
.option('--aps-workspace-alias <name>', 'Alias for new APS workspace');
|
|
49
55
|
|
|
56
|
+
// Dashboards
|
|
57
|
+
program
|
|
58
|
+
.option('--dashboards-url <url>', 'Reuse an existing OpenSearch UI URL');
|
|
59
|
+
|
|
50
60
|
// Pipeline tuning
|
|
51
61
|
program
|
|
52
62
|
.option('--min-ocu <n>', 'Minimum OCUs', DEFAULTS.minOcu)
|
|
@@ -59,13 +69,24 @@ export function parseCli(argv) {
|
|
|
59
69
|
.option('--dry-run', 'Generate config only; do not create AWS resources');
|
|
60
70
|
|
|
61
71
|
program.parse(argv);
|
|
62
|
-
const opts = program.opts();
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (userArgs.length === 0) return null;
|
|
73
|
+
return optsToConfig(program.opts());
|
|
74
|
+
}
|
|
67
75
|
|
|
68
|
-
|
|
76
|
+
function parseDemoArgs(argv) {
|
|
77
|
+
const program = new Command()
|
|
78
|
+
.name('open-stack demo')
|
|
79
|
+
.description('Create an EKS cluster and install the observability stack + OTel demo')
|
|
80
|
+
.option('--cluster-name <name>', 'EKS cluster name', 'open-stack-demo')
|
|
81
|
+
.option('--region <region>', 'AWS region', process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION)
|
|
82
|
+
.option('--pipeline <name>', 'Existing OSI pipeline name to connect')
|
|
83
|
+
.option('--node-count <n>', 'Number of EKS nodes', '3')
|
|
84
|
+
.option('--instance-type <type>', 'EKS node instance type', 'm8i.large')
|
|
85
|
+
.option('--skip-otel-demo', 'Skip installing the OpenTelemetry Demo app');
|
|
86
|
+
|
|
87
|
+
program.parse(argv.slice(1));
|
|
88
|
+
const opts = program.opts();
|
|
89
|
+
return { _command: 'demo', ...opts, nodeCount: Number(opts.nodeCount) };
|
|
69
90
|
}
|
|
70
91
|
|
|
71
92
|
/**
|
|
@@ -87,6 +108,10 @@ function optsToConfig(opts) {
|
|
|
87
108
|
if (opts.prometheusUrl) apsAction = 'reuse';
|
|
88
109
|
else if (opts.apsWorkspaceAlias) apsAction = 'create';
|
|
89
110
|
|
|
111
|
+
let dashboardsAction = '';
|
|
112
|
+
if (opts.dashboardsUrl) dashboardsAction = 'reuse';
|
|
113
|
+
else dashboardsAction = 'create';
|
|
114
|
+
|
|
90
115
|
// Auto-detect serverless from endpoint URL when not explicitly set
|
|
91
116
|
if (opts.serverless && opts.managed) {
|
|
92
117
|
throw new Error('--serverless and --managed are mutually exclusive');
|
|
@@ -127,6 +152,15 @@ function optsToConfig(opts) {
|
|
|
127
152
|
minOcu: Number(opts.minOcu),
|
|
128
153
|
maxOcu: Number(opts.maxOcu),
|
|
129
154
|
serviceMapWindow: opts.serviceMapWindow,
|
|
155
|
+
dashboardsAction,
|
|
156
|
+
dashboardsUrl: opts.dashboardsUrl || '',
|
|
157
|
+
dqsRoleName: '',
|
|
158
|
+
dqsRoleArn: '',
|
|
159
|
+
dqsDataSourceName: '',
|
|
160
|
+
dqsDataSourceArn: '',
|
|
161
|
+
appName: '',
|
|
162
|
+
appId: '',
|
|
163
|
+
appEndpoint: '',
|
|
130
164
|
outputFile: opts.output || '',
|
|
131
165
|
dryRun: opts.dryRun || false,
|
|
132
166
|
accountId: '',
|
|
@@ -144,6 +178,10 @@ export function applySimpleDefaults(cfg) {
|
|
|
144
178
|
if (!cfg.iamRoleName) cfg.iamRoleName = `${cfg.pipelineName}-osi-role`;
|
|
145
179
|
if (!cfg.apsAction) cfg.apsAction = 'create';
|
|
146
180
|
if (!cfg.apsWorkspaceAlias) cfg.apsWorkspaceAlias = cfg.pipelineName;
|
|
181
|
+
if (!cfg.dashboardsAction) cfg.dashboardsAction = 'create';
|
|
182
|
+
if (!cfg.dqsRoleName) cfg.dqsRoleName = `${cfg.pipelineName}-dqs-prometheus-role`;
|
|
183
|
+
if (!cfg.dqsDataSourceName) cfg.dqsDataSourceName = `${cfg.pipelineName.replace(/-/g, '_')}_prometheus`;
|
|
184
|
+
if (!cfg.appName) cfg.appName = cfg.pipelineName;
|
|
147
185
|
}
|
|
148
186
|
|
|
149
187
|
/**
|
|
@@ -161,6 +199,15 @@ export function fillDryRunPlaceholders(cfg) {
|
|
|
161
199
|
if (cfg.apsAction === 'create' && !cfg.prometheusUrl) {
|
|
162
200
|
cfg.prometheusUrl = `https://aps-workspaces.${cfg.region}.amazonaws.com/workspaces/<workspace-id>/api/v1/remote_write`;
|
|
163
201
|
}
|
|
202
|
+
if (!cfg.dqsRoleArn && cfg.dqsRoleName) {
|
|
203
|
+
cfg.dqsRoleArn = `arn:aws:iam::${cfg.accountId || '123456789012'}:role/${cfg.dqsRoleName}`;
|
|
204
|
+
}
|
|
205
|
+
if (!cfg.dqsDataSourceArn && cfg.dqsDataSourceName) {
|
|
206
|
+
cfg.dqsDataSourceArn = `arn:aws:opensearch:${cfg.region}:${cfg.accountId || '123456789012'}:datasource/${cfg.dqsDataSourceName}`;
|
|
207
|
+
}
|
|
208
|
+
if (!cfg.appEndpoint) {
|
|
209
|
+
cfg.appEndpoint = `https://<app-id>.${cfg.region}.opensearch.amazonaws.com`;
|
|
210
|
+
}
|
|
164
211
|
}
|
|
165
212
|
|
|
166
213
|
/**
|
|
@@ -181,6 +228,9 @@ export function validateConfig(cfg) {
|
|
|
181
228
|
if (cfg.apsAction === 'reuse' && !cfg.prometheusUrl) {
|
|
182
229
|
errors.push('--prometheus-url required when reusing APS workspace');
|
|
183
230
|
}
|
|
231
|
+
if (cfg.dashboardsAction === 'reuse' && !cfg.dashboardsUrl) {
|
|
232
|
+
errors.push('--dashboards-url required when reusing OpenSearch UI');
|
|
233
|
+
}
|
|
184
234
|
|
|
185
235
|
// Format checks
|
|
186
236
|
if (cfg.region && !/^[a-z]{2}-[a-z]+-\d+$/.test(cfg.region)) {
|
package/src/commands/create.mjs
CHANGED
|
@@ -1,15 +1,188 @@
|
|
|
1
1
|
import { runCreateWizard } from '../interactive.mjs';
|
|
2
|
-
import { applySimpleDefaults, validateConfig
|
|
3
|
-
import { renderPipeline } from '../render.mjs';
|
|
2
|
+
import { applySimpleDefaults, validateConfig } from '../cli.mjs';
|
|
4
3
|
import { executePipeline } from '../main.mjs';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { promptDemoAfterCreate } from './demo.mjs';
|
|
5
|
+
import { printError, printStep, theme, GoBack, eConfirm } from '../ui.mjs';
|
|
6
|
+
|
|
7
|
+
// ── Architecture diagram ────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const strip = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a styled box. Returns { w, mid, inner, top, bot, botC, lines }.
|
|
13
|
+
* All strings have correct visual width thanks to ANSI-aware padding.
|
|
14
|
+
*/
|
|
15
|
+
function box(texts, minInner = 0) {
|
|
16
|
+
const m = theme.muted;
|
|
17
|
+
const inner = Math.max(minInner, ...texts.map((t) => strip(t).length + 2));
|
|
18
|
+
const w = inner + 2;
|
|
19
|
+
const mid = Math.floor(w / 2);
|
|
20
|
+
|
|
21
|
+
const padLine = (t) => {
|
|
22
|
+
const gap = inner - strip(t).length - 1;
|
|
23
|
+
return m('│') + ' ' + t + ' '.repeat(Math.max(0, gap)) + m('│');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
w, mid, inner,
|
|
28
|
+
top: m('┌' + '─'.repeat(inner) + '┐'),
|
|
29
|
+
bot: m('└' + '─'.repeat(inner) + '┘'),
|
|
30
|
+
botC: m('└' + '─'.repeat(mid - 1) + '┬' + '─'.repeat(inner - mid) + '┘'),
|
|
31
|
+
lines: texts.map(padLine),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Build a horizontal connector line in a char array, then wrap with muted style. */
|
|
36
|
+
function hline(width, from, to, specs) {
|
|
37
|
+
const arr = Array(width).fill(' ');
|
|
38
|
+
for (let i = from; i <= to; i++) arr[i] = '─';
|
|
39
|
+
for (const [pos, ch] of specs) if (pos >= 0 && pos < width) arr[pos] = ch;
|
|
40
|
+
return theme.muted(arr.join('').trimEnd());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Place single characters at given positions, everything else is a space. */
|
|
44
|
+
function connector(width, specs) {
|
|
45
|
+
const arr = Array(width).fill(' ');
|
|
46
|
+
for (const [pos, ch] of specs) if (pos >= 0 && pos < width) arr[pos] = ch;
|
|
47
|
+
return theme.muted(arr.join('').trimEnd());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderArchitectureDiagram(cfg) {
|
|
51
|
+
const osLabel = cfg.serverless ? 'OpenSearch Serverless' : 'OpenSearch';
|
|
52
|
+
const pathLabel = `/${cfg.pipelineName}/v1/*`;
|
|
53
|
+
const m = theme.muted;
|
|
54
|
+
const a = theme.accent;
|
|
55
|
+
const p = theme.primary;
|
|
56
|
+
const h = theme.highlight;
|
|
57
|
+
const sp = (n) => ' '.repeat(Math.max(0, n));
|
|
58
|
+
|
|
59
|
+
// ── Define all boxes ──────────────────────────────────────────────────
|
|
60
|
+
const otlp = box([a('OSI Endpoint'), m(pathLabel)], 21);
|
|
61
|
+
const logs = box([h('Logs')], 9);
|
|
62
|
+
const traces = box([h('Traces')], 9);
|
|
63
|
+
const metrics = box([h('Metrics')], 9);
|
|
64
|
+
const raw = box([m('Raw'), m('Traces')], 9);
|
|
65
|
+
const svc = box([m('Service'), m('Map')], 9);
|
|
66
|
+
const os = box([p(osLabel), m('logs, traces, svc-map')]);
|
|
67
|
+
const prom = box([p('AWS Prometheus'), m('metrics, svc-map')]);
|
|
68
|
+
const dash = box([p('OpenSearch UI'), m('Observability workspace')]);
|
|
69
|
+
|
|
70
|
+
// ── Layout positions ──────────────────────────────────────────────────
|
|
71
|
+
const sigGap = 2;
|
|
72
|
+
const subGap = 1;
|
|
73
|
+
const sinkGap = 1;
|
|
74
|
+
|
|
75
|
+
// Signal row — three boxes side by side (column 0 origin)
|
|
76
|
+
const C_LOGS = logs.mid;
|
|
77
|
+
const C_TRACES = logs.w + sigGap + traces.mid;
|
|
78
|
+
const C_METRICS = logs.w + sigGap + traces.w + sigGap + metrics.mid;
|
|
79
|
+
const totalSigW = logs.w + sigGap + traces.w + sigGap + metrics.w;
|
|
80
|
+
|
|
81
|
+
// Sub-stage row — centered under Traces
|
|
82
|
+
const subTotalW = raw.w + subGap + svc.w;
|
|
83
|
+
const subOff = C_TRACES - Math.floor(subTotalW / 2);
|
|
84
|
+
const C_RAW = subOff + raw.mid;
|
|
85
|
+
const C_SVC = subOff + raw.w + subGap + svc.mid;
|
|
86
|
+
|
|
87
|
+
// OTLP — centered above Traces column
|
|
88
|
+
const otlpOff = Math.max(0, C_TRACES - otlp.mid);
|
|
89
|
+
const C_OTLP = otlpOff + otlp.mid;
|
|
90
|
+
|
|
91
|
+
// Service-map split — symmetric fork, left into OS box, right into Prom box
|
|
92
|
+
const C_SVC_L = C_SVC - 3;
|
|
93
|
+
const C_SVC_R = C_SVC + 3;
|
|
94
|
+
|
|
95
|
+
// Diagram width (widest row)
|
|
96
|
+
const W = Math.max(totalSigW, os.w + sinkGap + prom.w);
|
|
97
|
+
|
|
98
|
+
// ── Assemble lines ────────────────────────────────────────────────────
|
|
99
|
+
const out = [''];
|
|
100
|
+
|
|
101
|
+
// OTLP box
|
|
102
|
+
out.push(sp(otlpOff) + otlp.top);
|
|
103
|
+
for (const l of otlp.lines) out.push(sp(otlpOff) + l);
|
|
104
|
+
out.push(sp(otlpOff) + otlp.botC);
|
|
105
|
+
|
|
106
|
+
// Fan-out from OTLP to three signal columns
|
|
107
|
+
out.push(hline(W, C_LOGS, C_METRICS, [
|
|
108
|
+
[C_LOGS, '┌'], [C_OTLP, '┼'], [C_TRACES, C_TRACES === C_OTLP ? '┼' : '┬'], [C_METRICS, '┐'],
|
|
109
|
+
]));
|
|
110
|
+
out.push(connector(W, [[C_LOGS, '▼'], [C_TRACES, '▼'], [C_METRICS, '▼']]));
|
|
111
|
+
|
|
112
|
+
// Signal boxes
|
|
113
|
+
out.push([logs, traces, metrics].map((b) => b.top).join(sp(sigGap)));
|
|
114
|
+
const sigRows = Math.max(logs.lines.length, traces.lines.length, metrics.lines.length);
|
|
115
|
+
for (let r = 0; r < sigRows; r++) {
|
|
116
|
+
out.push([logs, traces, metrics].map((b) => b.lines[r] || sp(b.w)).join(sp(sigGap)));
|
|
117
|
+
}
|
|
118
|
+
out.push([logs, traces, metrics].map((b) => b.botC).join(sp(sigGap)));
|
|
119
|
+
|
|
120
|
+
// Traces fans into Raw Traces + Service Map
|
|
121
|
+
out.push(hline(W, C_RAW, C_SVC, [
|
|
122
|
+
[C_LOGS, '│'], [C_RAW, '┌'], [C_TRACES, '┴'], [C_SVC, '┐'], [C_METRICS, '│'],
|
|
123
|
+
]));
|
|
124
|
+
out.push(connector(W, [[C_LOGS, '│'], [C_RAW, '▼'], [C_SVC, '▼'], [C_METRICS, '│']]));
|
|
125
|
+
|
|
126
|
+
// Sub-stage boxes with Logs/Metrics pipes on either side
|
|
127
|
+
const subLine = (content) =>
|
|
128
|
+
sp(C_LOGS) + m('│') + sp(subOff - C_LOGS - 1) + content + sp(C_METRICS - subOff - subTotalW) + m('│');
|
|
129
|
+
|
|
130
|
+
out.push(subLine(raw.top + sp(subGap) + svc.top));
|
|
131
|
+
for (let r = 0; r < Math.max(raw.lines.length, svc.lines.length); r++) {
|
|
132
|
+
out.push(subLine((raw.lines[r] || sp(raw.w)) + sp(subGap) + (svc.lines[r] || sp(svc.w))));
|
|
133
|
+
}
|
|
134
|
+
out.push(subLine(raw.botC + sp(subGap) + svc.botC));
|
|
135
|
+
|
|
136
|
+
// Service-map split + merge toward sinks
|
|
137
|
+
out.push(hline(W, C_SVC_L, C_SVC_R, [
|
|
138
|
+
[C_LOGS, '│'], [C_RAW, '│'], [C_SVC_L, '┌'], [C_SVC, '┴'], [C_SVC_R, '┐'], [C_METRICS, '│'],
|
|
139
|
+
]));
|
|
140
|
+
out.push(connector(W, [
|
|
141
|
+
[C_LOGS, '▼'], [C_RAW, '▼'], [C_SVC_L, '▼'], [C_SVC_R, '▼'], [C_METRICS, '▼'],
|
|
142
|
+
]));
|
|
143
|
+
|
|
144
|
+
// Sink boxes
|
|
145
|
+
out.push(os.top + sp(sinkGap) + prom.top);
|
|
146
|
+
for (let r = 0; r < Math.max(os.lines.length, prom.lines.length); r++) {
|
|
147
|
+
out.push((os.lines[r] || sp(os.w)) + sp(sinkGap) + (prom.lines[r] || sp(prom.w)));
|
|
148
|
+
}
|
|
149
|
+
const C_PROM = os.w + sinkGap + prom.mid;
|
|
150
|
+
out.push(os.botC + sp(sinkGap) + prom.botC);
|
|
151
|
+
|
|
152
|
+
// Prometheus → DQS box
|
|
153
|
+
out.push(connector(os.w + sinkGap + prom.w, [[os.mid, '│'], [C_PROM, '│']]));
|
|
154
|
+
out.push(connector(os.w + sinkGap + prom.w, [[os.mid, '│'], [C_PROM, '▼']]));
|
|
155
|
+
|
|
156
|
+
const dqs = box([p('Direct Query Service')]);
|
|
157
|
+
const dqsOff = Math.max(0, C_PROM - dqs.mid);
|
|
158
|
+
const C_DQS = dqsOff + dqs.mid;
|
|
159
|
+
const osPipe = (rest) => sp(os.mid) + m('│') + sp(dqsOff - os.mid - 1) + rest;
|
|
160
|
+
out.push(osPipe(dqs.top));
|
|
161
|
+
for (const l of dqs.lines) out.push(osPipe(l));
|
|
162
|
+
out.push(osPipe(dqs.botC));
|
|
163
|
+
|
|
164
|
+
// Merge OpenSearch + DQS → Dashboards
|
|
165
|
+
out.push(hline(dqsOff + dqs.w, os.mid, C_DQS, [
|
|
166
|
+
[os.mid, '└'], [C_DQS, '┘'],
|
|
167
|
+
]));
|
|
168
|
+
const mergeMid = Math.floor((os.mid + C_DQS) / 2);
|
|
169
|
+
out.push(connector(dqsOff + dqs.w, [[mergeMid, '│']]));
|
|
170
|
+
out.push(connector(dqsOff + dqs.w, [[mergeMid, '▼']]));
|
|
171
|
+
|
|
172
|
+
// Dashboards box — centered under merge point
|
|
173
|
+
const dashOff = Math.max(0, mergeMid - dash.mid);
|
|
174
|
+
out.push(sp(dashOff) + dash.top);
|
|
175
|
+
for (const l of dash.lines) out.push(sp(dashOff) + l);
|
|
176
|
+
out.push(sp(dashOff) + dash.bot);
|
|
177
|
+
out.push('');
|
|
178
|
+
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
7
181
|
|
|
8
182
|
export async function runCreate(session) {
|
|
9
183
|
console.error();
|
|
10
|
-
printDivider();
|
|
11
|
-
|
|
12
184
|
const cfg = await runCreateWizard(session);
|
|
185
|
+
if (cfg === GoBack) return GoBack;
|
|
13
186
|
|
|
14
187
|
// Apply simple-mode defaults
|
|
15
188
|
if (!cfg.mode) cfg.mode = 'simple';
|
|
@@ -22,22 +195,31 @@ export async function runCreate(session) {
|
|
|
22
195
|
return;
|
|
23
196
|
}
|
|
24
197
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
198
|
+
// ── Show architecture diagram ──────────────────────────────────────────
|
|
199
|
+
printStep('Architecture');
|
|
200
|
+
const diagram = renderArchitectureDiagram(cfg);
|
|
201
|
+
for (const line of diagram) console.error(` ${line}`);
|
|
202
|
+
|
|
203
|
+
// ── Confirm to proceed ─────────────────────────────────────────────────
|
|
204
|
+
console.error();
|
|
205
|
+
const proceed = await eConfirm({
|
|
206
|
+
message: 'Create these resources and deploy the stack?',
|
|
207
|
+
default: true,
|
|
208
|
+
});
|
|
209
|
+
if (proceed === GoBack || !proceed) {
|
|
210
|
+
console.error(` ${theme.muted('Cancelled.')}`);
|
|
37
211
|
console.error();
|
|
38
212
|
return;
|
|
39
213
|
}
|
|
40
214
|
|
|
41
215
|
// Live path
|
|
42
216
|
await executePipeline(cfg);
|
|
217
|
+
|
|
218
|
+
// Offer to create demo EKS services
|
|
219
|
+
try {
|
|
220
|
+
await promptDemoAfterCreate(session, cfg.pipelineName);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
// Demo is optional — don't fail the create flow
|
|
223
|
+
console.error(` ${theme.muted(`Demo setup skipped: ${err.message}`)}`);
|
|
224
|
+
}
|
|
43
225
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
printStep, printInfo, printBox, printWarning,
|
|
3
|
+
theme, GoBack, STAR, eSelect, eInput, eConfirm, createSpinner,
|
|
4
|
+
} from '../ui.mjs';
|
|
5
|
+
import { listPipelines, getPipelineEndpoint } from '../aws.mjs';
|
|
6
|
+
import { checkDemoPrerequisites, createEksCluster, installHelmChart, installOtelDemo } from '../eks.mjs';
|
|
7
|
+
|
|
8
|
+
const DEMO_DEFAULTS = {
|
|
9
|
+
clusterName: 'open-stack-demo',
|
|
10
|
+
nodeCount: 3,
|
|
11
|
+
instanceType: 'm8i.large',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Prompt user to select an OSI pipeline. Returns { pipelineName, otlpEndpoint } or GoBack.
|
|
16
|
+
*/
|
|
17
|
+
async function selectPipeline(region) {
|
|
18
|
+
const spinner = createSpinner('Loading pipelines...');
|
|
19
|
+
spinner.start();
|
|
20
|
+
|
|
21
|
+
let pipelines;
|
|
22
|
+
try {
|
|
23
|
+
pipelines = await listPipelines(region);
|
|
24
|
+
spinner.succeed(`${pipelines.length} pipeline${pipelines.length !== 1 ? 's' : ''} found`);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
spinner.fail('Failed to list pipelines');
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const active = pipelines.filter((p) => p.status === 'ACTIVE');
|
|
31
|
+
if (active.length === 0) {
|
|
32
|
+
printWarning('No active OSI pipelines found. Create one first.');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const choice = await eSelect({
|
|
37
|
+
message: 'Select OSI pipeline to connect',
|
|
38
|
+
choices: active.map((p) => ({
|
|
39
|
+
name: `${theme.accent(p.name)} ${theme.muted(p.status)}`,
|
|
40
|
+
value: p.name,
|
|
41
|
+
})),
|
|
42
|
+
});
|
|
43
|
+
if (choice === GoBack) return GoBack;
|
|
44
|
+
|
|
45
|
+
const endpointSpinner = createSpinner('Getting pipeline endpoint...');
|
|
46
|
+
endpointSpinner.start();
|
|
47
|
+
try {
|
|
48
|
+
const endpoint = await getPipelineEndpoint(region, choice);
|
|
49
|
+
endpointSpinner.succeed(`Endpoint: ${endpoint}`);
|
|
50
|
+
return { pipelineName: choice, otlpEndpoint: endpoint };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
endpointSpinner.fail('Failed to get pipeline endpoint');
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run the demo services command — creates an EKS cluster and installs
|
|
59
|
+
* the observability stack Helm chart with demo applications.
|
|
60
|
+
*/
|
|
61
|
+
export async function runDemo(session) {
|
|
62
|
+
console.error();
|
|
63
|
+
printStep('Install Demo');
|
|
64
|
+
printInfo('Creates an EKS cluster and installs the observability stack Helm chart with demo applications');
|
|
65
|
+
printInfo(`Default config: ${DEMO_DEFAULTS.nodeCount} x ${DEMO_DEFAULTS.instanceType} nodes`);
|
|
66
|
+
console.error();
|
|
67
|
+
|
|
68
|
+
// Collect cluster name
|
|
69
|
+
const clusterName = await eInput({
|
|
70
|
+
message: 'EKS cluster name',
|
|
71
|
+
default: DEMO_DEFAULTS.clusterName,
|
|
72
|
+
validate: (v) => v.trim().length > 0 || 'Cluster name is required',
|
|
73
|
+
});
|
|
74
|
+
if (clusterName === GoBack) return GoBack;
|
|
75
|
+
|
|
76
|
+
// Select OSI pipeline to connect
|
|
77
|
+
const pipeline = await selectPipeline(session.region);
|
|
78
|
+
if (pipeline === GoBack) return GoBack;
|
|
79
|
+
if (!pipeline) return;
|
|
80
|
+
|
|
81
|
+
const { nodeCount, instanceType } = DEMO_DEFAULTS;
|
|
82
|
+
|
|
83
|
+
// Confirm
|
|
84
|
+
console.error();
|
|
85
|
+
printInfo(`Cluster: ${theme.accent(clusterName)} | Nodes: ${theme.accent(String(nodeCount))} x ${theme.accent(instanceType)} | Region: ${theme.accent(session.region)}`);
|
|
86
|
+
printInfo(`Pipeline: ${theme.accent(pipeline.pipelineName)}`);
|
|
87
|
+
console.error();
|
|
88
|
+
|
|
89
|
+
const proceed = await eConfirm({
|
|
90
|
+
message: 'Create EKS cluster and install demo services?',
|
|
91
|
+
default: true,
|
|
92
|
+
});
|
|
93
|
+
if (proceed === GoBack || !proceed) {
|
|
94
|
+
console.error(` ${theme.muted('Cancelled.')}`);
|
|
95
|
+
console.error();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Execute
|
|
100
|
+
console.error();
|
|
101
|
+
checkDemoPrerequisites();
|
|
102
|
+
await createEksCluster({
|
|
103
|
+
clusterName,
|
|
104
|
+
region: session.region,
|
|
105
|
+
nodeCount,
|
|
106
|
+
instanceType,
|
|
107
|
+
stackName: pipeline.pipelineName,
|
|
108
|
+
accountId: session.accountId,
|
|
109
|
+
});
|
|
110
|
+
console.error();
|
|
111
|
+
await installHelmChart({ otlpEndpoint: pipeline.otlpEndpoint });
|
|
112
|
+
console.error();
|
|
113
|
+
await installOtelDemo({ otlpEndpoint: pipeline.otlpEndpoint });
|
|
114
|
+
console.error();
|
|
115
|
+
|
|
116
|
+
printBox([
|
|
117
|
+
'',
|
|
118
|
+
`${theme.success.bold(`${STAR} Demo Services Deployed! ${STAR}`)}`,
|
|
119
|
+
'',
|
|
120
|
+
`${theme.label('Cluster:')} ${clusterName}`,
|
|
121
|
+
`${theme.label('Region:')} ${session.region}`,
|
|
122
|
+
`${theme.label('Nodes:')} ${nodeCount} x ${instanceType}`,
|
|
123
|
+
`${theme.label('Pipeline:')} ${pipeline.pipelineName}`,
|
|
124
|
+
`${theme.label('Namespace:')} observability`,
|
|
125
|
+
`${theme.label('Release:')} obs-stack`,
|
|
126
|
+
'',
|
|
127
|
+
`${theme.muted('Check pods:')} kubectl get pods -n observability`,
|
|
128
|
+
`${theme.muted('OTel Demo pods:')} kubectl get pods -n otel-demo`,
|
|
129
|
+
`${theme.muted('Dashboards:')} kubectl port-forward svc/obs-stack-opensearch-dashboards 5601:5601 -n observability`,
|
|
130
|
+
`${theme.muted('Demo Frontend:')} kubectl port-forward svc/otel-demo-frontend-proxy 8080:8080 -n otel-demo`,
|
|
131
|
+
'',
|
|
132
|
+
], { color: 'primary', padding: 2 });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Prompt the user to optionally create demo EKS services after stack creation.
|
|
137
|
+
* When called from the create flow, pipelineName is the pipeline just created.
|
|
138
|
+
* Returns true if demo was created, false if skipped.
|
|
139
|
+
*/
|
|
140
|
+
export async function promptDemoAfterCreate(session, pipelineName) {
|
|
141
|
+
console.error();
|
|
142
|
+
const wantDemo = await eConfirm({
|
|
143
|
+
message: 'Would you like to install demo EKS services?',
|
|
144
|
+
default: false,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (wantDemo === GoBack || !wantDemo) return false;
|
|
148
|
+
|
|
149
|
+
console.error();
|
|
150
|
+
const clusterName = await eInput({
|
|
151
|
+
message: 'EKS cluster name',
|
|
152
|
+
default: DEMO_DEFAULTS.clusterName,
|
|
153
|
+
validate: (v) => v.trim().length > 0 || 'Cluster name is required',
|
|
154
|
+
});
|
|
155
|
+
if (clusterName === GoBack) return false;
|
|
156
|
+
|
|
157
|
+
console.error();
|
|
158
|
+
checkDemoPrerequisites();
|
|
159
|
+
|
|
160
|
+
// Get the OTLP endpoint for the just-created pipeline
|
|
161
|
+
let otlpEndpoint;
|
|
162
|
+
try {
|
|
163
|
+
otlpEndpoint = await getPipelineEndpoint(session.region, pipelineName);
|
|
164
|
+
if (otlpEndpoint) {
|
|
165
|
+
printInfo(`Using pipeline: ${theme.accent(pipelineName)}`);
|
|
166
|
+
printInfo(`OTLP endpoint: ${theme.accent(otlpEndpoint)}`);
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
printWarning('Could not get pipeline endpoint — Helm chart will use default config');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const { nodeCount, instanceType } = DEMO_DEFAULTS;
|
|
173
|
+
|
|
174
|
+
console.error();
|
|
175
|
+
await createEksCluster({
|
|
176
|
+
clusterName,
|
|
177
|
+
region: session.region,
|
|
178
|
+
nodeCount,
|
|
179
|
+
instanceType,
|
|
180
|
+
stackName: pipelineName,
|
|
181
|
+
accountId: session.accountId,
|
|
182
|
+
});
|
|
183
|
+
console.error();
|
|
184
|
+
await installHelmChart({ otlpEndpoint });
|
|
185
|
+
console.error();
|
|
186
|
+
await installOtelDemo({ otlpEndpoint });
|
|
187
|
+
console.error();
|
|
188
|
+
|
|
189
|
+
printInfo(`Demo services deployed. Check: kubectl get pods -n observability`);
|
|
190
|
+
printInfo(`OTel Demo pods: kubectl get pods -n otel-demo`);
|
|
191
|
+
printInfo(`Demo Frontend: kubectl port-forward svc/otel-demo-frontend-proxy 8080:8080 -n otel-demo`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
@@ -1,71 +1,81 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { loadPipelines } from './index.mjs';
|
|
1
|
+
import { getStackResources, arnToName, describeResource, enrichResourceNames } from '../aws.mjs';
|
|
2
|
+
import { printInfo, createSpinner, printPanel, theme, GoBack, eSelect } from '../ui.mjs';
|
|
3
|
+
import { loadStacks } from './index.mjs';
|
|
5
4
|
|
|
6
5
|
export async function runDescribe(session) {
|
|
7
6
|
console.error();
|
|
8
7
|
|
|
9
|
-
const
|
|
8
|
+
const stacks = await loadStacks(session.region);
|
|
10
9
|
|
|
11
|
-
if (
|
|
12
|
-
printInfo('No
|
|
10
|
+
if (stacks.length === 0) {
|
|
11
|
+
printInfo('No open-stack stacks found in this region.');
|
|
13
12
|
console.error();
|
|
14
13
|
return;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
// Select a
|
|
18
|
-
const choices =
|
|
19
|
-
name: `${
|
|
20
|
-
value:
|
|
16
|
+
// Select a stack
|
|
17
|
+
const choices = stacks.map((s) => ({
|
|
18
|
+
name: `${s.name} ${theme.muted(`(${s.resources.length} resources)`)}`,
|
|
19
|
+
value: s.name,
|
|
21
20
|
}));
|
|
22
21
|
|
|
23
|
-
const
|
|
24
|
-
message: 'Select
|
|
22
|
+
const stackName = await eSelect({
|
|
23
|
+
message: 'Select stack',
|
|
25
24
|
choices,
|
|
26
25
|
});
|
|
26
|
+
if (stackName === GoBack) return GoBack;
|
|
27
27
|
|
|
28
|
-
// Fetch full
|
|
29
|
-
const detailSpinner = createSpinner(`Loading ${
|
|
28
|
+
// Fetch full resource list
|
|
29
|
+
const detailSpinner = createSpinner(`Loading ${stackName}...`);
|
|
30
30
|
detailSpinner.start();
|
|
31
31
|
|
|
32
|
-
let
|
|
32
|
+
let resources;
|
|
33
33
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
resources = await getStackResources(session.region, stackName);
|
|
35
|
+
await enrichResourceNames(session.region, resources);
|
|
36
|
+
detailSpinner.succeed(`Stack: ${stackName} (${resources.length} resources)`);
|
|
36
37
|
} catch (err) {
|
|
37
|
-
detailSpinner.fail('Failed to get
|
|
38
|
+
detailSpinner.fail('Failed to get stack details');
|
|
38
39
|
throw err;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
console.error();
|
|
43
|
-
const entries = [
|
|
44
|
-
['Name', pipeline.name],
|
|
45
|
-
['ARN', theme.muted(pipeline.arn)],
|
|
46
|
-
['Status', colorStatus(pipeline.status)],
|
|
47
|
-
];
|
|
48
|
-
if (pipeline.statusReason) {
|
|
49
|
-
entries.push(['Status Reason', pipeline.statusReason]);
|
|
50
|
-
}
|
|
51
|
-
entries.push(
|
|
52
|
-
['Min OCUs', String(pipeline.minUnits)],
|
|
53
|
-
['Max OCUs', String(pipeline.maxUnits)],
|
|
54
|
-
['Created', formatDate(pipeline.createdAt)],
|
|
55
|
-
['Last Updated', formatDate(pipeline.lastUpdatedAt)],
|
|
56
|
-
);
|
|
57
|
-
printPanel(pipelineName, entries);
|
|
42
|
+
const resName = (r) => r.displayName || arnToName(r.arn);
|
|
58
43
|
|
|
59
|
-
|
|
44
|
+
// Resource selection loop
|
|
45
|
+
while (true) {
|
|
60
46
|
console.error();
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
47
|
+
const resourceChoices = resources.map((r) => ({
|
|
48
|
+
name: `${theme.accentBold(r.type)} ${theme.muted(resName(r))}`,
|
|
49
|
+
value: r,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const selected = await eSelect({
|
|
53
|
+
message: 'Select resource',
|
|
54
|
+
choices: resourceChoices,
|
|
55
|
+
});
|
|
56
|
+
if (selected === GoBack) break;
|
|
57
|
+
|
|
58
|
+
// Fetch and display resource details
|
|
59
|
+
const resSpinner = createSpinner(`Loading ${selected.type}...`);
|
|
60
|
+
resSpinner.start();
|
|
61
|
+
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
result = await describeResource(session.region, selected);
|
|
65
|
+
resSpinner.succeed(`${selected.type}: ${resName(selected)}`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
resSpinner.fail(`Failed to describe ${selected.type}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
64
70
|
|
|
65
|
-
if (pipeline.pipelineConfigurationBody) {
|
|
66
71
|
console.error();
|
|
67
|
-
const
|
|
68
|
-
|
|
72
|
+
const panelEntries = result.entries.map(([label, value]) => [label, theme.muted(value)]);
|
|
73
|
+
printPanel(resName(selected), panelEntries);
|
|
74
|
+
|
|
75
|
+
if (result.rawConfig) {
|
|
76
|
+
console.error();
|
|
77
|
+
console.error(theme.muted(result.rawConfig));
|
|
78
|
+
}
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
console.error();
|