@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/aws.mjs
CHANGED
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
DescribeDomainCommand,
|
|
6
6
|
ListDomainNamesCommand,
|
|
7
7
|
DescribeDomainsCommand,
|
|
8
|
+
AddDirectQueryDataSourceCommand,
|
|
9
|
+
GetDirectQueryDataSourceCommand,
|
|
10
|
+
CreateApplicationCommand,
|
|
11
|
+
GetApplicationCommand,
|
|
12
|
+
UpdateApplicationCommand,
|
|
13
|
+
ListApplicationsCommand,
|
|
8
14
|
} from '@aws-sdk/client-opensearch';
|
|
9
15
|
import {
|
|
10
16
|
OpenSearchServerlessClient,
|
|
@@ -25,14 +31,23 @@ import {
|
|
|
25
31
|
AmpClient,
|
|
26
32
|
ListWorkspacesCommand,
|
|
27
33
|
CreateWorkspaceCommand,
|
|
34
|
+
DescribeWorkspaceCommand,
|
|
28
35
|
} from '@aws-sdk/client-amp';
|
|
29
36
|
import {
|
|
30
37
|
OSISClient,
|
|
31
38
|
ListPipelinesCommand,
|
|
32
39
|
CreatePipelineCommand,
|
|
33
40
|
GetPipelineCommand,
|
|
34
|
-
UpdatePipelineCommand,
|
|
35
41
|
} from '@aws-sdk/client-osis';
|
|
42
|
+
import {
|
|
43
|
+
EKSClient,
|
|
44
|
+
DescribeClusterCommand,
|
|
45
|
+
} from '@aws-sdk/client-eks';
|
|
46
|
+
import {
|
|
47
|
+
ResourceGroupsTaggingAPIClient,
|
|
48
|
+
GetResourcesCommand,
|
|
49
|
+
TagResourcesCommand,
|
|
50
|
+
} from '@aws-sdk/client-resource-groups-tagging-api';
|
|
36
51
|
import {
|
|
37
52
|
printStep,
|
|
38
53
|
printSuccess,
|
|
@@ -43,6 +58,14 @@ import {
|
|
|
43
58
|
} from './ui.mjs';
|
|
44
59
|
import chalk from 'chalk';
|
|
45
60
|
|
|
61
|
+
// ── Tagging ─────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
const TAG_KEY = 'open-stack';
|
|
64
|
+
|
|
65
|
+
function stackTags(stackName) {
|
|
66
|
+
return [{ Key: TAG_KEY, Value: stackName }];
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
// ── Prerequisites ───────────────────────────────────────────────────────────
|
|
47
70
|
|
|
48
71
|
export async function checkRequirements(cfg) {
|
|
@@ -71,6 +94,8 @@ export async function checkRequirements(cfg) {
|
|
|
71
94
|
console.error(` ${chalk.bold('export AWS_ACCESS_KEY_ID=<key>')}`);
|
|
72
95
|
console.error(` ${chalk.bold('export AWS_SECRET_ACCESS_KEY=<secret>')}`);
|
|
73
96
|
console.error(` ${chalk.bold('export AWS_SESSION_TOKEN=<token>')} ${chalk.dim('(if using temporary creds)')}`);
|
|
97
|
+
console.error();
|
|
98
|
+
console.error(` ${chalk.dim('Docs:')} ${chalk.underline('https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html#getting-started-quickstart-new-command')}`);
|
|
74
99
|
} else if (/expired|ExpiredToken/i.test(err.message)) {
|
|
75
100
|
console.error(` ${chalk.bold('Your session has expired. Refresh credentials:')}`);
|
|
76
101
|
console.error();
|
|
@@ -84,6 +109,8 @@ export async function checkRequirements(cfg) {
|
|
|
84
109
|
console.error(` ${chalk.bold('Try:')}`);
|
|
85
110
|
console.error(` ${chalk.bold('aws configure')} ${chalk.dim('— set up credentials')}`);
|
|
86
111
|
console.error(` ${chalk.bold('aws sso login')} ${chalk.dim('— refresh SSO session')}`);
|
|
112
|
+
console.error();
|
|
113
|
+
console.error(` ${chalk.dim('Docs:')} ${chalk.underline('https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html#getting-started-quickstart-new-command')}`);
|
|
87
114
|
}
|
|
88
115
|
console.error();
|
|
89
116
|
throw new Error('AWS credentials are not configured or have expired');
|
|
@@ -145,6 +172,7 @@ async function createServerlessCollection(cfg) {
|
|
|
145
172
|
await client.send(new CreateCollectionCommand({
|
|
146
173
|
name: collectionName,
|
|
147
174
|
type: 'TIMESERIES',
|
|
175
|
+
tags: stackTags(cfg.pipelineName).map((t) => ({ key: t.Key, value: t.Value })),
|
|
148
176
|
}));
|
|
149
177
|
printSuccess('Collection creation initiated — waiting for endpoint');
|
|
150
178
|
} catch (err) {
|
|
@@ -322,6 +350,7 @@ async function createManagedDomain(cfg) {
|
|
|
322
350
|
},
|
|
323
351
|
},
|
|
324
352
|
AccessPolicies: accessPolicy,
|
|
353
|
+
TagList: stackTags(cfg.pipelineName),
|
|
325
354
|
}));
|
|
326
355
|
printSuccess('Domain creation initiated — waiting for endpoint');
|
|
327
356
|
} catch (createErr) {
|
|
@@ -397,11 +426,11 @@ export async function mapOsiRoleInDomain(cfg) {
|
|
|
397
426
|
} else {
|
|
398
427
|
const body = await resp.text();
|
|
399
428
|
printWarning(`FGAC mapping returned ${resp.status}: ${body}`);
|
|
400
|
-
printInfo('You may need to manually map the IAM role in OpenSearch
|
|
429
|
+
printInfo('You may need to manually map the IAM role in OpenSearch UI → Security → Roles');
|
|
401
430
|
}
|
|
402
431
|
} catch (err) {
|
|
403
432
|
printWarning(`Could not map OSI role in FGAC: ${err.message}`);
|
|
404
|
-
printInfo('You may need to manually map the IAM role in OpenSearch
|
|
433
|
+
printInfo('You may need to manually map the IAM role in OpenSearch UI → Security → Roles');
|
|
405
434
|
}
|
|
406
435
|
}
|
|
407
436
|
|
|
@@ -436,6 +465,7 @@ export async function createIamRole(cfg) {
|
|
|
436
465
|
const result = await client.send(new CreateRoleCommand({
|
|
437
466
|
RoleName: cfg.iamRoleName,
|
|
438
467
|
AssumeRolePolicyDocument: trustPolicy,
|
|
468
|
+
Tags: stackTags(cfg.pipelineName),
|
|
439
469
|
}));
|
|
440
470
|
cfg.iamRoleArn = result.Role.Arn;
|
|
441
471
|
printSuccess(`Role created: ${cfg.iamRoleArn}`);
|
|
@@ -518,7 +548,10 @@ export async function createApsWorkspace(cfg) {
|
|
|
518
548
|
} catch { /* proceed to create */ }
|
|
519
549
|
|
|
520
550
|
try {
|
|
521
|
-
const result = await client.send(new CreateWorkspaceCommand({
|
|
551
|
+
const result = await client.send(new CreateWorkspaceCommand({
|
|
552
|
+
alias: cfg.apsWorkspaceAlias,
|
|
553
|
+
tags: { [TAG_KEY]: cfg.pipelineName },
|
|
554
|
+
}));
|
|
522
555
|
cfg.apsWorkspaceId = result.workspaceId;
|
|
523
556
|
cfg.prometheusUrl = `https://aps-workspaces.${cfg.region}.amazonaws.com/workspaces/${cfg.apsWorkspaceId}/api/v1/remote_write`;
|
|
524
557
|
|
|
@@ -572,6 +605,8 @@ export async function createOsiPipeline(cfg, pipelineYaml) {
|
|
|
572
605
|
MinUnits: cfg.minOcu,
|
|
573
606
|
MaxUnits: cfg.maxOcu,
|
|
574
607
|
PipelineConfigurationBody: pipelineYaml,
|
|
608
|
+
PipelineRoleArn: cfg.iamRoleArn,
|
|
609
|
+
Tags: stackTags(cfg.pipelineName),
|
|
575
610
|
}));
|
|
576
611
|
printSuccess(`Pipeline '${cfg.pipelineName}' creation initiated`);
|
|
577
612
|
} catch (err) {
|
|
@@ -590,7 +625,7 @@ export async function createOsiPipeline(cfg, pipelineYaml) {
|
|
|
590
625
|
// Wait for pipeline to become active
|
|
591
626
|
const spinner = createSpinner('Waiting for pipeline to activate...');
|
|
592
627
|
spinner.start();
|
|
593
|
-
const maxWait =
|
|
628
|
+
const maxWait = 900_000; // 15 min
|
|
594
629
|
const start = Date.now();
|
|
595
630
|
|
|
596
631
|
while (Date.now() - start < maxWait) {
|
|
@@ -598,10 +633,11 @@ export async function createOsiPipeline(cfg, pipelineYaml) {
|
|
|
598
633
|
const resp = await client.send(new GetPipelineCommand({ PipelineName: cfg.pipelineName }));
|
|
599
634
|
const status = resp.Pipeline?.Status;
|
|
600
635
|
if (status === 'ACTIVE') {
|
|
601
|
-
const urls = resp.Pipeline?.IngestEndpointUrls;
|
|
636
|
+
const urls = resp.Pipeline?.IngestEndpointUrls || [];
|
|
637
|
+
cfg.ingestEndpoints = urls;
|
|
602
638
|
spinner.succeed('Pipeline is active');
|
|
603
|
-
|
|
604
|
-
printInfo(`Ingestion endpoint:
|
|
639
|
+
for (const url of urls) {
|
|
640
|
+
printInfo(`Ingestion endpoint: https://${url}`);
|
|
605
641
|
}
|
|
606
642
|
return;
|
|
607
643
|
}
|
|
@@ -611,13 +647,360 @@ export async function createOsiPipeline(cfg, pipelineYaml) {
|
|
|
611
647
|
printInfo(`Reason: ${reason}`);
|
|
612
648
|
throw new Error(`Pipeline creation failed: ${reason}`);
|
|
613
649
|
}
|
|
614
|
-
} catch {
|
|
650
|
+
} catch (err) {
|
|
651
|
+
if (err.message?.startsWith('Pipeline creation failed')) throw err;
|
|
652
|
+
/* keep polling */
|
|
653
|
+
}
|
|
615
654
|
await sleepWithTicker(10_000, spinner, start,
|
|
616
655
|
(s) => `Waiting for pipeline... (${fmtElapsed(s)})`);
|
|
617
656
|
}
|
|
618
657
|
|
|
619
|
-
spinner.
|
|
620
|
-
|
|
658
|
+
spinner.fail('Timed out waiting for pipeline after 15 minutes');
|
|
659
|
+
throw new Error(`Pipeline '${cfg.pipelineName}' did not become active within 15 minutes`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ── OpenSearch UI workspace ──────────────────────────────────────────
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Set up OpenSearch UI: derive the URL and create an Observability workspace.
|
|
666
|
+
* Skipped when dashboardsAction is 'reuse' (user provided their own URL).
|
|
667
|
+
* For managed domains, uses basic auth to call the Dashboards API.
|
|
668
|
+
* For serverless, provides the URL only (workspace setup via AWS console).
|
|
669
|
+
*/
|
|
670
|
+
export async function setupDashboards(cfg) {
|
|
671
|
+
if (!cfg.opensearchEndpoint) return;
|
|
672
|
+
if (cfg.dashboardsAction === 'reuse') {
|
|
673
|
+
printStep('OpenSearch UI');
|
|
674
|
+
printSuccess(`Using existing Dashboards: ${cfg.dashboardsUrl}`);
|
|
675
|
+
if (!cfg.serverless) {
|
|
676
|
+
await createObservabilityWorkspace(cfg);
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
printStep('Setting up OpenSearch UI...');
|
|
682
|
+
|
|
683
|
+
// Use OpenSearch Application URL
|
|
684
|
+
if (!cfg.appEndpoint && cfg.appId) {
|
|
685
|
+
const client = new OpenSearchClient({ region: cfg.region });
|
|
686
|
+
await fetchAppEndpoint(client, cfg);
|
|
687
|
+
}
|
|
688
|
+
cfg.dashboardsUrl = cfg.appEndpoint || '';
|
|
689
|
+
if (!cfg.dashboardsUrl) {
|
|
690
|
+
printWarning('No OpenSearch Application endpoint available');
|
|
691
|
+
printInfo('Create an OpenSearch Application in the AWS console to get the UI URL');
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
printSuccess(`URL: ${cfg.dashboardsUrl}`);
|
|
695
|
+
|
|
696
|
+
// Create observability workspace (managed domains only — uses basic auth)
|
|
697
|
+
if (!cfg.serverless) {
|
|
698
|
+
await createObservabilityWorkspace(cfg);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function createObservabilityWorkspace(cfg) {
|
|
703
|
+
if (!cfg.dashboardsUrl) return;
|
|
704
|
+
const dashboardsBase = cfg.dashboardsUrl.replace(/\/+$/, '');
|
|
705
|
+
const url = `${dashboardsBase}/api/workspaces`;
|
|
706
|
+
const auth = Buffer.from(`${MANAGED_MASTER_USER}:${MANAGED_MASTER_PASS}`).toString('base64');
|
|
707
|
+
|
|
708
|
+
// Check if an observability workspace already exists
|
|
709
|
+
try {
|
|
710
|
+
const listResp = await fetch(url, {
|
|
711
|
+
method: 'GET',
|
|
712
|
+
headers: {
|
|
713
|
+
'osd-xsrf': 'true',
|
|
714
|
+
'Authorization': `Basic ${auth}`,
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
if (listResp.ok) {
|
|
719
|
+
const data = await listResp.json();
|
|
720
|
+
const existing = (data.result?.workspaces || []).find(
|
|
721
|
+
(w) => w.name === 'Observability'
|
|
722
|
+
);
|
|
723
|
+
if (existing) {
|
|
724
|
+
printSuccess(`Observability workspace already exists (id: ${existing.id})`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} catch { /* proceed to create */ }
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
const resp = await fetch(url, {
|
|
732
|
+
method: 'POST',
|
|
733
|
+
headers: {
|
|
734
|
+
'Content-Type': 'application/json',
|
|
735
|
+
'osd-xsrf': 'true',
|
|
736
|
+
'Authorization': `Basic ${auth}`,
|
|
737
|
+
},
|
|
738
|
+
body: JSON.stringify({
|
|
739
|
+
attributes: {
|
|
740
|
+
name: 'Observability',
|
|
741
|
+
description: 'Observability workspace for traces, logs, and metrics',
|
|
742
|
+
features: ['use-case-observability'],
|
|
743
|
+
},
|
|
744
|
+
}),
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
if (resp.ok) {
|
|
748
|
+
const data = await resp.json();
|
|
749
|
+
const workspaceId = data.result?.id;
|
|
750
|
+
if (workspaceId) {
|
|
751
|
+
printSuccess(`Observability workspace created (id: ${workspaceId})`);
|
|
752
|
+
} else {
|
|
753
|
+
printSuccess('Observability workspace created');
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
const body = await resp.text();
|
|
757
|
+
printWarning(`Could not create Observability workspace (${resp.status}): ${body}`);
|
|
758
|
+
printInfo('You can create one manually in OpenSearch UI → Workspaces');
|
|
759
|
+
}
|
|
760
|
+
} catch (err) {
|
|
761
|
+
printWarning(`Could not create Observability workspace: ${err.message}`);
|
|
762
|
+
printInfo('You can create one manually in OpenSearch UI → Workspaces');
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ── Direct Query Data Source (AMP → OpenSearch) ─────────────────────────────
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Create an IAM role for the Direct Query Service to access AMP.
|
|
770
|
+
* Trust policy allows directquery.opensearchservice.amazonaws.com to assume it.
|
|
771
|
+
*/
|
|
772
|
+
export async function createDqsPrometheusRole(cfg) {
|
|
773
|
+
const roleName = cfg.dqsRoleName;
|
|
774
|
+
printStep(`Creating DQS Prometheus role '${roleName}'...`);
|
|
775
|
+
|
|
776
|
+
const client = new IAMClient({ region: cfg.region });
|
|
777
|
+
|
|
778
|
+
// Check if role already exists
|
|
779
|
+
try {
|
|
780
|
+
const existing = await client.send(new GetRoleCommand({ RoleName: roleName }));
|
|
781
|
+
cfg.dqsRoleArn = existing.Role.Arn;
|
|
782
|
+
printSuccess(`DQS role already exists: ${cfg.dqsRoleArn}`);
|
|
783
|
+
return;
|
|
784
|
+
} catch (err) {
|
|
785
|
+
if (err.name !== 'NoSuchEntityException') throw err;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const trustPolicy = JSON.stringify({
|
|
789
|
+
Version: '2012-10-17',
|
|
790
|
+
Statement: [{
|
|
791
|
+
Effect: 'Allow',
|
|
792
|
+
Principal: { Service: 'directquery.opensearchservice.amazonaws.com' },
|
|
793
|
+
Action: 'sts:AssumeRole',
|
|
794
|
+
}],
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
const result = await client.send(new CreateRoleCommand({
|
|
799
|
+
RoleName: roleName,
|
|
800
|
+
AssumeRolePolicyDocument: trustPolicy,
|
|
801
|
+
Tags: stackTags(cfg.pipelineName),
|
|
802
|
+
}));
|
|
803
|
+
cfg.dqsRoleArn = result.Role.Arn;
|
|
804
|
+
printSuccess(`DQS role created: ${cfg.dqsRoleArn}`);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
printError('Failed to create DQS Prometheus role');
|
|
807
|
+
console.error(` ${chalk.dim(err.message)}`);
|
|
808
|
+
console.error();
|
|
809
|
+
throw new Error('Failed to create DQS Prometheus role');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Attach APS access policy
|
|
813
|
+
const apsWorkspaceArn = `arn:aws:aps:${cfg.region}:${cfg.accountId}:workspace/${cfg.apsWorkspaceId}`;
|
|
814
|
+
const permissionsPolicy = JSON.stringify({
|
|
815
|
+
Version: '2012-10-17',
|
|
816
|
+
Statement: [{ Effect: 'Allow', Action: 'aps:*', Resource: apsWorkspaceArn }],
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
await client.send(new PutRolePolicyCommand({
|
|
821
|
+
RoleName: roleName,
|
|
822
|
+
PolicyName: 'APSAccess',
|
|
823
|
+
PolicyDocument: permissionsPolicy,
|
|
824
|
+
}));
|
|
825
|
+
printSuccess('APS access policy attached to DQS role');
|
|
826
|
+
} catch (err) {
|
|
827
|
+
printError('Failed to attach APS policy to DQS role');
|
|
828
|
+
console.error(` ${chalk.dim(err.message)}`);
|
|
829
|
+
console.error();
|
|
830
|
+
throw new Error('Failed to attach APS policy to DQS role');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
await sleep(5000);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Create a Direct Query Data Source connecting OpenSearch to AMP (Prometheus).
|
|
838
|
+
* Uses the OpenSearch service control plane API.
|
|
839
|
+
*/
|
|
840
|
+
export async function createDirectQueryDataSource(cfg) {
|
|
841
|
+
const dataSourceName = cfg.dqsDataSourceName;
|
|
842
|
+
printStep(`Creating Direct Query data source '${dataSourceName}'...`);
|
|
843
|
+
|
|
844
|
+
const client = new OpenSearchClient({ region: cfg.region });
|
|
845
|
+
const workspaceArn = `arn:aws:aps:${cfg.region}:${cfg.accountId}:workspace/${cfg.apsWorkspaceId}`;
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
const result = await client.send(new AddDirectQueryDataSourceCommand({
|
|
849
|
+
DataSourceName: dataSourceName,
|
|
850
|
+
DataSourceType: {
|
|
851
|
+
Prometheus: {
|
|
852
|
+
RoleArn: cfg.dqsRoleArn,
|
|
853
|
+
WorkspaceArn: workspaceArn,
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
Description: `Prometheus data source for ${cfg.pipelineName} observability stack`,
|
|
857
|
+
}));
|
|
858
|
+
cfg.dqsDataSourceArn = result.DataSourceArn;
|
|
859
|
+
printSuccess(`Direct Query data source created: ${cfg.dqsDataSourceArn}`);
|
|
860
|
+
await tagResource(cfg.region, cfg.dqsDataSourceArn, cfg.pipelineName);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
// Treat "already exists" as success
|
|
863
|
+
if (/already exists/i.test(err.message) || err.name === 'ResourceAlreadyExistsException') {
|
|
864
|
+
cfg.dqsDataSourceArn = `arn:aws:opensearch:${cfg.region}:${cfg.accountId}:datasource/${dataSourceName}`;
|
|
865
|
+
printSuccess(`Data source '${dataSourceName}' already exists`);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
printError('Failed to create Direct Query data source');
|
|
869
|
+
console.error(` ${chalk.dim(err.message)}`);
|
|
870
|
+
console.error();
|
|
871
|
+
throw new Error('Failed to create Direct Query data source');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Create an OpenSearch Application (the new OpenSearch UI) and associate
|
|
877
|
+
* the OpenSearch domain/collection and the DQS data source with it.
|
|
878
|
+
*/
|
|
879
|
+
export async function createOpenSearchApplication(cfg) {
|
|
880
|
+
const appName = cfg.appName;
|
|
881
|
+
printStep(`Creating OpenSearch Application '${appName}'...`);
|
|
882
|
+
|
|
883
|
+
const client = new OpenSearchClient({ region: cfg.region });
|
|
884
|
+
|
|
885
|
+
// Check if app already exists
|
|
886
|
+
try {
|
|
887
|
+
const list = await client.send(new ListApplicationsCommand({}));
|
|
888
|
+
const existing = (list.ApplicationSummaries || []).find((a) => a.name === appName);
|
|
889
|
+
if (existing) {
|
|
890
|
+
cfg.appId = existing.id;
|
|
891
|
+
printSuccess(`Application '${appName}' already exists (id: ${cfg.appId})`);
|
|
892
|
+
await fetchAppEndpoint(client, cfg);
|
|
893
|
+
// Update data sources on existing app
|
|
894
|
+
await associateDataSourcesWithApp(cfg, client);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
} catch { /* proceed to create */ }
|
|
898
|
+
|
|
899
|
+
// Build data sources list
|
|
900
|
+
const dataSources = buildAppDataSources(cfg);
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
const result = await client.send(new CreateApplicationCommand({
|
|
904
|
+
name: appName,
|
|
905
|
+
dataSources,
|
|
906
|
+
appConfigs: [
|
|
907
|
+
{
|
|
908
|
+
key: 'opensearchDashboards.dashboardAdmin.users',
|
|
909
|
+
value: JSON.stringify(['*']),
|
|
910
|
+
},
|
|
911
|
+
],
|
|
912
|
+
iamIdentityCenterOptions: {
|
|
913
|
+
enabled: false,
|
|
914
|
+
},
|
|
915
|
+
}));
|
|
916
|
+
cfg.appId = result.id;
|
|
917
|
+
printSuccess(`Application created: ${cfg.appId}`);
|
|
918
|
+
if (result.arn) {
|
|
919
|
+
await tagResource(cfg.region, result.arn, cfg.pipelineName);
|
|
920
|
+
}
|
|
921
|
+
await fetchAppEndpoint(client, cfg);
|
|
922
|
+
} catch (err) {
|
|
923
|
+
if (/already exists/i.test(err.message) || err.name === 'ResourceAlreadyExistsException' || err.name === 'ConflictException') {
|
|
924
|
+
printSuccess(`Application '${appName}' already exists`);
|
|
925
|
+
// Try to find and update it
|
|
926
|
+
try {
|
|
927
|
+
const list = await client.send(new ListApplicationsCommand({}));
|
|
928
|
+
const existing = (list.ApplicationSummaries || []).find((a) => a.name === appName);
|
|
929
|
+
if (existing) {
|
|
930
|
+
cfg.appId = existing.id;
|
|
931
|
+
await fetchAppEndpoint(client, cfg);
|
|
932
|
+
await associateDataSourcesWithApp(cfg, client);
|
|
933
|
+
}
|
|
934
|
+
} catch { /* best effort */ }
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
printWarning(`Could not create OpenSearch Application: ${err.message}`);
|
|
938
|
+
printInfo('You can create one manually in the AWS console');
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Fetch the application endpoint via GetApplicationCommand.
|
|
944
|
+
*/
|
|
945
|
+
async function fetchAppEndpoint(client, cfg) {
|
|
946
|
+
if (!cfg.appId) return;
|
|
947
|
+
try {
|
|
948
|
+
const resp = await client.send(new GetApplicationCommand({ id: cfg.appId }));
|
|
949
|
+
cfg.appEndpoint = resp.endpoint || '';
|
|
950
|
+
// endpoint logged by setupDashboards
|
|
951
|
+
} catch { /* best effort */ }
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Build the data sources list for application create/update.
|
|
956
|
+
*/
|
|
957
|
+
function buildAppDataSources(cfg) {
|
|
958
|
+
const dataSources = [];
|
|
959
|
+
if (cfg.serverless) {
|
|
960
|
+
const collectionId = extractServerlessCollectionId(cfg.opensearchEndpoint);
|
|
961
|
+
if (collectionId) {
|
|
962
|
+
dataSources.push({
|
|
963
|
+
dataSourceArn: `arn:aws:aoss:${cfg.region}:${cfg.accountId}:collection/${collectionId}`,
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
} else if (cfg.osDomainName) {
|
|
967
|
+
dataSources.push({
|
|
968
|
+
dataSourceArn: `arn:aws:es:${cfg.region}:${cfg.accountId}:domain/${cfg.osDomainName}`,
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
if (cfg.dqsDataSourceArn) {
|
|
972
|
+
dataSources.push({ dataSourceArn: cfg.dqsDataSourceArn });
|
|
973
|
+
}
|
|
974
|
+
return dataSources;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Associate the OpenSearch domain and DQS data source with the application.
|
|
979
|
+
*/
|
|
980
|
+
async function associateDataSourcesWithApp(cfg, client) {
|
|
981
|
+
if (!cfg.appId) return;
|
|
982
|
+
|
|
983
|
+
const dataSources = buildAppDataSources(cfg);
|
|
984
|
+
if (dataSources.length === 0) return;
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
await client.send(new UpdateApplicationCommand({
|
|
988
|
+
id: cfg.appId,
|
|
989
|
+
dataSources,
|
|
990
|
+
}));
|
|
991
|
+
printSuccess('Data sources associated with application');
|
|
992
|
+
} catch (err) {
|
|
993
|
+
printWarning(`Could not associate data sources: ${err.message}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Extract serverless collection ID from endpoint URL.
|
|
999
|
+
* Endpoint format: https://<id>.<region>.aoss.amazonaws.com
|
|
1000
|
+
*/
|
|
1001
|
+
function extractServerlessCollectionId(endpoint) {
|
|
1002
|
+
const match = endpoint?.match(/https?:\/\/([^.]+)\./);
|
|
1003
|
+
return match?.[1] || '';
|
|
621
1004
|
}
|
|
622
1005
|
|
|
623
1006
|
// ── Resource listing (for interactive reuse selection) ──────────────────────
|
|
@@ -710,6 +1093,20 @@ export async function listWorkspaces(region) {
|
|
|
710
1093
|
}));
|
|
711
1094
|
}
|
|
712
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* List OpenSearch Applications in the given region.
|
|
1098
|
+
* Returns [{ name, id, endpoint }].
|
|
1099
|
+
*/
|
|
1100
|
+
export async function listApplications(region) {
|
|
1101
|
+
const client = new OpenSearchClient({ region });
|
|
1102
|
+
const resp = await client.send(new ListApplicationsCommand({}));
|
|
1103
|
+
return (resp.ApplicationSummaries || []).map((a) => ({
|
|
1104
|
+
name: a.name,
|
|
1105
|
+
id: a.id,
|
|
1106
|
+
endpoint: a.endpoint || '',
|
|
1107
|
+
}));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
713
1110
|
// ── Pipeline listing / describe / update ─────────────────────────────────────
|
|
714
1111
|
|
|
715
1112
|
/**
|
|
@@ -729,74 +1126,296 @@ export async function listPipelines(region) {
|
|
|
729
1126
|
}));
|
|
730
1127
|
}
|
|
731
1128
|
|
|
1129
|
+
|
|
732
1130
|
/**
|
|
733
|
-
* Get
|
|
1131
|
+
* Get the OTLP ingest endpoint URL for an OSI pipeline.
|
|
734
1132
|
*/
|
|
735
|
-
export async function
|
|
1133
|
+
export async function getPipelineEndpoint(region, pipelineName) {
|
|
736
1134
|
const client = new OSISClient({ region });
|
|
737
1135
|
const resp = await client.send(new GetPipelineCommand({ PipelineName: pipelineName }));
|
|
738
|
-
const
|
|
739
|
-
return
|
|
740
|
-
name: p.PipelineName,
|
|
741
|
-
arn: p.PipelineArn,
|
|
742
|
-
status: p.Status,
|
|
743
|
-
statusReason: p.StatusReason?.Description,
|
|
744
|
-
minUnits: p.MinUnits,
|
|
745
|
-
maxUnits: p.MaxUnits,
|
|
746
|
-
ingestEndpoints: p.IngestEndpointUrls || [],
|
|
747
|
-
createdAt: p.CreatedAt,
|
|
748
|
-
lastUpdatedAt: p.LastUpdatedAt,
|
|
749
|
-
pipelineConfigurationBody: p.PipelineConfigurationBody,
|
|
750
|
-
logPublishingOptions: p.LogPublishingOptions,
|
|
751
|
-
bufferOptions: p.BufferOptions,
|
|
752
|
-
};
|
|
1136
|
+
const urls = resp.Pipeline?.IngestEndpointUrls;
|
|
1137
|
+
return urls?.length ? urls[0] : null;
|
|
753
1138
|
}
|
|
754
1139
|
|
|
1140
|
+
// ── Stack discovery (tag-based) ──────────────────────────────────────────────
|
|
1141
|
+
|
|
755
1142
|
/**
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
* @param {string} pipelineName
|
|
759
|
-
* @param {{ minUnits?: number, maxUnits?: number, pipelineConfigurationBody?: string, logPublishingOptions?: object, bufferOptions?: object }} params
|
|
1143
|
+
* Tag a resource after creation using the Resource Groups Tagging API.
|
|
1144
|
+
* Best-effort — failures are silently ignored.
|
|
760
1145
|
*/
|
|
761
|
-
export async function
|
|
762
|
-
|
|
1146
|
+
export async function tagResource(region, arn, stackName) {
|
|
1147
|
+
try {
|
|
1148
|
+
const client = new ResourceGroupsTaggingAPIClient({ region });
|
|
1149
|
+
await client.send(new TagResourcesCommand({
|
|
1150
|
+
ResourceARNList: [arn],
|
|
1151
|
+
Tags: { [TAG_KEY]: stackName },
|
|
1152
|
+
}));
|
|
1153
|
+
} catch { /* best effort */ }
|
|
1154
|
+
}
|
|
763
1155
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1156
|
+
/**
|
|
1157
|
+
* List all open-stack stacks in a region by querying the Resource Groups Tagging API.
|
|
1158
|
+
* Returns [{ name, resources: [{ arn, type }] }] grouped by stack name.
|
|
1159
|
+
*/
|
|
1160
|
+
export async function listStacks(region) {
|
|
1161
|
+
const client = new ResourceGroupsTaggingAPIClient({ region });
|
|
1162
|
+
const stacks = new Map();
|
|
770
1163
|
|
|
771
|
-
|
|
1164
|
+
let paginationToken;
|
|
1165
|
+
do {
|
|
1166
|
+
const resp = await client.send(new GetResourcesCommand({
|
|
1167
|
+
TagFilters: [{ Key: TAG_KEY }],
|
|
1168
|
+
PaginationToken: paginationToken || undefined,
|
|
1169
|
+
}));
|
|
772
1170
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1171
|
+
for (const r of resp.ResourceTagMappingList || []) {
|
|
1172
|
+
const tag = (r.Tags || []).find((t) => t.Key === TAG_KEY);
|
|
1173
|
+
if (!tag) continue;
|
|
1174
|
+
const stackName = tag.Value;
|
|
1175
|
+
if (!stacks.has(stackName)) {
|
|
1176
|
+
stacks.set(stackName, []);
|
|
1177
|
+
}
|
|
1178
|
+
stacks.get(stackName).push({
|
|
1179
|
+
arn: r.ResourceARN,
|
|
1180
|
+
type: arnToType(r.ResourceARN),
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
778
1183
|
|
|
779
|
-
|
|
1184
|
+
paginationToken = resp.PaginationToken;
|
|
1185
|
+
} while (paginationToken);
|
|
1186
|
+
|
|
1187
|
+
// Supplement with OpenSearch Applications (may not appear in tagging API)
|
|
1188
|
+
await supplementApplications(region, stacks);
|
|
1189
|
+
|
|
1190
|
+
return [...stacks.entries()].map(([name, resources]) => ({ name, resources }));
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Get all resources for a specific stack by its tag value.
|
|
1195
|
+
* Returns [{ arn, type }].
|
|
1196
|
+
*/
|
|
1197
|
+
export async function getStackResources(region, stackName) {
|
|
1198
|
+
const client = new ResourceGroupsTaggingAPIClient({ region });
|
|
1199
|
+
const resources = [];
|
|
1200
|
+
|
|
1201
|
+
let paginationToken;
|
|
1202
|
+
do {
|
|
1203
|
+
const resp = await client.send(new GetResourcesCommand({
|
|
1204
|
+
TagFilters: [{ Key: TAG_KEY, Values: [stackName] }],
|
|
1205
|
+
PaginationToken: paginationToken || undefined,
|
|
1206
|
+
}));
|
|
1207
|
+
|
|
1208
|
+
for (const r of resp.ResourceTagMappingList || []) {
|
|
1209
|
+
resources.push({
|
|
1210
|
+
arn: r.ResourceARN,
|
|
1211
|
+
type: arnToType(r.ResourceARN),
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
paginationToken = resp.PaginationToken;
|
|
1216
|
+
} while (paginationToken);
|
|
1217
|
+
|
|
1218
|
+
// Supplement with OpenSearch Application if not already present
|
|
1219
|
+
const stacks = new Map([[stackName, resources]]);
|
|
1220
|
+
await supplementApplications(region, stacks);
|
|
1221
|
+
|
|
1222
|
+
return resources;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Map an ARN to a human-readable resource type.
|
|
1227
|
+
*/
|
|
1228
|
+
function arnToType(arn) {
|
|
1229
|
+
if (/^arn:aws:osis:/.test(arn)) return 'OSI Pipeline';
|
|
1230
|
+
if (/^arn:aws:aoss:.*:collection\//.test(arn)) return 'OpenSearch Serverless';
|
|
1231
|
+
if (/^arn:aws:es:.*:domain\//.test(arn)) return 'OpenSearch Domain';
|
|
1232
|
+
if (/^arn:aws:iam:.*:role\//.test(arn)) return 'IAM Role';
|
|
1233
|
+
if (/^arn:aws:aps:.*:workspace\//.test(arn)) return 'APS Workspace';
|
|
1234
|
+
if (/^arn:aws:opensearch:.*:datasource\//.test(arn)) return 'DQ Data Source';
|
|
1235
|
+
if (/^arn:aws:opensearch:.*:application\//.test(arn)) return 'OpenSearch Application';
|
|
1236
|
+
if (/^arn:aws:eks:.*:cluster\//.test(arn)) return 'EKS Cluster';
|
|
1237
|
+
return 'Resource';
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Extract the resource name from an ARN.
|
|
1242
|
+
*/
|
|
1243
|
+
export function arnToName(arn) {
|
|
1244
|
+
// IAM roles: arn:aws:iam::123:role/role-name
|
|
1245
|
+
const iamMatch = arn.match(/:role\/(.+)$/);
|
|
1246
|
+
if (iamMatch) return iamMatch[1];
|
|
1247
|
+
// Most others: .../{name} or ...:<name>
|
|
1248
|
+
const lastSlash = arn.lastIndexOf('/');
|
|
1249
|
+
if (lastSlash !== -1) return arn.slice(lastSlash + 1);
|
|
1250
|
+
const lastColon = arn.lastIndexOf(':');
|
|
1251
|
+
if (lastColon !== -1) return arn.slice(lastColon + 1);
|
|
1252
|
+
return arn;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Enrich resource objects with display names where the ARN-derived name is not
|
|
1257
|
+
* human-friendly (e.g. APS workspace IDs → aliases, application IDs → names).
|
|
1258
|
+
*/
|
|
1259
|
+
export async function enrichResourceNames(region, resources) {
|
|
1260
|
+
// APS workspaces: resolve alias
|
|
1261
|
+
const apsResources = resources.filter((r) => r.type === 'APS Workspace');
|
|
1262
|
+
if (apsResources.length) {
|
|
1263
|
+
const client = new AmpClient({ region });
|
|
1264
|
+
for (const r of apsResources) {
|
|
1265
|
+
try {
|
|
1266
|
+
const wsId = arnToName(r.arn);
|
|
1267
|
+
const resp = await client.send(new DescribeWorkspaceCommand({ workspaceId: wsId }));
|
|
1268
|
+
if (resp.workspace?.alias) r.displayName = resp.workspace.alias;
|
|
1269
|
+
} catch { /* keep default */ }
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
// OpenSearch Applications: resolve name from ID
|
|
1273
|
+
const appResources = resources.filter((r) => r.type === 'OpenSearch Application');
|
|
1274
|
+
if (appResources.length) {
|
|
780
1275
|
try {
|
|
781
|
-
const
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1276
|
+
const client = new OpenSearchClient({ region });
|
|
1277
|
+
const list = await client.send(new ListApplicationsCommand({}));
|
|
1278
|
+
for (const r of appResources) {
|
|
1279
|
+
const appId = arnToName(r.arn);
|
|
1280
|
+
const app = (list.ApplicationSummaries || []).find((a) => a.id === appId);
|
|
1281
|
+
if (app?.name) r.displayName = app.name;
|
|
786
1282
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1283
|
+
} catch { /* keep default */ }
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Find OpenSearch Applications whose name matches a stack name and add them
|
|
1289
|
+
* to the resource list if not already present (tagging API may not return them).
|
|
1290
|
+
*/
|
|
1291
|
+
async function supplementApplications(region, stacks) {
|
|
1292
|
+
if (stacks.size === 0) return;
|
|
1293
|
+
try {
|
|
1294
|
+
const client = new OpenSearchClient({ region });
|
|
1295
|
+
const list = await client.send(new ListApplicationsCommand({}));
|
|
1296
|
+
for (const app of list.ApplicationSummaries || []) {
|
|
1297
|
+
if (!app.name || !app.arn) continue;
|
|
1298
|
+
const resources = stacks.get(app.name);
|
|
1299
|
+
if (!resources) continue;
|
|
1300
|
+
const alreadyPresent = resources.some((r) => r.type === 'OpenSearch Application');
|
|
1301
|
+
if (!alreadyPresent) {
|
|
1302
|
+
resources.push({ arn: app.arn, type: 'OpenSearch Application' });
|
|
791
1303
|
}
|
|
792
|
-
} catch (err) {
|
|
793
|
-
if (err.message.startsWith('Pipeline update failed')) throw err;
|
|
794
1304
|
}
|
|
795
|
-
|
|
796
|
-
|
|
1305
|
+
} catch { /* best effort */ }
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Fetch detailed information for a single resource by ARN.
|
|
1310
|
+
* Returns { entries: [[label, value], ...], rawConfig?: string }.
|
|
1311
|
+
*/
|
|
1312
|
+
export async function describeResource(region, resource) {
|
|
1313
|
+
const { arn, type } = resource;
|
|
1314
|
+
const name = arnToName(arn);
|
|
1315
|
+
const entries = [['ARN', arn]];
|
|
1316
|
+
let rawConfig;
|
|
1317
|
+
|
|
1318
|
+
try {
|
|
1319
|
+
if (type === 'OSI Pipeline') {
|
|
1320
|
+
const client = new OSISClient({ region });
|
|
1321
|
+
const resp = await client.send(new GetPipelineCommand({ PipelineName: name }));
|
|
1322
|
+
const p = resp.Pipeline || {};
|
|
1323
|
+
entries.push(['Status', p.Status || 'Unknown']);
|
|
1324
|
+
if (p.StatusReason?.Message) entries.push(['Status Reason', p.StatusReason.Message]);
|
|
1325
|
+
entries.push(['Min Units', String(p.MinUnits ?? '')]);
|
|
1326
|
+
entries.push(['Max Units', String(p.MaxUnits ?? '')]);
|
|
1327
|
+
if (p.IngestEndpointUrls?.length) {
|
|
1328
|
+
for (const url of p.IngestEndpointUrls) entries.push(['Ingest Endpoint', url]);
|
|
1329
|
+
}
|
|
1330
|
+
if (p.PipelineRoleArn) entries.push(['Role ARN', p.PipelineRoleArn]);
|
|
1331
|
+
if (p.CreatedAt) entries.push(['Created', p.CreatedAt.toISOString()]);
|
|
1332
|
+
if (p.LastUpdatedAt) entries.push(['Last Updated', p.LastUpdatedAt.toISOString()]);
|
|
1333
|
+
if (p.PipelineConfigurationBody) rawConfig = p.PipelineConfigurationBody;
|
|
1334
|
+
} else if (type === 'OpenSearch Serverless') {
|
|
1335
|
+
const client = new OpenSearchServerlessClient({ region });
|
|
1336
|
+
const resp = await client.send(new BatchGetCollectionCommand({ ids: [name] }));
|
|
1337
|
+
const c = (resp.collectionDetails || [])[0] || {};
|
|
1338
|
+
if (c.status) entries.push(['Status', c.status]);
|
|
1339
|
+
if (c.type) entries.push(['Type', c.type]);
|
|
1340
|
+
if (c.collectionEndpoint) entries.push(['Collection Endpoint', c.collectionEndpoint]);
|
|
1341
|
+
if (c.dashboardEndpoint) entries.push(['Dashboard Endpoint', c.dashboardEndpoint]);
|
|
1342
|
+
if (c.description) entries.push(['Description', c.description]);
|
|
1343
|
+
if (c.createdDate) entries.push(['Created', new Date(c.createdDate).toISOString()]);
|
|
1344
|
+
if (c.lastModifiedDate) entries.push(['Last Modified', new Date(c.lastModifiedDate).toISOString()]);
|
|
1345
|
+
} else if (type === 'OpenSearch Domain') {
|
|
1346
|
+
const client = new OpenSearchClient({ region });
|
|
1347
|
+
const resp = await client.send(new DescribeDomainCommand({ DomainName: name }));
|
|
1348
|
+
const d = resp.DomainStatus || {};
|
|
1349
|
+
if (d.EngineVersion) entries.push(['Engine Version', d.EngineVersion]);
|
|
1350
|
+
if (d.Endpoint) entries.push(['Endpoint', `https://${d.Endpoint}`]);
|
|
1351
|
+
if (d.ClusterConfig) {
|
|
1352
|
+
const cc = d.ClusterConfig;
|
|
1353
|
+
if (cc.InstanceType) entries.push(['Instance Type', cc.InstanceType]);
|
|
1354
|
+
entries.push(['Instance Count', String(cc.InstanceCount ?? 1)]);
|
|
1355
|
+
}
|
|
1356
|
+
entries.push(['Processing', String(d.Processing ?? false)]);
|
|
1357
|
+
if (d.Created !== undefined) entries.push(['Created', String(d.Created)]);
|
|
1358
|
+
} else if (type === 'APS Workspace') {
|
|
1359
|
+
const wsId = name;
|
|
1360
|
+
const client = new AmpClient({ region });
|
|
1361
|
+
const resp = await client.send(new DescribeWorkspaceCommand({ workspaceId: wsId }));
|
|
1362
|
+
const w = resp.workspace || {};
|
|
1363
|
+
if (w.status?.statusCode) entries.push(['Status', w.status.statusCode]);
|
|
1364
|
+
if (w.alias) entries.push(['Alias', w.alias]);
|
|
1365
|
+
if (w.prometheusEndpoint) entries.push(['Prometheus Endpoint', w.prometheusEndpoint]);
|
|
1366
|
+
if (w.createdAt) entries.push(['Created', w.createdAt.toISOString()]);
|
|
1367
|
+
} else if (type === 'IAM Role') {
|
|
1368
|
+
const client = new IAMClient({ region });
|
|
1369
|
+
const resp = await client.send(new GetRoleCommand({ RoleName: name }));
|
|
1370
|
+
const r = resp.Role || {};
|
|
1371
|
+
if (r.Description) entries.push(['Description', r.Description]);
|
|
1372
|
+
if (r.Path) entries.push(['Path', r.Path]);
|
|
1373
|
+
if (r.CreateDate) entries.push(['Created', r.CreateDate.toISOString()]);
|
|
1374
|
+
if (r.MaxSessionDuration) entries.push(['Max Session Duration', `${r.MaxSessionDuration}s`]);
|
|
1375
|
+
} else if (type === 'DQ Data Source') {
|
|
1376
|
+
const client = new OpenSearchClient({ region });
|
|
1377
|
+
const resp = await client.send(new GetDirectQueryDataSourceCommand({ DataSourceName: name }));
|
|
1378
|
+
if (resp.DataSourceType) {
|
|
1379
|
+
const typeKey = Object.keys(resp.DataSourceType)[0];
|
|
1380
|
+
if (typeKey) entries.push(['Data Source Type', typeKey]);
|
|
1381
|
+
}
|
|
1382
|
+
if (resp.Description) entries.push(['Description', resp.Description]);
|
|
1383
|
+
if (resp.OpenSearchArns?.length) {
|
|
1384
|
+
for (const a of resp.OpenSearchArns) entries.push(['OpenSearch ARN', a]);
|
|
1385
|
+
}
|
|
1386
|
+
} else if (type === 'OpenSearch Application') {
|
|
1387
|
+
const client = new OpenSearchClient({ region });
|
|
1388
|
+
const appId = name;
|
|
1389
|
+
const resp = await client.send(new GetApplicationCommand({ id: appId }));
|
|
1390
|
+
if (resp.status) entries.push(['Status', resp.status]);
|
|
1391
|
+
if (resp.endpoint) entries.push(['Endpoint', resp.endpoint]);
|
|
1392
|
+
if (resp.dataSources?.length) {
|
|
1393
|
+
for (const ds of resp.dataSources) {
|
|
1394
|
+
if (ds.dataSourceArn) entries.push(['Data Source', ds.dataSourceArn]);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (resp.createdAt) entries.push(['Created', resp.createdAt.toISOString()]);
|
|
1398
|
+
if (resp.lastUpdatedAt) entries.push(['Last Updated', resp.lastUpdatedAt.toISOString()]);
|
|
1399
|
+
} else if (type === 'EKS Cluster') {
|
|
1400
|
+
const client = new EKSClient({ region });
|
|
1401
|
+
const resp = await client.send(new DescribeClusterCommand({ name }));
|
|
1402
|
+
const c = resp.cluster || {};
|
|
1403
|
+
if (c.status) entries.push(['Status', c.status]);
|
|
1404
|
+
if (c.version) entries.push(['Kubernetes Version', c.version]);
|
|
1405
|
+
if (c.platformVersion) entries.push(['Platform Version', c.platformVersion]);
|
|
1406
|
+
if (c.endpoint) entries.push(['API Endpoint', c.endpoint]);
|
|
1407
|
+
if (c.roleArn) entries.push(['Role ARN', c.roleArn]);
|
|
1408
|
+
if (c.resourcesVpcConfig) {
|
|
1409
|
+
const vpc = c.resourcesVpcConfig;
|
|
1410
|
+
if (vpc.vpcId) entries.push(['VPC', vpc.vpcId]);
|
|
1411
|
+
}
|
|
1412
|
+
if (c.createdAt) entries.push(['Created', c.createdAt.toISOString()]);
|
|
1413
|
+
}
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
entries.push(['Error', err.message]);
|
|
797
1416
|
}
|
|
798
1417
|
|
|
799
|
-
|
|
1418
|
+
return { entries, rawConfig };
|
|
800
1419
|
}
|
|
801
1420
|
|
|
802
1421
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|