@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.
@@ -4,16 +4,15 @@ export async function runHelp() {
4
4
  console.error();
5
5
  const lines = [
6
6
  '',
7
- `${theme.accentBold('create')} Create a new OSI pipeline (interactive wizard)`,
8
- `${theme.accentBold('list')} List existing OSI pipelines`,
9
- `${theme.accentBold('describe')} Show details of a specific pipeline`,
10
- `${theme.accentBold('update')} Update an existing pipeline's settings`,
7
+ `${theme.accentBold('create')} Create a new observability stack`,
8
+ `${theme.accentBold('list')} List existing stacks`,
9
+ `${theme.accentBold('describe')} Show details of a stack`,
11
10
  `${theme.accentBold('help')} Show this help message`,
12
- `${theme.accentBold('quit')} Exit the pipeline manager`,
11
+ `${theme.accentBold('quit')} Exit Open Stack`,
13
12
  '',
14
13
  ];
15
14
  printBox(lines, { title: 'Commands', color: 'dim', padding: 2 });
16
15
  console.error();
17
- printKeyHint([['Ctrl+C', 'cancel any operation and return to menu']]);
16
+ printKeyHint([['Ctrl+C', 'exit']]);
18
17
  console.error();
19
18
  }
@@ -1,23 +1,22 @@
1
1
  import { runCreate } from './create.mjs';
2
2
  import { runList } from './list.mjs';
3
3
  import { runDescribe } from './describe.mjs';
4
- import { runUpdate } from './update.mjs';
5
- import { runHelp } from './help.mjs';
6
- import { listPipelines } from '../aws.mjs';
4
+ import { runDemo } from './demo.mjs';
5
+ import { listStacks } from '../aws.mjs';
7
6
  import { createSpinner, theme } from '../ui.mjs';
8
7
 
9
8
  /**
10
- * Load pipelines with a spinner. Shared by describe, list, and update commands.
9
+ * Load stacks with a spinner. Shared by describe and list commands.
11
10
  */
12
- export async function loadPipelines(region) {
13
- const spinner = createSpinner('Loading pipelines...');
11
+ export async function loadStacks(region) {
12
+ const spinner = createSpinner('Loading stacks...');
14
13
  spinner.start();
15
14
  try {
16
- const pipelines = await listPipelines(region);
17
- spinner.succeed(`${pipelines.length} pipeline${pipelines.length !== 1 ? 's' : ''} found`);
18
- return pipelines;
15
+ const stacks = await listStacks(region);
16
+ spinner.succeed(`${stacks.length} stack${stacks.length !== 1 ? 's' : ''} found`);
17
+ return stacks;
19
18
  } catch (err) {
20
- spinner.fail('Failed to list pipelines');
19
+ spinner.fail('Failed to list stacks');
21
20
  throw err;
22
21
  }
23
22
  }
@@ -26,15 +25,13 @@ export const COMMANDS = {
26
25
  create: runCreate,
27
26
  list: runList,
28
27
  describe: runDescribe,
29
- update: runUpdate,
30
- help: runHelp,
28
+ demo: runDemo,
31
29
  };
32
30
 
33
31
  export const COMMAND_CHOICES = [
34
- { name: `\u2728 Create ${theme.muted('Create a new OSI pipeline')}`, value: 'create' },
35
- { name: `\u2630 List ${theme.muted('List existing pipelines')}`, value: 'list' },
36
- { name: `\uD83D\uDD0D Describe ${theme.muted('Show details of a pipeline')}`, value: 'describe' },
37
- { name: `\u270E Update ${theme.muted('Update a pipeline')}`, value: 'update' },
38
- { name: `\u2753 Help ${theme.muted('Show available commands')}`, value: 'help' },
32
+ { name: `\u2728 Create ${theme.muted('Create a new observability stack')}`, value: 'create' },
33
+ { name: `\u2630 List ${theme.muted('List existing stacks')}`, value: 'list' },
34
+ { name: `\uD83D\uDD0D Describe ${theme.muted('Show details of a stack')}`, value: 'describe' },
35
+ { name: `\uD83D\uDE80 Install Demo ${theme.muted('Create demo services on EKS')}`, value: 'demo' },
39
36
  { name: `\uD83D\uDEAA Quit ${theme.muted('Exit')}`, value: 'quit' },
40
37
  ];
@@ -1,26 +1,27 @@
1
- import { printTable, printInfo, colorStatus, formatDate } from '../ui.mjs';
2
- import { loadPipelines } from './index.mjs';
1
+ import { printTable, printInfo, theme } from '../ui.mjs';
2
+ import { loadStacks } from './index.mjs';
3
3
 
4
4
  export async function runList(session) {
5
5
  console.error();
6
6
 
7
- const pipelines = await loadPipelines(session.region);
7
+ const stacks = await loadStacks(session.region);
8
8
 
9
- if (pipelines.length === 0) {
10
- printInfo('No OSI pipelines found in this region.');
9
+ if (stacks.length === 0) {
10
+ printInfo('No open-stack stacks found in this region.');
11
11
  console.error();
12
12
  return;
13
13
  }
14
14
 
15
15
  console.error();
16
- const headers = ['Name', 'Status', 'OCUs', 'Created', 'Updated'];
17
- const rows = pipelines.map((p) => [
18
- p.name,
19
- colorStatus(p.status),
20
- `${p.minUnits}\u2013${p.maxUnits}`,
21
- formatDate(p.createdAt),
22
- formatDate(p.lastUpdatedAt),
23
- ]);
16
+ const headers = ['Stack', 'Resources', 'Types'];
17
+ const rows = stacks.map((s) => {
18
+ const types = [...new Set(s.resources.map((r) => r.type))];
19
+ return [
20
+ s.name,
21
+ String(s.resources.length),
22
+ types.join(theme.muted(', ')),
23
+ ];
24
+ });
24
25
 
25
26
  printTable(headers, rows);
26
27
  }
package/src/config.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  * Default configuration values.
3
3
  */
4
4
  export const DEFAULTS = {
5
- pipelineName: 'observability-stack',
5
+ pipelineName: `open-stack-${Math.floor(Date.now() / 1000)}`,
6
6
  osInstanceType: 'r6g.large.search',
7
7
  osInstanceCount: 1,
8
8
  osVolumeSize: 100,
@@ -38,6 +38,16 @@ export function createDefaultConfig() {
38
38
  minOcu: DEFAULTS.minOcu,
39
39
  maxOcu: DEFAULTS.maxOcu,
40
40
  serviceMapWindow: DEFAULTS.serviceMapWindow,
41
+ dashboardsAction: '',
42
+ dashboardsUrl: '',
43
+ dqsRoleName: '',
44
+ dqsRoleArn: '',
45
+ dqsDataSourceName: '',
46
+ dqsDataSourceArn: '',
47
+ appName: '',
48
+ appId: '',
49
+ appEndpoint: '',
50
+ ingestEndpoints: [],
41
51
  outputFile: '',
42
52
  dryRun: false,
43
53
  accountId: '',
package/src/eks.mjs ADDED
@@ -0,0 +1,356 @@
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import {
6
+ printStep,
7
+ printSuccess,
8
+ printError,
9
+ printWarning,
10
+ printInfo,
11
+ createSpinner,
12
+ } from './ui.mjs';
13
+ import { tagResource } from './aws.mjs';
14
+ import chalk from 'chalk';
15
+
16
+ const HELM_CHART_REPO = 'https://github.com/kylehounslow/observability-stack.git';
17
+ const HELM_CHART_BRANCH = 'feat/helm-charts';
18
+ const HELM_CHART_PATH = 'charts/observability-stack';
19
+ const HELM_RELEASE_NAME = 'obs-stack';
20
+ const HELM_NAMESPACE = 'observability';
21
+
22
+ const OTEL_DEMO_REPO = 'https://open-telemetry.github.io/opentelemetry-helm-charts';
23
+ const OTEL_DEMO_RELEASE_NAME = 'otel-demo';
24
+ const OTEL_DEMO_NAMESPACE = 'otel-demo';
25
+
26
+ // ── Prerequisites ───────────────────────────────────────────────────────────
27
+
28
+ function commandExists(cmd) {
29
+ try {
30
+ execSync(`command -v ${cmd}`, { stdio: 'ignore' });
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ export function checkDemoPrerequisites() {
38
+ printStep('Checking demo prerequisites...');
39
+ console.error();
40
+
41
+ const required = [
42
+ { cmd: 'aws', install: 'https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html' },
43
+ { cmd: 'eksctl', install: 'https://eksctl.io/installation/' },
44
+ { cmd: 'kubectl', install: 'https://kubernetes.io/docs/tasks/tools/' },
45
+ { cmd: 'helm', install: 'https://helm.sh/docs/intro/install/' },
46
+ { cmd: 'git', install: 'https://git-scm.com/downloads' },
47
+ ];
48
+
49
+ const missing = [];
50
+ for (const { cmd, install } of required) {
51
+ if (commandExists(cmd)) {
52
+ printSuccess(`${cmd} found`);
53
+ } else {
54
+ missing.push({ cmd, install });
55
+ printError(`${cmd} not found`);
56
+ }
57
+ }
58
+
59
+ if (missing.length > 0) {
60
+ console.error();
61
+ console.error(` ${chalk.bold('Missing required tools:')}`);
62
+ for (const { cmd, install } of missing) {
63
+ console.error(` ${chalk.bold(cmd)}: ${chalk.underline(install)}`);
64
+ }
65
+ console.error();
66
+ process.exit(1);
67
+ }
68
+
69
+ console.error();
70
+ }
71
+
72
+ // ── EKS Cluster ─────────────────────────────────────────────────────────────
73
+
74
+ function runCommand(cmd, args, { spinner, prefix = '' } = {}) {
75
+ return new Promise((resolve, reject) => {
76
+ const proc = spawn(cmd, args, {
77
+ stdio: ['ignore', 'pipe', 'pipe'],
78
+ env: { ...process.env },
79
+ });
80
+
81
+ let stdout = '';
82
+ let stderr = '';
83
+
84
+ proc.stdout.on('data', (data) => {
85
+ stdout += data.toString();
86
+ const lines = data.toString().trim().split('\n');
87
+ for (const line of lines) {
88
+ if (spinner && line.trim()) {
89
+ spinner.text = `${prefix}${line.trim()}`;
90
+ }
91
+ }
92
+ });
93
+
94
+ proc.stderr.on('data', (data) => {
95
+ stderr += data.toString();
96
+ const lines = data.toString().trim().split('\n');
97
+ for (const line of lines) {
98
+ if (spinner && line.trim()) {
99
+ spinner.text = `${prefix}${line.trim()}`;
100
+ }
101
+ }
102
+ });
103
+
104
+ proc.on('close', (code) => {
105
+ if (code === 0) {
106
+ resolve(stdout.trim());
107
+ } else {
108
+ reject(new Error(stderr.trim() || `Command failed with exit code ${code}`));
109
+ }
110
+ });
111
+
112
+ proc.on('error', reject);
113
+ });
114
+ }
115
+
116
+ export async function createEksCluster(cfg) {
117
+ const { clusterName, region, nodeCount, instanceType, stackName, accountId } = cfg;
118
+
119
+ printStep(`Creating EKS cluster '${clusterName}'...`);
120
+ console.error();
121
+
122
+ // Check if cluster already exists
123
+ try {
124
+ const existing = execSync(
125
+ `eksctl get cluster --name ${clusterName} --region ${region} 2>/dev/null`,
126
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] },
127
+ );
128
+ if (existing.includes(clusterName)) {
129
+ printSuccess(`Cluster '${clusterName}' already exists`);
130
+ printInfo('Updating kubeconfig...');
131
+ execSync(
132
+ `aws eks update-kubeconfig --name ${clusterName} --region ${region}`,
133
+ { stdio: 'ignore' },
134
+ );
135
+ printSuccess('kubeconfig updated');
136
+ return;
137
+ }
138
+ } catch { /* cluster doesn't exist — proceed to create */ }
139
+
140
+ const spinner = createSpinner('Creating EKS cluster (15-25 min)...');
141
+ spinner.start();
142
+
143
+ try {
144
+ await runCommand('eksctl', [
145
+ 'create', 'cluster',
146
+ '--name', clusterName,
147
+ '--region', region,
148
+ '--nodes', String(nodeCount),
149
+ '--node-type', instanceType,
150
+ '--managed',
151
+ ], { spinner, prefix: 'EKS: ' });
152
+
153
+ spinner.succeed(`Cluster '${clusterName}' created`);
154
+ } catch (err) {
155
+ spinner.fail('Failed to create EKS cluster');
156
+ console.error();
157
+ console.error(` ${chalk.dim(err.message)}`);
158
+ console.error();
159
+ throw new Error('Failed to create EKS cluster');
160
+ }
161
+
162
+ // Tag the EKS cluster for stack discovery
163
+ if (stackName && accountId) {
164
+ const clusterArn = `arn:aws:eks:${region}:${accountId}:cluster/${clusterName}`;
165
+ await tagResource(region, clusterArn, stackName);
166
+ printSuccess('Cluster tagged');
167
+ }
168
+
169
+ // Update kubeconfig
170
+ try {
171
+ execSync(
172
+ `aws eks update-kubeconfig --name ${clusterName} --region ${region}`,
173
+ { stdio: 'ignore' },
174
+ );
175
+ printSuccess('kubeconfig updated');
176
+ } catch (err) {
177
+ printWarning(`Could not update kubeconfig: ${err.message}`);
178
+ printInfo(`Run: aws eks update-kubeconfig --name ${clusterName} --region ${region}`);
179
+ }
180
+ }
181
+
182
+ // ── Helm Chart Installation ─────────────────────────────────────────────────
183
+
184
+ export async function installHelmChart(cfg) {
185
+ printStep('Installing observability stack Helm chart...');
186
+ console.error();
187
+
188
+ const tmpDir = mkdtempSync(join(tmpdir(), 'obs-stack-'));
189
+ const chartDir = join(tmpDir, 'observability-stack', HELM_CHART_PATH);
190
+
191
+ try {
192
+ // Clone the chart repo
193
+ const cloneSpinner = createSpinner('Cloning Helm chart repository...');
194
+ cloneSpinner.start();
195
+
196
+ try {
197
+ await runCommand('git', [
198
+ 'clone',
199
+ '--branch', HELM_CHART_BRANCH,
200
+ '--depth', '1',
201
+ HELM_CHART_REPO,
202
+ join(tmpDir, 'observability-stack'),
203
+ ], { spinner: cloneSpinner });
204
+ cloneSpinner.succeed('Chart repository cloned');
205
+ } catch (err) {
206
+ cloneSpinner.fail('Failed to clone chart repository');
207
+ console.error(` ${chalk.dim(err.message)}`);
208
+ throw new Error('Failed to clone Helm chart repository');
209
+ }
210
+
211
+ // Patch: remove duplicate data-prepper metrics port (4900).
212
+ // The subchart template hardcodes a "server" port on 4900, so having
213
+ // "metrics: 4900" in values.yaml causes a duplicate port warning.
214
+ try {
215
+ const valuesPath = join(chartDir, 'values.yaml');
216
+ const values = readFileSync(valuesPath, 'utf-8');
217
+ writeFileSync(valuesPath, values.replace(/^\s*- name: metrics\n\s*port: 4900\n/m, ''));
218
+ } catch { /* best effort — chart may have been fixed upstream */ }
219
+
220
+ // Build dependencies
221
+ const depSpinner = createSpinner('Building Helm dependencies...');
222
+ depSpinner.start();
223
+
224
+ try {
225
+ await runCommand('helm', ['dependency', 'build', chartDir], { spinner: depSpinner });
226
+ depSpinner.succeed('Helm dependencies built');
227
+ } catch (err) {
228
+ depSpinner.fail('Failed to build Helm dependencies');
229
+ console.error(` ${chalk.dim(err.message)}`);
230
+ throw new Error('Failed to build Helm dependencies');
231
+ }
232
+
233
+ // Install chart
234
+ const installSpinner = createSpinner('Installing Helm chart (this may take a few minutes)...');
235
+ installSpinner.start();
236
+
237
+ const helmArgs = [
238
+ 'install', HELM_RELEASE_NAME, chartDir,
239
+ '--namespace', HELM_NAMESPACE,
240
+ '--create-namespace',
241
+ '--wait',
242
+ '--timeout', '20m',
243
+ ];
244
+
245
+ // If the stack config has an OTLP endpoint, pass it as a value override
246
+ if (cfg.otlpEndpoint) {
247
+ helmArgs.push('--set', `opentelemetry-collector.config.exporters.otlp/osi.endpoint=${cfg.otlpEndpoint}`);
248
+ }
249
+
250
+ try {
251
+ await runCommand('helm', helmArgs, { spinner: installSpinner });
252
+ installSpinner.succeed('Helm chart installed');
253
+ } catch (err) {
254
+ // Check if release already exists
255
+ if (/already exists/i.test(err.message) || /cannot re-use/i.test(err.message)) {
256
+ installSpinner.succeed(`Release '${HELM_RELEASE_NAME}' already installed`);
257
+ printInfo(`To upgrade: helm upgrade ${HELM_RELEASE_NAME} ${chartDir} -n ${HELM_NAMESPACE}`);
258
+ return;
259
+ }
260
+ installSpinner.fail('Failed to install Helm chart');
261
+ console.error(` ${chalk.dim(err.message)}`);
262
+ throw new Error('Failed to install Helm chart');
263
+ }
264
+
265
+ // Show deployment status
266
+ console.error();
267
+ printSuccess('Demo services deployed to EKS');
268
+ printInfo(`Namespace: ${HELM_NAMESPACE}`);
269
+ printInfo(`Release: ${HELM_RELEASE_NAME}`);
270
+ printInfo(`Check status: kubectl get pods -n ${HELM_NAMESPACE}`);
271
+ } finally {
272
+ // Clean up temp directory
273
+ try {
274
+ rmSync(tmpDir, { recursive: true, force: true });
275
+ } catch { /* best effort cleanup */ }
276
+ }
277
+ }
278
+
279
+ // ── OpenTelemetry Demo ────────────────────────────────────────────────────
280
+
281
+ export async function installOtelDemo(cfg) {
282
+ printStep('Installing OpenTelemetry Demo...');
283
+ console.error();
284
+
285
+ // Add the Helm repo
286
+ const repoSpinner = createSpinner('Adding OpenTelemetry Helm repo...');
287
+ repoSpinner.start();
288
+
289
+ try {
290
+ await runCommand('helm', [
291
+ 'repo', 'add', 'open-telemetry', OTEL_DEMO_REPO,
292
+ ], { spinner: repoSpinner });
293
+ await runCommand('helm', ['repo', 'update'], { spinner: repoSpinner });
294
+ repoSpinner.succeed('OpenTelemetry Helm repo added');
295
+ } catch (err) {
296
+ // Repo may already exist — try update anyway
297
+ try {
298
+ await runCommand('helm', ['repo', 'update'], { spinner: repoSpinner });
299
+ repoSpinner.succeed('OpenTelemetry Helm repo updated');
300
+ } catch (updateErr) {
301
+ repoSpinner.fail('Failed to add Helm repo');
302
+ console.error(` ${chalk.dim(updateErr.message)}`);
303
+ throw new Error('Failed to add OpenTelemetry Helm repo');
304
+ }
305
+ }
306
+
307
+ // Install the OpenTelemetry Demo chart
308
+ const installSpinner = createSpinner('Installing OpenTelemetry Demo (this may take a few minutes)...');
309
+ installSpinner.start();
310
+
311
+ const helmArgs = [
312
+ 'install', OTEL_DEMO_RELEASE_NAME, 'open-telemetry/opentelemetry-demo',
313
+ '--namespace', OTEL_DEMO_NAMESPACE,
314
+ '--create-namespace',
315
+ '--wait',
316
+ '--timeout', '10m',
317
+ // Disable the demo's built-in observability backends — we use our own stack
318
+ '--set', 'opensearch.enabled=false',
319
+ '--set', 'grafana.enabled=false',
320
+ '--set', 'prometheus.enabled=false',
321
+ '--set', 'jaeger.enabled=false',
322
+ ];
323
+
324
+ // Point the demo's collector at the observability stack's collector
325
+ if (cfg.otlpEndpoint) {
326
+ helmArgs.push(
327
+ '--set', `opentelemetry-collector.config.exporters.otlp/osi.endpoint=${cfg.otlpEndpoint}`,
328
+ );
329
+ } else {
330
+ // Default: send to the obs-stack collector in the observability namespace
331
+ helmArgs.push(
332
+ '--set', `default.env[0].name=OTEL_EXPORTER_OTLP_ENDPOINT`,
333
+ '--set', `default.env[0].value=http://${HELM_RELEASE_NAME}-opentelemetry-collector.${HELM_NAMESPACE}:4317`,
334
+ );
335
+ }
336
+
337
+ try {
338
+ await runCommand('helm', helmArgs, { spinner: installSpinner });
339
+ installSpinner.succeed('OpenTelemetry Demo installed');
340
+ } catch (err) {
341
+ if (/already exists/i.test(err.message) || /cannot re-use/i.test(err.message)) {
342
+ installSpinner.succeed(`Release '${OTEL_DEMO_RELEASE_NAME}' already installed`);
343
+ return;
344
+ }
345
+ installSpinner.fail('Failed to install OpenTelemetry Demo');
346
+ console.error(` ${chalk.dim(err.message)}`);
347
+ throw new Error('Failed to install OpenTelemetry Demo');
348
+ }
349
+
350
+ console.error();
351
+ printSuccess('OpenTelemetry Demo deployed');
352
+ printInfo(`Namespace: ${OTEL_DEMO_NAMESPACE}`);
353
+ printInfo(`Release: ${OTEL_DEMO_RELEASE_NAME}`);
354
+ printInfo(`Check status: kubectl get pods -n ${OTEL_DEMO_NAMESPACE}`);
355
+ printInfo(`Frontend: kubectl port-forward svc/${OTEL_DEMO_RELEASE_NAME}-frontend-proxy 8080:8080 -n ${OTEL_DEMO_NAMESPACE}`);
356
+ }