rds_ssm_connect 1.5.2 → 1.6.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.
@@ -3,16 +3,19 @@ on:
3
3
  release:
4
4
  types: [published]
5
5
  workflow_dispatch:
6
+
6
7
  jobs:
7
8
  build:
8
9
  runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ id-token: write
9
13
  steps:
10
- - uses: actions/checkout@v3
11
- - uses: actions/setup-node@v3
14
+ - uses: actions/checkout@v6
15
+ - uses: actions/setup-node@v6
12
16
  with:
13
- node-version: '20.x'
17
+ node-version: '22.x'
14
18
  registry-url: 'https://registry.npmjs.org'
19
+ - run: npm install -g npm@latest
15
20
  - run: npm ci
16
- - run: npm publish
17
- env:
18
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
21
+ - run: npm publish --access public
package/CLAUDE.md CHANGED
@@ -16,20 +16,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
16
16
  - Establishes SSM port forwarding session
17
17
 
18
18
  ### Configuration
19
- - `envPortMapping.js` - Environment-to-port mapping configuration
20
- - Maps environment suffixes (dev, stage, prod, team1-5, qa1-5, etc.) to local port numbers (5433-5452)
21
- - Defines default database name: `emr`
22
- - Defines AWS region: `us-east-2`
23
- - **Critical**: When adding new environments, update this mapping to assign unique ports
19
+ - `envPortMapping.js` - Multi-project configuration
20
+ - `PROJECT_CONFIGS` - Object containing per-project settings:
21
+ - `tln`: TLN/EMR project (us-east-2, Aurora clusters, `rds!cluster` secrets)
22
+ - `covered`: Covered Healthcare project (us-west-1, RDS instances, `rds!db` secrets)
23
+ - Each project defines: region, database, secretPrefix, rdsType, rdsPattern, profileFilter, envPortMapping
24
+ - Legacy exports maintained for backward compatibility
24
25
 
25
26
  ### Core Flow
26
27
  1. Read AWS profiles from `~/.aws/config`
27
- 2. Prompt user to select environment (AWS profile)
28
- 3. Query Secrets Manager for RDS credentials (secrets starting with `rds!cluster`)
29
- 4. Find running bastion instance (tagged with `Name=*bastion*`)
30
- 5. Get RDS Aurora cluster endpoint (cluster IDs ending with `-rds-aurora`)
31
- 6. Start SSM port forwarding session
32
- 7. Display connection details for database client
28
+ 2. Prompt user to select project (TLN or Covered)
29
+ 3. Filter and prompt for environment (AWS profile) based on project
30
+ 4. Query Secrets Manager for RDS credentials (project-specific prefix)
31
+ 5. Find running bastion instance (tagged with `Name=*bastion*`)
32
+ 6. Get RDS endpoint (cluster or instance based on project config)
33
+ 7. Start SSM port forwarding session with correct remote port
34
+ 8. Display connection details for database client
33
35
 
34
36
  ## Development Commands
35
37
 
@@ -71,17 +73,27 @@ Package is published to npm via GitHub Actions workflow (`.github/workflows/npm-
71
73
 
72
74
  ## AWS Resource Naming Conventions
73
75
 
74
- The application relies on specific AWS resource naming patterns:
76
+ The application relies on specific AWS resource naming patterns (per project):
77
+
78
+ **TLN Project:**
75
79
  - **Secrets**: Must start with `rds!cluster`
76
80
  - **Bastion instances**: Must be tagged with `Name=*bastion*` and in `running` state
77
81
  - **RDS clusters**: DBClusterIdentifier must end with `-rds-aurora` and be in `available` state
82
+ - **Region**: us-east-2
83
+
84
+ **Covered Project:**
85
+ - **Secrets**: Must start with `rds!db`
86
+ - **Bastion instances**: Must be tagged with `Name=*bastion*` and in `running` state
87
+ - **RDS instances**: DBInstanceIdentifier must contain `covered-db` and be in `available` state
88
+ - **Region**: us-west-1
89
+ - **AWS Profiles**: Must start with `covered` (e.g., `covered`, `covered-staging`)
78
90
 
79
91
  ## Important Notes
80
92
 
81
- - Port assignment is based on longest-matching environment suffix (e.g., `my-team-dev` matches `dev` → port 5433)
93
+ - Port assignment is based on project-specific port mappings
82
94
  - The tool keeps the SSM session running until Ctrl+C is pressed
83
- - Default port is 5432 if no environment suffix matches
84
- - All AWS operations use the `us-east-2` region by default
95
+ - Each project has its own default port if no environment suffix matches
96
+ - AWS region is determined by the selected project
85
97
 
86
98
  ## Error Handling & Recovery
87
99
 
package/connect.js CHANGED
@@ -6,7 +6,11 @@ import os from 'os'
6
6
  import path from 'path'
7
7
  import { exec } from 'child_process'
8
8
  import { promisify } from 'util'
9
- import { envPortMapping, REGION, TABLE_NAME } from './envPortMapping.js'
9
+ import { createRequire } from 'module'
10
+ import { PROJECT_CONFIGS } from './envPortMapping.js'
11
+
12
+ const require = createRequire(import.meta.url)
13
+ const packageJson = require('./package.json')
10
14
 
11
15
  const execAsync = promisify(exec)
12
16
 
@@ -18,6 +22,44 @@ const RETRY_CONFIG = {
18
22
  SSM_AGENT_READY_WAIT_MS: 10000
19
23
  }
20
24
 
25
+ // Version check configuration
26
+ const VERSION_CHECK_TIMEOUT_MS = 3000
27
+ const PACKAGE_NAME = packageJson.name
28
+ const CURRENT_VERSION = packageJson.version
29
+
30
+ async function checkForUpdates () {
31
+ try {
32
+ const controller = new AbortController()
33
+ const timeoutId = setTimeout(() => controller.abort(), VERSION_CHECK_TIMEOUT_MS)
34
+
35
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
36
+ signal: controller.signal
37
+ })
38
+ clearTimeout(timeoutId)
39
+
40
+ if (!response.ok) return
41
+
42
+ const data = await response.json()
43
+ const latestVersion = data.version
44
+
45
+ if (latestVersion && latestVersion !== CURRENT_VERSION) {
46
+ const [latestMajor, latestMinor, latestPatch] = latestVersion.split('.').map(Number)
47
+ const [currentMajor, currentMinor, currentPatch] = CURRENT_VERSION.split('.').map(Number)
48
+
49
+ const isNewer = latestMajor > currentMajor ||
50
+ (latestMajor === currentMajor && latestMinor > currentMinor) ||
51
+ (latestMajor === currentMajor && latestMinor === currentMinor && latestPatch > currentPatch)
52
+
53
+ if (isNewer) {
54
+ console.log(`\n Update available: ${CURRENT_VERSION} → ${latestVersion}`)
55
+ console.log(` Run: npm update -g ${PACKAGE_NAME}\n`)
56
+ }
57
+ }
58
+ } catch {
59
+ // Silently ignore version check failures
60
+ }
61
+ }
62
+
21
63
  // Store active child processes for cleanup
22
64
  let activeChildProcesses = []
23
65
 
@@ -44,6 +86,7 @@ async function readAwsConfig () {
44
86
  const awsConfig = await fs.readFile(awsConfigPath, { encoding: 'utf-8' })
45
87
  return awsConfig
46
88
  .split(/\r?\n/)
89
+ .map(line => line.trim())
47
90
  .filter(line => line.startsWith('[') && line.endsWith(']'))
48
91
  .map(line => line.slice(1, -1))
49
92
  .map(line => line.replace('profile ', '').trim())
@@ -68,27 +111,27 @@ async function sleep (ms) {
68
111
  return new Promise(resolve => setTimeout(resolve, ms))
69
112
  }
70
113
 
71
- async function terminateBastionInstance (ENV, instanceId) {
114
+ async function terminateBastionInstance (ENV, instanceId, region) {
72
115
  console.log(`Terminating disconnected bastion instance: ${instanceId}`)
73
- const terminateCommand = `aws-vault exec ${ENV} -- aws ec2 terminate-instances --region ${REGION} --instance-ids ${instanceId}`
116
+ const terminateCommand = `aws-vault exec ${ENV} -- aws ec2 terminate-instances --region ${region} --instance-ids ${instanceId}`
74
117
  await runCommand(terminateCommand)
75
118
  console.log('Bastion instance terminated. ASG will spin up a new instance...')
76
119
  }
77
120
 
78
- async function waitForNewBastionInstance (ENV, oldInstanceId, maxRetries = RETRY_CONFIG.BASTION_WAIT_MAX_RETRIES, retryDelay = RETRY_CONFIG.BASTION_WAIT_RETRY_DELAY_MS) {
121
+ async function waitForNewBastionInstance (ENV, oldInstanceId, region, maxRetries = RETRY_CONFIG.BASTION_WAIT_MAX_RETRIES, retryDelay = RETRY_CONFIG.BASTION_WAIT_RETRY_DELAY_MS) {
79
122
  console.log('Waiting for new bastion instance to be ready...')
80
123
 
81
124
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
82
125
  console.log(`Checking for new bastion instance (attempt ${attempt}/${maxRetries})...`)
83
126
 
84
- const instanceIdCommand = `aws-vault exec ${ENV} -- aws ec2 describe-instances --region ${REGION} --filters "Name=tag:Name,Values='*bastion*'" "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].[InstanceId] | [0][0]" --output text`
127
+ const instanceIdCommand = `aws-vault exec ${ENV} -- aws ec2 describe-instances --region ${region} --filters "Name=tag:Name,Values='*bastion*'" "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].[InstanceId] | [0][0]" --output text`
85
128
  const newInstanceId = await runCommand(instanceIdCommand)
86
129
 
87
130
  if (newInstanceId && newInstanceId !== oldInstanceId && newInstanceId !== 'None') {
88
131
  console.log(`New bastion instance found: ${newInstanceId}`)
89
132
 
90
133
  // Verify SSM agent is ready
91
- const isReady = await waitForSSMAgentReady(ENV, newInstanceId)
134
+ const isReady = await waitForSSMAgentReady(ENV, newInstanceId, region)
92
135
  if (isReady) {
93
136
  return newInstanceId
94
137
  } else {
@@ -104,11 +147,11 @@ async function waitForNewBastionInstance (ENV, oldInstanceId, maxRetries = RETRY
104
147
  return null
105
148
  }
106
149
 
107
- async function waitForSSMAgentReady (ENV, instanceId, maxRetries = 10, retryDelay = 3000) {
150
+ async function waitForSSMAgentReady (ENV, instanceId, region, maxRetries = 10, retryDelay = 3000) {
108
151
  console.log('Verifying SSM agent status...')
109
152
 
110
153
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
111
- const ssmStatusCommand = `aws-vault exec ${ENV} -- aws ssm describe-instance-information --region ${REGION} --filters "Key=InstanceIds,Values=${instanceId}" --query "InstanceInformationList[0].PingStatus" --output text`
154
+ const ssmStatusCommand = `aws-vault exec ${ENV} -- aws ssm describe-instance-information --region ${region} --filters "Key=InstanceIds,Values=${instanceId}" --query "InstanceInformationList[0].PingStatus" --output text`
112
155
  const status = await runCommand(ssmStatusCommand)
113
156
 
114
157
  if (status === 'Online') {
@@ -166,14 +209,14 @@ function monitorPortForwardingSession (child) {
166
209
  return state
167
210
  }
168
211
 
169
- async function handleTargetNotConnectedError (ENV, instanceId, rdsEndpoint, portNumber, retryCount, maxRetries) {
212
+ async function handleTargetNotConnectedError (ENV, instanceId, rdsEndpoint, portNumber, remotePort, region, retryCount, maxRetries) {
170
213
  console.log(`\nDetected TargetNotConnected error. Attempting recovery (retry ${retryCount + 1}/${maxRetries})...`)
171
214
 
172
215
  // Terminate the disconnected instance
173
- await terminateBastionInstance(ENV, instanceId)
216
+ await terminateBastionInstance(ENV, instanceId, region)
174
217
 
175
218
  // Wait for new instance to be ready
176
- const newInstanceId = await waitForNewBastionInstance(ENV, instanceId)
219
+ const newInstanceId = await waitForNewBastionInstance(ENV, instanceId, region)
177
220
 
178
221
  if (!newInstanceId) {
179
222
  throw new Error('Failed to find new bastion instance after waiting.')
@@ -181,11 +224,11 @@ async function handleTargetNotConnectedError (ENV, instanceId, rdsEndpoint, port
181
224
 
182
225
  // Retry with new instance
183
226
  console.log('Retrying port forwarding with new bastion instance...')
184
- return await startPortForwarding(ENV, newInstanceId, rdsEndpoint, portNumber, retryCount + 1, maxRetries)
227
+ return await startPortForwardingWithConfig(ENV, newInstanceId, rdsEndpoint, portNumber, remotePort, region, retryCount + 1, maxRetries)
185
228
  }
186
229
 
187
- function executePortForwardingCommand (ENV, instanceId, rdsEndpoint, portNumber) {
188
- const portForwardingCommand = `aws-vault exec ${ENV} -- aws ssm start-session --target ${instanceId} --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters "host=${rdsEndpoint},portNumber='5432',localPortNumber='${portNumber}'" --cli-connect-timeout 0`
230
+ function executePortForwardingCommand (ENV, instanceId, rdsEndpoint, portNumber, remotePort, region) {
231
+ const portForwardingCommand = `aws-vault exec ${ENV} -- aws ssm start-session --region ${region} --target ${instanceId} --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters "host=${rdsEndpoint},portNumber='${remotePort}',localPortNumber='${portNumber}'" --cli-connect-timeout 0`
189
232
 
190
233
  console.log('Starting port forwarding session...')
191
234
  const child = exec(portForwardingCommand)
@@ -196,9 +239,9 @@ function executePortForwardingCommand (ENV, instanceId, rdsEndpoint, portNumber)
196
239
  return child
197
240
  }
198
241
 
199
- async function startPortForwarding (ENV, instanceId, rdsEndpoint, portNumber, retryCount = 0, maxRetries = RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES) {
242
+ async function startPortForwardingWithConfig (ENV, instanceId, rdsEndpoint, portNumber, remotePort, region, retryCount = 0, maxRetries = RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES) {
200
243
  return new Promise((resolve, reject) => {
201
- const child = executePortForwardingCommand(ENV, instanceId, rdsEndpoint, portNumber)
244
+ const child = executePortForwardingCommand(ENV, instanceId, rdsEndpoint, portNumber, remotePort, region)
202
245
  const sessionState = monitorPortForwardingSession(child)
203
246
 
204
247
  child.on('close', async (code) => {
@@ -208,7 +251,7 @@ async function startPortForwarding (ENV, instanceId, rdsEndpoint, portNumber, re
208
251
  try {
209
252
  // Handle TargetNotConnected error with retry
210
253
  if (code === 254 && sessionState.targetNotConnectedError && retryCount < maxRetries) {
211
- await handleTargetNotConnectedError(ENV, instanceId, rdsEndpoint, portNumber, retryCount, maxRetries)
254
+ await handleTargetNotConnectedError(ENV, instanceId, rdsEndpoint, portNumber, remotePort, region, retryCount, maxRetries)
212
255
  resolve()
213
256
  } else if (code !== 0) {
214
257
  console.log(`Port forwarding session ended with code ${code}`)
@@ -225,19 +268,116 @@ async function startPortForwarding (ENV, instanceId, rdsEndpoint, portNumber, re
225
268
  })
226
269
  }
227
270
 
271
+ // Legacy function for backward compatibility
272
+ async function startPortForwarding (ENV, instanceId, rdsEndpoint, portNumber, retryCount = 0, maxRetries = RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES) {
273
+ return startPortForwardingWithConfig(ENV, instanceId, rdsEndpoint, portNumber, '5432', 'us-east-2', retryCount, maxRetries)
274
+ }
275
+
276
+ async function getRdsEndpoint (ENV, projectConfig) {
277
+ const { region, rdsType, rdsPattern } = projectConfig
278
+
279
+ if (rdsType === 'cluster') {
280
+ // Aurora cluster lookup
281
+ const rdsEndpointCommand = `aws-vault exec ${ENV} -- aws rds describe-db-clusters --region ${region} --query "DBClusters[?Status=='available' && ends_with(DBClusterIdentifier, '${rdsPattern}')].Endpoint | [0]" --output text`
282
+ return await runCommand(rdsEndpointCommand)
283
+ } else {
284
+ // Single RDS instance lookup
285
+ const rdsEndpointCommand = `aws-vault exec ${ENV} -- aws rds describe-db-instances --region ${region} --query "DBInstances[?DBInstanceStatus=='available' && contains(DBInstanceIdentifier, '${rdsPattern}')].Endpoint.Address | [0]" --output text`
286
+ return await runCommand(rdsEndpointCommand)
287
+ }
288
+ }
289
+
290
+ async function getRdsPort (ENV, projectConfig) {
291
+ const { region, rdsType, rdsPattern } = projectConfig
292
+
293
+ if (rdsType === 'cluster') {
294
+ const portCommand = `aws-vault exec ${ENV} -- aws rds describe-db-clusters --region ${region} --query "DBClusters[?Status=='available' && ends_with(DBClusterIdentifier, '${rdsPattern}')].Port | [0]" --output text`
295
+ return await runCommand(portCommand) || '5432'
296
+ } else {
297
+ const portCommand = `aws-vault exec ${ENV} -- aws rds describe-db-instances --region ${region} --query "DBInstances[?DBInstanceStatus=='available' && contains(DBInstanceIdentifier, '${rdsPattern}')].Endpoint.Port | [0]" --output text`
298
+ return await runCommand(portCommand) || '5432'
299
+ }
300
+ }
301
+
302
+ function getProfilesForProject (allProfiles, projectConfig, allProjectConfigs) {
303
+ const { profileFilter } = projectConfig
304
+
305
+ if (profileFilter) {
306
+ // Project has explicit filter - return profiles starting with filter
307
+ return allProfiles.filter(env => env.startsWith(profileFilter))
308
+ } else {
309
+ // No filter (legacy project like TLN) - return profiles that don't match any other project's filter
310
+ const otherFilters = Object.values(allProjectConfigs)
311
+ .filter(config => config.profileFilter)
312
+ .map(config => config.profileFilter)
313
+
314
+ return allProfiles.filter(env =>
315
+ !otherFilters.some(filter => env.startsWith(filter))
316
+ )
317
+ }
318
+ }
319
+
228
320
  async function main () {
229
321
  // Setup process cleanup handlers
230
322
  setupProcessCleanup()
231
323
 
324
+ // Check for updates (non-blocking)
325
+ await checkForUpdates()
326
+
232
327
  try {
233
- const ENVS = await readAwsConfig()
328
+ // Read all AWS profiles first
329
+ const allProfiles = await readAwsConfig()
234
330
 
235
- if (ENVS.length === 0) {
331
+ if (allProfiles.length === 0) {
236
332
  console.error('No environments found in AWS config file.')
237
333
  return
238
334
  }
239
335
 
240
- const answers = await inquirer.prompt([
336
+ // Step 1: Filter projects based on available profiles
337
+ const projectChoices = Object.entries(PROJECT_CONFIGS)
338
+ .filter(([key, config]) => {
339
+ const matchingProfiles = getProfilesForProject(allProfiles, config, PROJECT_CONFIGS)
340
+ return matchingProfiles.length > 0
341
+ })
342
+ .map(([key, config]) => ({
343
+ name: config.name,
344
+ value: key
345
+ }))
346
+
347
+ if (projectChoices.length === 0) {
348
+ console.error('No projects available for the configured AWS profiles.')
349
+ return
350
+ }
351
+
352
+ // Skip project selection if only one project available
353
+ let projectKey
354
+ if (projectChoices.length === 1) {
355
+ projectKey = projectChoices[0].value
356
+ console.log(`Auto-selected project: ${projectChoices[0].name}`)
357
+ } else {
358
+ const projectAnswer = await inquirer.prompt([
359
+ {
360
+ type: 'select',
361
+ name: 'project',
362
+ message: 'Please select the project:',
363
+ choices: projectChoices,
364
+ },
365
+ ])
366
+ projectKey = projectAnswer.project
367
+ }
368
+
369
+ const projectConfig = PROJECT_CONFIGS[projectKey]
370
+ const { region, database, secretPrefix, envPortMapping, defaultPort } = projectConfig
371
+
372
+ // Step 2: Get profiles for selected project
373
+ let ENVS = getProfilesForProject(allProfiles, projectConfig, PROJECT_CONFIGS)
374
+
375
+ if (ENVS.length === 0) {
376
+ console.error('No AWS profiles found for this project.')
377
+ return
378
+ }
379
+
380
+ const envAnswer = await inquirer.prompt([
241
381
  {
242
382
  type: 'select',
243
383
  name: 'ENV',
@@ -246,20 +386,24 @@ async function main () {
246
386
  },
247
387
  ])
248
388
 
249
- const ENV = answers.ENV
389
+ const ENV = envAnswer.ENV
390
+
391
+ // Determine local port number
250
392
  const allEnvSuffixes = Object.keys(envPortMapping).sort((a, b) => b.length - a.length)
251
- const matchedSuffix = allEnvSuffixes.find(suffix => ENV.endsWith(suffix))
252
- const portNumber = envPortMapping[matchedSuffix] || '5432'
393
+ const matchedSuffix = allEnvSuffixes.find(suffix => ENV.endsWith(suffix)) ||
394
+ allEnvSuffixes.find(suffix => ENV === suffix)
395
+ const portNumber = envPortMapping[matchedSuffix] || defaultPort
253
396
 
254
- const secretsListCommand = `aws-vault exec ${ENV} -- aws secretsmanager list-secrets --region ${REGION} --query "SecretList[?starts_with(Name, 'rds!cluster')].Name | [0]" --output text`
397
+ // Get RDS credentials from Secrets Manager
398
+ const secretsListCommand = `aws-vault exec ${ENV} -- aws secretsmanager list-secrets --region ${region} --query "SecretList[?starts_with(Name, '${secretPrefix}')].Name | [0]" --output text`
255
399
  const SECRET_NAME = await runCommand(secretsListCommand)
256
400
 
257
- if (!SECRET_NAME) {
258
- console.error('No secret found with name starting with rds!cluster.')
401
+ if (!SECRET_NAME || SECRET_NAME === 'None') {
402
+ console.error(`No secret found with name starting with '${secretPrefix}'.`)
259
403
  return
260
404
  }
261
405
 
262
- const secretsGetCommand = `aws-vault exec ${ENV} -- aws secretsmanager get-secret-value --region ${REGION} --secret-id "${SECRET_NAME}" --query SecretString --output text`
406
+ const secretsGetCommand = `aws-vault exec ${ENV} -- aws secretsmanager get-secret-value --region ${region} --secret-id "${SECRET_NAME}" --query SecretString --output text`
263
407
  const secretString = await runCommand(secretsGetCommand)
264
408
 
265
409
  if (!secretString) {
@@ -281,14 +425,15 @@ async function main () {
281
425
  const USERNAME = CREDENTIALS.username
282
426
  const PASSWORD = CREDENTIALS.password
283
427
 
284
- console.log(`Your connection details:
428
+ console.log(`\nYour connection details:
285
429
  Host: localhost
286
430
  Port: ${portNumber}
287
431
  User: ${USERNAME}
288
- Database: ${TABLE_NAME}
289
- Password: ${PASSWORD}`)
432
+ Database: ${database}
433
+ Password: ${PASSWORD}\n`)
290
434
 
291
- const instanceIdCommand = `aws-vault exec ${ENV} -- aws ec2 describe-instances --region ${REGION} --filters "Name=tag:Name,Values='*bastion*'" "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].[InstanceId] | [0][0]" --output text`
435
+ // Find bastion instance
436
+ const instanceIdCommand = `aws-vault exec ${ENV} -- aws ec2 describe-instances --region ${region} --filters "Name=tag:Name,Values='*bastion*'" "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].[InstanceId] | [0][0]" --output text`
292
437
  const INSTANCE_ID = await runCommand(instanceIdCommand)
293
438
 
294
439
  if (!INSTANCE_ID || INSTANCE_ID === 'None') {
@@ -296,15 +441,18 @@ async function main () {
296
441
  return
297
442
  }
298
443
 
299
- const rdsEndpointCommand = `aws-vault exec ${ENV} -- aws rds describe-db-clusters --region ${REGION} --query "DBClusters[?Status=='available' && ends_with(DBClusterIdentifier, '-rds-aurora')].Endpoint | [0]" --output text`
300
- const RDS_ENDPOINT = await runCommand(rdsEndpointCommand)
444
+ // Get RDS endpoint
445
+ const RDS_ENDPOINT = await getRdsEndpoint(ENV, projectConfig)
301
446
 
302
447
  if (!RDS_ENDPOINT || RDS_ENDPOINT === 'None') {
303
448
  console.error('Failed to find the RDS endpoint.')
304
449
  return
305
450
  }
306
451
 
307
- await startPortForwarding(ENV, INSTANCE_ID, RDS_ENDPOINT, portNumber)
452
+ // Get RDS port (remote port)
453
+ const rdsPort = await getRdsPort(ENV, projectConfig)
454
+
455
+ await startPortForwardingWithConfig(ENV, INSTANCE_ID, RDS_ENDPOINT, portNumber, rdsPort, region)
308
456
  } catch (error) {
309
457
  console.error(`Error: ${error.message}`)
310
458
  console.error('Exiting due to unhandled error')
package/envPortMapping.js CHANGED
@@ -1,29 +1,54 @@
1
- // Define a mapping of environment suffixes to port numbers
2
- export const envPortMapping = {
3
- dev: '5433',
4
- stage: '5434',
5
- 'pre-prod': '5435',
6
- prod: '5436',
7
- dev2: '5437',
8
- stage2: '5438',
9
- sandbox: '5439',
10
- 'perf-dev': '5440',
11
- support: '5441',
12
- team1: '5442',
13
- team2: '5443',
14
- team3: '5444',
15
- team4: '5445',
16
- team5: '5446',
17
- qa1: '5447',
18
- qa2: '5448',
19
- qa3: '5449',
20
- qa4: '5450',
21
- qa5: '5451',
22
- hotfix: '5452'
1
+ // Project configurations
2
+ export const PROJECT_CONFIGS = {
3
+ tln: {
4
+ name: 'TLN (EMR)',
5
+ region: 'us-east-2',
6
+ database: 'emr',
7
+ secretPrefix: 'rds!cluster',
8
+ rdsType: 'cluster', // Aurora cluster
9
+ rdsPattern: '-rds-aurora', // DBClusterIdentifier ends with this
10
+ profileFilter: null, // Show all profiles (legacy behavior)
11
+ envPortMapping: {
12
+ dev: '5433',
13
+ stage: '5434',
14
+ 'pre-prod': '5435',
15
+ prod: '5436',
16
+ dev2: '5437',
17
+ stage2: '5438',
18
+ sandbox: '5439',
19
+ 'perf-dev': '5440',
20
+ support: '5441',
21
+ team1: '5442',
22
+ team2: '5443',
23
+ team3: '5444',
24
+ team4: '5445',
25
+ team5: '5446',
26
+ qa1: '5447',
27
+ qa2: '5448',
28
+ qa3: '5449',
29
+ qa4: '5450',
30
+ qa5: '5451',
31
+ hotfix: '5452'
32
+ },
33
+ defaultPort: '5432'
34
+ },
35
+ covered: {
36
+ name: 'Covered (Healthcare)',
37
+ region: 'us-west-1',
38
+ database: 'covered',
39
+ secretPrefix: 'rds!db',
40
+ rdsType: 'instance', // Single RDS instance (not Aurora)
41
+ rdsPattern: 'covered-db', // DBInstanceIdentifier contains this
42
+ profileFilter: 'covered', // Only show profiles starting with 'covered'
43
+ envPortMapping: {
44
+ 'covered': '5460',
45
+ 'covered-staging': '5461'
46
+ },
47
+ defaultPort: '5460'
48
+ }
23
49
  }
24
50
 
25
- // Define the table name
26
- export const TABLE_NAME = 'emr'
27
-
28
- // Define the AWS region
29
- export const REGION = 'us-east-2'
51
+ // Legacy exports for backward compatibility
52
+ export const envPortMapping = PROJECT_CONFIGS.tln.envPortMapping
53
+ export const TABLE_NAME = PROJECT_CONFIGS.tln.database
54
+ export const REGION = PROJECT_CONFIGS.tln.region
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "rds_ssm_connect",
3
- "version": "1.5.2",
3
+ "version": "1.6.2",
4
4
  "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/yarka-guru/connection_app.git"
8
+ },
5
9
  "dependencies": {
6
10
  "@aws-sdk/client-ec2": "^3.980.0",
7
11
  "@aws-sdk/client-rds": "^3.980.0",
8
12
  "@aws-sdk/client-ssm": "^3.980.0",
13
+ "fast-xml-parser": "^5.3.4",
9
14
  "glob": "^13.0.0",
10
15
  "inquirer": "^13.2.2",
11
16
  "rimraf": "^6.1.2"