33 * Deletes in reverse dependency order: EC2 → Application → DQS → OSIS → IAM → (preserves AOS/AMP).
44 */
55import { OSISClient , DeletePipelineCommand , GetPipelineCommand } from '@aws-sdk/client-osis' ;
6- import { OpenSearchClient , ListApplicationsCommand , DeleteApplicationCommand , DeleteDirectQueryDataSourceCommand } from '@aws-sdk/client-opensearch' ;
6+ import { OpenSearchClient , ListApplicationsCommand , DeleteApplicationCommand , DeleteDirectQueryDataSourceCommand , GetApplicationCommand , DescribeDomainCommand } from '@aws-sdk/client-opensearch' ;
77import { IAMClient , DeleteRolePolicyCommand , DeleteRoleCommand , ListRolePoliciesCommand } from '@aws-sdk/client-iam' ;
88import { printStep , printSuccess , printWarning , printInfo , createSpinner } from './ui.mjs' ;
99import { teardownDemoInstance } from './ec2-demo.mjs' ;
1010
11+ async function cleanupFgacRoles ( region , pipelineName , opensearchPassword , os ) {
12+ try {
13+ const { ApplicationSummaries } = await os . send ( new ListApplicationsCommand ( { } ) ) ;
14+ const app = ( ApplicationSummaries || [ ] ) . find ( a => a . name === pipelineName ) ;
15+ if ( ! app ) return ;
16+
17+ const { dataSources } = await os . send ( new GetApplicationCommand ( { id : app . id } ) ) ;
18+ const domainArn = ( dataSources || [ ] ) . find ( d => d . dataSourceArn ?. includes ( ':domain/' ) ) ?. dataSourceArn ;
19+ if ( ! domainArn ) return ;
20+
21+ const domainName = domainArn . split ( '/' ) . pop ( ) ;
22+ const { DomainStatus } = await os . send ( new DescribeDomainCommand ( { DomainName : domainName } ) ) ;
23+ if ( ! DomainStatus ?. Endpoint ) return ;
24+
25+ // Get password from Secrets Manager or flag
26+ let masterPass = opensearchPassword || '' ;
27+ if ( ! masterPass ) {
28+ try {
29+ const { SecretsManagerClient, GetSecretValueCommand } = await import ( '@aws-sdk/client-secrets-manager' ) ;
30+ const sm = new SecretsManagerClient ( { region } ) ;
31+ const { SecretString } = await sm . send ( new GetSecretValueCommand ( {
32+ SecretId : `open-stack/${ pipelineName } /master-password` ,
33+ } ) ) ;
34+ masterPass = SecretString ;
35+ } catch { /* no secret found */ }
36+ }
37+ if ( ! masterPass ) {
38+ printWarning ( 'No master password available. FGAC cleanup skipped. Pass --opensearch-password to clean up role mappings.' ) ;
39+ return ;
40+ }
41+
42+ const endpoint = `https://${ DomainStatus . Endpoint } ` ;
43+ const url = `${ endpoint } /_plugins/_security/api/rolesmapping/all_access` ;
44+ const auth = Buffer . from ( `admin:${ masterPass } ` ) . toString ( 'base64' ) ;
45+ const headers = { 'Content-Type' : 'application/json' , 'Authorization' : `Basic ${ auth } ` } ;
46+
47+ const getResp = await fetch ( url , { headers } ) ;
48+ if ( ! getResp . ok ) return ;
49+
50+ const data = await getResp . json ( ) ;
51+ const existing = data ?. all_access ?. backend_roles || [ ] ;
52+ const filtered = existing . filter ( r => ! r . includes ( pipelineName ) ) ;
53+
54+ if ( filtered . length !== existing . length ) {
55+ await fetch ( url , {
56+ method : 'PATCH' ,
57+ headers,
58+ body : JSON . stringify ( [ { op : 'add' , path : '/backend_roles' , value : filtered } ] ) ,
59+ } ) ;
60+ printSuccess ( 'FGAC backend role mappings cleaned up' ) ;
61+ }
62+ } catch ( e ) {
63+ printWarning ( `FGAC cleanup: ${ e . message } ` ) ;
64+ }
65+ }
66+
1167export async function destroy ( cfg ) {
1268 const { pipelineName, region } = cfg ;
1369 if ( ! pipelineName ) throw new Error ( '--pipeline-name is required' ) ;
@@ -18,8 +74,11 @@ export async function destroy(cfg) {
1874 // 1. EC2 demo instance + SG + instance profile
1975 await teardownDemoInstance ( cfg ) ;
2076
21- // 2. OpenSearch Application
77+ // 2. Clean up FGAC backend role mappings (before deleting Application)
2278 const os = new OpenSearchClient ( { region } ) ;
79+ await cleanupFgacRoles ( region , pipelineName , cfg . opensearchPassword , os ) ;
80+
81+ // 3. OpenSearch Application
2382 try {
2483 const { ApplicationSummaries } = await os . send ( new ListApplicationsCommand ( { } ) ) ;
2584 const app = ( ApplicationSummaries || [ ] ) . find ( a => a . name === pipelineName ) ;
0 commit comments