@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/interactive.mjs
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import { select, input, confirm } from '@inquirer/prompts';
|
|
2
1
|
import {
|
|
3
|
-
printHeader, printStep, printInfo, printSubStep,
|
|
4
|
-
createSpinner, theme, GoBack,
|
|
2
|
+
printHeader, printStep, printInfo, printSubStep,
|
|
3
|
+
createSpinner, theme, GoBack, eSelect, eInput,
|
|
5
4
|
} from './ui.mjs';
|
|
6
5
|
import { createDefaultConfig, DEFAULTS } from './config.mjs';
|
|
7
|
-
import { listDomains,
|
|
6
|
+
import { listDomains, listWorkspaces, listApplications } from './aws.mjs';
|
|
8
7
|
|
|
9
8
|
const CUSTOM_INPUT = Symbol('custom');
|
|
10
9
|
|
|
11
|
-
const eSelect = withEscape(select);
|
|
12
|
-
const eInput = withEscape(input);
|
|
13
|
-
const eConfirm = withEscape(confirm);
|
|
14
|
-
|
|
15
10
|
/**
|
|
16
11
|
* Fetch resources with a spinner, returning [] on failure.
|
|
17
12
|
*/
|
|
@@ -37,7 +32,7 @@ async function stepMode(cfg) {
|
|
|
37
32
|
const mode = await eSelect({
|
|
38
33
|
message: 'Mode',
|
|
39
34
|
choices: [
|
|
40
|
-
{ name: `Simple ${theme.muted('\u2014
|
|
35
|
+
{ name: `Simple ${theme.muted('\u2014 creates all resources with defaults')}`, value: 'simple' },
|
|
41
36
|
{ name: `Advanced ${theme.muted('\u2014 create new or reuse existing resources; tune pipeline settings')}`, value: 'advanced' },
|
|
42
37
|
],
|
|
43
38
|
default: cfg.mode || 'simple',
|
|
@@ -82,215 +77,331 @@ async function stepCore(cfg, session) {
|
|
|
82
77
|
cfg.iamRoleName = `${cfg.pipelineName}-osi-role`;
|
|
83
78
|
cfg.apsAction = 'create';
|
|
84
79
|
cfg.apsWorkspaceAlias = cfg.pipelineName;
|
|
80
|
+
cfg.dashboardsAction = 'create';
|
|
81
|
+
cfg.dqsRoleName = `${cfg.pipelineName}-dqs-prometheus-role`;
|
|
82
|
+
cfg.dqsDataSourceName = `${cfg.pipelineName.replace(/-/g, '_')}_prometheus`;
|
|
83
|
+
cfg.appName = cfg.pipelineName;
|
|
85
84
|
console.error();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
85
|
+
printInfo(`Will create:`);
|
|
86
|
+
printSubStep(`OpenSearch Serverless collection: ${theme.accent(cfg.osDomainName)}`);
|
|
87
|
+
printSubStep(`IAM role: ${theme.accent(cfg.iamRoleName)}`);
|
|
88
|
+
printSubStep(`APS workspace: ${theme.accent(cfg.apsWorkspaceAlias)}`);
|
|
89
|
+
printSubStep(`DQS role: ${theme.accent(cfg.dqsRoleName)}`);
|
|
90
|
+
printSubStep(`DQS data source: ${theme.accent(cfg.dqsDataSourceName)}`);
|
|
91
|
+
printSubStep(`OpenSearch Application: ${theme.accent(cfg.appName)}`);
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
async function stepOpenSearch(cfg) {
|
|
94
96
|
if (cfg.mode !== 'advanced') return 'skip';
|
|
95
97
|
|
|
96
|
-
printStep('OpenSearch
|
|
98
|
+
printStep('OpenSearch');
|
|
97
99
|
console.error();
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
101
|
+
while (true) {
|
|
102
|
+
const osChoice = await eSelect({
|
|
103
|
+
message: 'Create new or reuse existing?',
|
|
104
|
+
choices: [
|
|
105
|
+
{ name: 'Create new', value: 'create' },
|
|
106
|
+
{ name: 'Reuse existing', value: 'reuse' },
|
|
107
|
+
],
|
|
108
|
+
default: cfg.osAction || 'create',
|
|
109
|
+
});
|
|
110
|
+
if (osChoice === GoBack) return GoBack;
|
|
111
|
+
|
|
112
|
+
if (osChoice === 'reuse') {
|
|
113
|
+
cfg.osAction = 'reuse';
|
|
114
|
+
|
|
115
|
+
const domains = await fetchWithSpinner(
|
|
116
|
+
'Loading OpenSearch domains & collections',
|
|
117
|
+
() => listDomains(cfg.region),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (domains.length > 0) {
|
|
121
|
+
const choices = domains.map((d) => ({
|
|
122
|
+
name: d.endpoint
|
|
123
|
+
? `${d.name} ${theme.muted(`\u2014 ${d.endpoint} (${d.engineVersion})`)}`
|
|
124
|
+
: `${d.name} ${theme.muted(`\u2014 provisioning... (${d.engineVersion})`)}`,
|
|
125
|
+
value: { endpoint: d.endpoint, serverless: d.serverless },
|
|
126
|
+
disabled: !d.endpoint ? '(no endpoint yet)' : false,
|
|
127
|
+
}));
|
|
128
|
+
choices.push({ name: theme.accent('Enter manually...'), value: CUSTOM_INPUT });
|
|
129
|
+
|
|
130
|
+
const selected = await eSelect({ message: 'Select domain or collection', choices });
|
|
131
|
+
if (selected === GoBack) continue;
|
|
132
|
+
if (selected === CUSTOM_INPUT) {
|
|
133
|
+
const ep = await promptEndpoint();
|
|
134
|
+
if (ep === GoBack) continue;
|
|
135
|
+
cfg.opensearchEndpoint = ep;
|
|
136
|
+
cfg.serverless = isServerlessEndpoint(ep);
|
|
137
|
+
} else {
|
|
138
|
+
cfg.opensearchEndpoint = selected.endpoint;
|
|
139
|
+
cfg.serverless = selected.serverless;
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
printInfo('No domains or collections found \u2014 enter endpoint manually');
|
|
130
143
|
const ep = await promptEndpoint();
|
|
131
|
-
if (ep === GoBack)
|
|
144
|
+
if (ep === GoBack) continue;
|
|
132
145
|
cfg.opensearchEndpoint = ep;
|
|
133
146
|
cfg.serverless = isServerlessEndpoint(ep);
|
|
134
|
-
} else {
|
|
135
|
-
cfg.opensearchEndpoint = selected.endpoint;
|
|
136
|
-
cfg.serverless = selected.serverless;
|
|
137
147
|
}
|
|
148
|
+
|
|
149
|
+
printSubStep(`Detected type: ${cfg.serverless ? theme.accent('OpenSearch Serverless') : theme.accent('Managed OpenSearch domain')}`);
|
|
138
150
|
} else {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
151
|
+
cfg.osAction = 'create';
|
|
152
|
+
|
|
153
|
+
const osType = await eSelect({
|
|
154
|
+
message: 'OpenSearch type',
|
|
155
|
+
choices: [
|
|
156
|
+
{ name: `Serverless ${theme.muted('\u2014 fully managed, auto-scales')}`, value: 'serverless' },
|
|
157
|
+
{ name: `Managed domain ${theme.muted('\u2014 configure instance type, count, and storage')}`, value: 'managed' },
|
|
158
|
+
],
|
|
159
|
+
default: cfg.serverless === true ? 'serverless' : cfg.serverless === false ? 'managed' : 'serverless',
|
|
160
|
+
});
|
|
161
|
+
if (osType === GoBack) continue;
|
|
162
|
+
cfg.serverless = osType === 'serverless';
|
|
163
|
+
|
|
164
|
+
const nameMsg = cfg.serverless ? 'Collection name' : 'Domain name';
|
|
165
|
+
const domainName = await eInput({ message: nameMsg, default: cfg.osDomainName || cfg.pipelineName });
|
|
166
|
+
if (domainName === GoBack) continue;
|
|
167
|
+
cfg.osDomainName = domainName;
|
|
168
|
+
|
|
169
|
+
if (!cfg.serverless) {
|
|
170
|
+
const instType = await eInput({ message: 'Instance type', default: cfg.osInstanceType || DEFAULTS.osInstanceType });
|
|
171
|
+
if (instType === GoBack) continue;
|
|
172
|
+
cfg.osInstanceType = instType;
|
|
173
|
+
|
|
174
|
+
const instCount = await eInput({
|
|
175
|
+
message: 'Instance count',
|
|
176
|
+
default: String(cfg.osInstanceCount || DEFAULTS.osInstanceCount),
|
|
177
|
+
validate: (v) => /^\d+$/.test(v.trim()) && Number(v) >= 1 || 'Must be a positive integer',
|
|
178
|
+
});
|
|
179
|
+
if (instCount === GoBack) continue;
|
|
180
|
+
cfg.osInstanceCount = Number(instCount);
|
|
181
|
+
|
|
182
|
+
const volSize = await eInput({
|
|
183
|
+
message: 'EBS volume size (GB)',
|
|
184
|
+
default: String(cfg.osVolumeSize || DEFAULTS.osVolumeSize),
|
|
185
|
+
validate: (v) => /^\d+$/.test(v.trim()) && Number(v) >= 10 || 'Must be at least 10 GB',
|
|
186
|
+
});
|
|
187
|
+
if (volSize === GoBack) continue;
|
|
188
|
+
cfg.osVolumeSize = Number(volSize);
|
|
189
|
+
|
|
190
|
+
const engineVer = await eInput({ message: 'Engine version', default: cfg.osEngineVersion || DEFAULTS.osEngineVersion });
|
|
191
|
+
if (engineVer === GoBack) continue;
|
|
192
|
+
cfg.osEngineVersion = engineVer;
|
|
193
|
+
}
|
|
144
194
|
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
145
198
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
cfg.osAction = 'create';
|
|
149
|
-
|
|
150
|
-
const domainName = await eInput({ message: 'Domain name', default: cfg.osDomainName || cfg.pipelineName });
|
|
151
|
-
if (domainName === GoBack) return GoBack;
|
|
152
|
-
cfg.osDomainName = domainName;
|
|
153
|
-
|
|
154
|
-
const instType = await eInput({ message: 'Instance type', default: cfg.osInstanceType || DEFAULTS.osInstanceType });
|
|
155
|
-
if (instType === GoBack) return GoBack;
|
|
156
|
-
cfg.osInstanceType = instType;
|
|
199
|
+
async function stepIam(cfg) {
|
|
200
|
+
if (cfg.mode !== 'advanced') return 'skip';
|
|
157
201
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
validate: (v) => /^\d+$/.test(v.trim()) && Number(v) >= 1 || 'Must be a positive integer',
|
|
162
|
-
});
|
|
163
|
-
if (instCount === GoBack) return GoBack;
|
|
164
|
-
cfg.osInstanceCount = Number(instCount);
|
|
202
|
+
printStep('IAM role for OSI pipeline');
|
|
203
|
+
printInfo('This role allows the ingestion pipeline to write to OpenSearch and Prometheus');
|
|
204
|
+
console.error();
|
|
165
205
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
206
|
+
while (true) {
|
|
207
|
+
const iamChoice = await eSelect({
|
|
208
|
+
message: 'Create new or reuse existing?',
|
|
209
|
+
choices: [
|
|
210
|
+
{ name: 'Create new', value: 'create' },
|
|
211
|
+
{ name: 'Reuse existing', value: 'reuse' },
|
|
212
|
+
],
|
|
213
|
+
default: cfg.iamAction || 'create',
|
|
170
214
|
});
|
|
171
|
-
if (
|
|
172
|
-
cfg.osVolumeSize = Number(volSize);
|
|
215
|
+
if (iamChoice === GoBack) return GoBack;
|
|
173
216
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
217
|
+
if (iamChoice === 'reuse') {
|
|
218
|
+
cfg.iamAction = 'reuse';
|
|
219
|
+
const arn = await promptArn('IAM role ARN');
|
|
220
|
+
if (arn === GoBack) continue;
|
|
221
|
+
cfg.iamRoleArn = arn;
|
|
222
|
+
} else {
|
|
223
|
+
cfg.iamAction = 'create';
|
|
224
|
+
const roleName = await eInput({ message: 'Role name', default: cfg.iamRoleName || `${cfg.pipelineName}-osi-role` });
|
|
225
|
+
if (roleName === GoBack) continue;
|
|
226
|
+
cfg.iamRoleName = roleName;
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
177
229
|
}
|
|
178
230
|
}
|
|
179
231
|
|
|
180
|
-
async function
|
|
232
|
+
async function stepAps(cfg) {
|
|
181
233
|
if (cfg.mode !== 'advanced') return 'skip';
|
|
182
234
|
|
|
183
|
-
printStep('
|
|
235
|
+
printStep('Amazon Managed Prometheus (APS) workspace');
|
|
184
236
|
console.error();
|
|
185
237
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
238
|
+
while (true) {
|
|
239
|
+
const apsChoice = await eSelect({
|
|
240
|
+
message: 'Create new or reuse existing?',
|
|
241
|
+
choices: [
|
|
242
|
+
{ name: 'Create new', value: 'create' },
|
|
243
|
+
{ name: 'Reuse existing', value: 'reuse' },
|
|
244
|
+
],
|
|
245
|
+
default: cfg.apsAction || 'create',
|
|
246
|
+
});
|
|
247
|
+
if (apsChoice === GoBack) return GoBack;
|
|
248
|
+
|
|
249
|
+
if (apsChoice === 'reuse') {
|
|
250
|
+
cfg.apsAction = 'reuse';
|
|
251
|
+
|
|
252
|
+
const workspaces = await fetchWithSpinner(
|
|
253
|
+
'Loading APS workspaces',
|
|
254
|
+
() => listWorkspaces(cfg.region),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (workspaces.length > 0) {
|
|
258
|
+
const choices = workspaces.map((w) => ({
|
|
259
|
+
name: w.alias
|
|
260
|
+
? `${w.alias} ${theme.muted(`\u2014 ${w.id}`)}`
|
|
261
|
+
: `${w.id} ${theme.muted('(no alias)')}`,
|
|
262
|
+
value: w.url,
|
|
263
|
+
}));
|
|
264
|
+
choices.push({ name: theme.accent('Enter URL manually...'), value: CUSTOM_INPUT });
|
|
265
|
+
|
|
266
|
+
const selected = await eSelect({ message: 'Select workspace', choices });
|
|
267
|
+
if (selected === GoBack) continue;
|
|
268
|
+
if (selected === CUSTOM_INPUT) {
|
|
269
|
+
const url = await promptUrl('Prometheus remote-write URL');
|
|
270
|
+
if (url === GoBack) continue;
|
|
271
|
+
cfg.prometheusUrl = url;
|
|
272
|
+
} else {
|
|
273
|
+
cfg.prometheusUrl = selected;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
printInfo('No workspaces found \u2014 enter URL manually');
|
|
277
|
+
const url = await promptUrl('Prometheus remote-write URL');
|
|
278
|
+
if (url === GoBack) continue;
|
|
279
|
+
cfg.prometheusUrl = url;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
cfg.apsAction = 'create';
|
|
283
|
+
const alias = await eInput({ message: 'Workspace alias', default: cfg.apsWorkspaceAlias || cfg.pipelineName });
|
|
284
|
+
if (alias === GoBack) continue;
|
|
285
|
+
cfg.apsWorkspaceAlias = alias;
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
195
290
|
|
|
196
|
-
|
|
197
|
-
|
|
291
|
+
async function stepDqsRole(cfg) {
|
|
292
|
+
if (cfg.mode !== 'advanced') return 'skip';
|
|
198
293
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
);
|
|
294
|
+
printStep('Direct Query IAM role');
|
|
295
|
+
printInfo('This role allows OpenSearch to query Prometheus metrics via Direct Query Service');
|
|
296
|
+
console.error();
|
|
203
297
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (selected === CUSTOM_INPUT) {
|
|
221
|
-
const arn = await promptArn();
|
|
222
|
-
if (arn === GoBack) return GoBack;
|
|
223
|
-
cfg.iamRoleArn = arn;
|
|
224
|
-
} else {
|
|
225
|
-
cfg.iamRoleArn = selected;
|
|
226
|
-
}
|
|
298
|
+
while (true) {
|
|
299
|
+
const choice = await eSelect({
|
|
300
|
+
message: 'Create new or reuse existing?',
|
|
301
|
+
choices: [
|
|
302
|
+
{ name: 'Create new', value: 'create' },
|
|
303
|
+
{ name: 'Reuse existing', value: 'reuse' },
|
|
304
|
+
],
|
|
305
|
+
default: 'create',
|
|
306
|
+
});
|
|
307
|
+
if (choice === GoBack) return GoBack;
|
|
308
|
+
|
|
309
|
+
if (choice === 'reuse') {
|
|
310
|
+
const arn = await promptArn('DQS role ARN');
|
|
311
|
+
if (arn === GoBack) continue;
|
|
312
|
+
cfg.dqsRoleArn = arn;
|
|
313
|
+
cfg.dqsRoleName = '';
|
|
227
314
|
} else {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
315
|
+
const roleName = await eInput({
|
|
316
|
+
message: 'DQS role name',
|
|
317
|
+
default: cfg.dqsRoleName || `${cfg.pipelineName}-dqs-prometheus-role`,
|
|
318
|
+
});
|
|
319
|
+
if (roleName === GoBack) continue;
|
|
320
|
+
cfg.dqsRoleName = roleName;
|
|
232
321
|
}
|
|
233
|
-
|
|
234
|
-
cfg.iamAction = 'create';
|
|
235
|
-
const roleName = await eInput({ message: 'Role name', default: cfg.iamRoleName || `${cfg.pipelineName}-osi-role` });
|
|
236
|
-
if (roleName === GoBack) return GoBack;
|
|
237
|
-
cfg.iamRoleName = roleName;
|
|
322
|
+
return;
|
|
238
323
|
}
|
|
239
324
|
}
|
|
240
325
|
|
|
241
|
-
async function
|
|
326
|
+
async function stepDqsDataSource(cfg) {
|
|
242
327
|
if (cfg.mode !== 'advanced') return 'skip';
|
|
328
|
+
// Skip if no DQS role was configured
|
|
329
|
+
if (!cfg.dqsRoleName && !cfg.dqsRoleArn) return 'skip';
|
|
243
330
|
|
|
244
|
-
printStep('
|
|
331
|
+
printStep('Direct Query data source');
|
|
332
|
+
printInfo('Connects OpenSearch to Prometheus so you can query metrics from OpenSearch UI');
|
|
245
333
|
console.error();
|
|
246
334
|
|
|
247
|
-
const
|
|
248
|
-
message: '
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
{ name: 'Reuse existing', value: 'reuse' },
|
|
252
|
-
],
|
|
253
|
-
default: cfg.apsAction || 'create',
|
|
335
|
+
const dsName = await eInput({
|
|
336
|
+
message: 'Data source name',
|
|
337
|
+
default: cfg.dqsDataSourceName || `${cfg.pipelineName.replace(/-/g, '_')}_prometheus`,
|
|
338
|
+
validate: (v) => /^[a-z][a-z0-9_]+$/.test(v.trim()) || 'Must match [a-z][a-z0-9_]+ (lowercase, underscores only)',
|
|
254
339
|
});
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
value:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
340
|
+
if (dsName === GoBack) return GoBack;
|
|
341
|
+
cfg.dqsDataSourceName = dsName;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function stepApp(cfg) {
|
|
345
|
+
if (cfg.mode !== 'advanced') return 'skip';
|
|
346
|
+
|
|
347
|
+
printStep('OpenSearch UI');
|
|
348
|
+
printInfo('The OpenSearch Application provides a unified dashboard for your observability data');
|
|
349
|
+
console.error();
|
|
350
|
+
|
|
351
|
+
while (true) {
|
|
352
|
+
const choice = await eSelect({
|
|
353
|
+
message: 'Create new or reuse existing?',
|
|
354
|
+
choices: [
|
|
355
|
+
{ name: `Create new ${theme.muted('\u2014 creates an OpenSearch Application with data sources')}`, value: 'create' },
|
|
356
|
+
{ name: 'Reuse existing', value: 'reuse' },
|
|
357
|
+
],
|
|
358
|
+
default: cfg.dashboardsAction || 'create',
|
|
359
|
+
});
|
|
360
|
+
if (choice === GoBack) return GoBack;
|
|
361
|
+
|
|
362
|
+
if (choice === 'reuse') {
|
|
363
|
+
cfg.dashboardsAction = 'reuse';
|
|
364
|
+
|
|
365
|
+
const apps = await fetchWithSpinner(
|
|
366
|
+
'Loading OpenSearch Applications',
|
|
367
|
+
() => listApplications(cfg.region),
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (apps.length > 0) {
|
|
371
|
+
const choices = apps.map((a) => ({
|
|
372
|
+
name: a.endpoint
|
|
373
|
+
? `${a.name} ${theme.muted(`\u2014 ${a.endpoint}`)}`
|
|
374
|
+
: `${a.name} ${theme.muted(`(${a.id})`)}`,
|
|
375
|
+
value: a.endpoint || a.id,
|
|
376
|
+
}));
|
|
377
|
+
choices.push({ name: theme.accent('Enter URL manually...'), value: CUSTOM_INPUT });
|
|
378
|
+
|
|
379
|
+
const selected = await eSelect({ message: 'Select application', choices });
|
|
380
|
+
if (selected === GoBack) continue;
|
|
381
|
+
if (selected === CUSTOM_INPUT) {
|
|
382
|
+
const url = await promptUrl('OpenSearch UI URL');
|
|
383
|
+
if (url === GoBack) continue;
|
|
384
|
+
cfg.dashboardsUrl = url;
|
|
385
|
+
} else {
|
|
386
|
+
cfg.dashboardsUrl = selected;
|
|
387
|
+
}
|
|
280
388
|
} else {
|
|
281
|
-
|
|
389
|
+
printInfo('No applications found \u2014 enter URL manually');
|
|
390
|
+
const url = await promptUrl('OpenSearch UI URL');
|
|
391
|
+
if (url === GoBack) continue;
|
|
392
|
+
cfg.dashboardsUrl = url;
|
|
282
393
|
}
|
|
394
|
+
cfg.appName = '';
|
|
283
395
|
} else {
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
396
|
+
cfg.dashboardsAction = 'create';
|
|
397
|
+
const appName = await eInput({
|
|
398
|
+
message: 'Application name',
|
|
399
|
+
default: cfg.appName || cfg.pipelineName,
|
|
400
|
+
});
|
|
401
|
+
if (appName === GoBack) continue;
|
|
402
|
+
cfg.appName = appName;
|
|
288
403
|
}
|
|
289
|
-
|
|
290
|
-
cfg.apsAction = 'create';
|
|
291
|
-
const alias = await eInput({ message: 'Workspace alias', default: cfg.apsWorkspaceAlias || cfg.pipelineName });
|
|
292
|
-
if (alias === GoBack) return GoBack;
|
|
293
|
-
cfg.apsWorkspaceAlias = alias;
|
|
404
|
+
return;
|
|
294
405
|
}
|
|
295
406
|
}
|
|
296
407
|
|
|
@@ -321,22 +432,6 @@ async function stepTuning(cfg) {
|
|
|
321
432
|
cfg.serviceMapWindow = window;
|
|
322
433
|
}
|
|
323
434
|
|
|
324
|
-
async function stepOutput(cfg) {
|
|
325
|
-
printStep('Output');
|
|
326
|
-
console.error();
|
|
327
|
-
|
|
328
|
-
const outputFile = await eInput({ message: 'Output file for pipeline YAML (leave empty for stdout)', default: cfg.outputFile || '' });
|
|
329
|
-
if (outputFile === GoBack) return GoBack;
|
|
330
|
-
cfg.outputFile = outputFile;
|
|
331
|
-
|
|
332
|
-
const dryRun = await eConfirm({
|
|
333
|
-
message: 'Dry run only (generate config, skip resource creation)?',
|
|
334
|
-
default: cfg.dryRun ?? true,
|
|
335
|
-
});
|
|
336
|
-
if (dryRun === GoBack) return GoBack;
|
|
337
|
-
cfg.dryRun = dryRun;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
435
|
// ── Main wizard ──────────────────────────────────────────────────────────────
|
|
341
436
|
|
|
342
437
|
/**
|
|
@@ -348,10 +443,8 @@ export async function runCreateWizard(session = null) {
|
|
|
348
443
|
const cfg = createDefaultConfig();
|
|
349
444
|
|
|
350
445
|
if (!session) printHeader();
|
|
351
|
-
printKeyHint([['Esc', 'back'], ['Ctrl+C', 'cancel']]);
|
|
352
|
-
console.error();
|
|
353
446
|
|
|
354
|
-
const steps = [stepMode, stepCore, stepOpenSearch, stepIam, stepAps,
|
|
447
|
+
const steps = [stepMode, stepCore, stepOpenSearch, stepIam, stepAps, stepDqsRole, stepDqsDataSource, stepApp, stepTuning];
|
|
355
448
|
const visited = [];
|
|
356
449
|
let i = 0;
|
|
357
450
|
|
|
@@ -360,8 +453,8 @@ export async function runCreateWizard(session = null) {
|
|
|
360
453
|
|
|
361
454
|
if (result === GoBack) {
|
|
362
455
|
if (visited.length === 0) {
|
|
363
|
-
// Escape at first step →
|
|
364
|
-
|
|
456
|
+
// Escape at first step → return to menu
|
|
457
|
+
return GoBack;
|
|
365
458
|
}
|
|
366
459
|
i = visited.pop();
|
|
367
460
|
} else if (result === 'skip') {
|
|
@@ -372,7 +465,6 @@ export async function runCreateWizard(session = null) {
|
|
|
372
465
|
}
|
|
373
466
|
}
|
|
374
467
|
|
|
375
|
-
console.error();
|
|
376
468
|
return cfg;
|
|
377
469
|
}
|
|
378
470
|
|
|
@@ -389,9 +481,9 @@ function promptEndpoint() {
|
|
|
389
481
|
});
|
|
390
482
|
}
|
|
391
483
|
|
|
392
|
-
function promptArn() {
|
|
484
|
+
function promptArn(message) {
|
|
393
485
|
return eInput({
|
|
394
|
-
message
|
|
486
|
+
message,
|
|
395
487
|
validate: (v) => {
|
|
396
488
|
if (!v.trim()) return 'ARN is required';
|
|
397
489
|
if (!v.startsWith('arn:aws:iam:')) return 'Must start with arn:aws:iam:';
|