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.
- package/.github/workflows/npm-publish.yml +9 -6
- package/CLAUDE.md +27 -15
- package/connect.js +182 -34
- package/envPortMapping.js +52 -27
- package/package.json +6 -1
|
@@ -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@
|
|
11
|
-
- uses: actions/setup-node@
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
12
16
|
with:
|
|
13
|
-
node-version: '
|
|
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` -
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
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
|
|
28
|
-
3.
|
|
29
|
-
4.
|
|
30
|
-
5.
|
|
31
|
-
6.
|
|
32
|
-
7.
|
|
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
|
|
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
|
-
-
|
|
84
|
-
-
|
|
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 {
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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='
|
|
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
|
|
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
|
-
|
|
328
|
+
// Read all AWS profiles first
|
|
329
|
+
const allProfiles = await readAwsConfig()
|
|
234
330
|
|
|
235
|
-
if (
|
|
331
|
+
if (allProfiles.length === 0) {
|
|
236
332
|
console.error('No environments found in AWS config file.')
|
|
237
333
|
return
|
|
238
334
|
}
|
|
239
335
|
|
|
240
|
-
|
|
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 =
|
|
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
|
-
|
|
393
|
+
const matchedSuffix = allEnvSuffixes.find(suffix => ENV.endsWith(suffix)) ||
|
|
394
|
+
allEnvSuffixes.find(suffix => ENV === suffix)
|
|
395
|
+
const portNumber = envPortMapping[matchedSuffix] || defaultPort
|
|
253
396
|
|
|
254
|
-
|
|
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(
|
|
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 ${
|
|
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(
|
|
428
|
+
console.log(`\nYour connection details:
|
|
285
429
|
Host: localhost
|
|
286
430
|
Port: ${portNumber}
|
|
287
431
|
User: ${USERNAME}
|
|
288
|
-
Database: ${
|
|
289
|
-
Password: ${PASSWORD}`)
|
|
432
|
+
Database: ${database}
|
|
433
|
+
Password: ${PASSWORD}\n`)
|
|
290
434
|
|
|
291
|
-
|
|
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
|
-
|
|
300
|
-
const RDS_ENDPOINT = await
|
|
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
|
-
|
|
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
|
-
//
|
|
2
|
-
export const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
//
|
|
26
|
-
export const
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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"
|