rds_ssm_connect 1.2.2 → 1.3.0

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.
Files changed (2) hide show
  1. package/connect.js +106 -117
  2. package/package.json +9 -6
package/connect.js CHANGED
@@ -1,137 +1,126 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Import necessary modules
4
- import { spawn } from 'child_process' // For spawning child processes
5
- import inquirer from 'inquirer' // For prompting the user for input
6
- import fs from 'fs' // For reading files
7
- import os from 'os' // For getting the user's home directory
8
- import path from 'path' // For working with file paths
3
+ import inquirer from 'inquirer'
4
+ import fs from 'fs/promises'
5
+ import os from 'os'
6
+ import path from 'path'
7
+ import { exec } from 'child_process'
8
+ import { promisify } from 'util'
9
9
  import { envPortMapping, REGION, TABLE_NAME } from './envPortMapping.js'
10
10
 
11
- // Get the path to the AWS config file
12
- const awsConfigPath = path.join(os.homedir(), '.aws', 'config')
13
-
14
- // Read the contents of the AWS config file
15
- const awsConfig = fs.readFileSync(awsConfigPath, 'utf-8')
16
-
17
- // Extract environments from the AWS config file
18
- const ENVS = awsConfig
19
- .split('\n')
20
- .filter(line => line.startsWith('[') && line.endsWith(']'))
21
- .map(line => line.slice(1, -1))
22
- .map(line => line.replace('profile ', ''))
23
-
24
- // Prompt the user to select an environment
25
- inquirer
26
- .prompt([
27
- {
28
- type: 'list',
29
- name: 'ENV',
30
- message: 'Please select the environment:',
31
- choices: ENVS
11
+ const execAsync = promisify(exec)
12
+
13
+ async function readAwsConfig () {
14
+ const awsConfigPath = path.join(os.homedir(), '.aws', 'config')
15
+ try {
16
+ const awsConfig = await fs.readFile(awsConfigPath, 'utf-8')
17
+ return awsConfig
18
+ .split('\n')
19
+ .filter(line => line.startsWith('[') && line.endsWith(']'))
20
+ .map(line => line.slice(1, -1))
21
+ .map(line => line.replace('profile ', ''))
22
+ } catch (error) {
23
+ console.error('Error reading AWS config file:', error)
24
+ return []
25
+ }
26
+ }
27
+
28
+ async function runCommand (command) {
29
+ try {
30
+ const { stdout } = await execAsync(command)
31
+ return stdout.trim()
32
+ } catch (error) {
33
+ console.error(`Error executing command: ${command}`)
34
+ console.error(error)
35
+ return null
36
+ }
37
+ }
38
+
39
+ async function main () {
40
+ try {
41
+ const ENVS = await readAwsConfig()
42
+
43
+ if (ENVS.length === 0) {
44
+ console.error('No environments found in AWS config file.')
45
+ return
32
46
  }
33
- ])
34
- .then((answers) => {
35
- const ENV = answers.ENV // Get the selected environment from the user's answers
36
- console.log(`You selected: ${ENV}`)
37
47
 
38
- // Sort all environment suffixes by length, longest first
39
- const allEnvSuffixes = Object.keys(envPortMapping).sort((a, b) => b.length - a.length)
48
+ const answers = await inquirer.prompt([
49
+ {
50
+ type: 'list',
51
+ name: 'ENV',
52
+ message: 'Please select the environment:',
53
+ choices: ENVS
54
+ }
55
+ ])
40
56
 
41
- // Find the first matching suffix in your envPortMapping object
57
+ const ENV = answers.ENV
58
+ const allEnvSuffixes = Object.keys(envPortMapping).sort((a, b) => b.length - a.length)
42
59
  const matchedSuffix = allEnvSuffixes.find(suffix => ENV.endsWith(suffix))
60
+ const portNumber = envPortMapping[matchedSuffix] || '5432'
43
61
 
44
- // If no port number is found for the environment, default to 5432
45
- let portNumber = envPortMapping[matchedSuffix]
62
+ const secretsListCommand = `aws-vault exec ${ENV} -- aws secretsmanager list-secrets --region ${REGION} --query "SecretList[?starts_with(Name, 'rds!cluster')].Name | [0]" --output text`
63
+ const SECRET_NAME = await runCommand(secretsListCommand)
46
64
 
47
- if (!portNumber) {
48
- console.error(`No port number found for environment: ${ENV}. Defaulting to 5432.`)
49
- portNumber = '5432'
65
+ if (!SECRET_NAME) {
66
+ console.error('No secret found with name starting with rds!cluster.')
67
+ return
50
68
  }
51
69
 
52
- // Set up the commands to run inside the aws-vault environment
53
- const awsVaultExecCommand = ['aws-vault', 'exec', ENV, '--']
54
- const secretsDescribeCommand = `aws secretsmanager list-secrets --region ${REGION} --query 'SecretList[?starts_with(Name, \`rds!cluster\`)].Name' --output text | head -n 1`
70
+ const secretsGetCommand = `aws-vault exec ${ENV} -- aws secretsmanager get-secret-value --region ${REGION} --secret-id "${SECRET_NAME}" --query SecretString --output text`
71
+ const secretString = await runCommand(secretsGetCommand)
72
+ const CREDENTIALS = JSON.parse(secretString)
73
+ const USERNAME = CREDENTIALS.username
74
+ const PASSWORD = CREDENTIALS.password
55
75
 
56
- // Run the commands inside aws-vault environment
57
- const secretsDescribeProcess = spawn('sh', ['-c', `${awsVaultExecCommand.join(' ')} ${secretsDescribeCommand}`])
76
+ console.log(`Your connection string is: psql -h localhost -p ${portNumber} -U ${USERNAME} -d ${TABLE_NAME}`)
77
+ console.log(`Use the password: ${PASSWORD}`)
58
78
 
59
- // Get the name of the secret containing the RDS credentials
60
- secretsDescribeProcess.stdout.on('data', (data) => {
61
- const SECRET_NAME = data.toString().trim()
79
+ 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`
80
+ const INSTANCE_ID = await runCommand(instanceIdCommand)
62
81
 
63
- if (!SECRET_NAME) {
64
- console.error('No secret found with name starting with rds!cluster.')
65
- return
66
- }
82
+ if (!INSTANCE_ID) {
83
+ console.error('Failed to find a running instance with tag Name=*bastion*.')
84
+ return
85
+ }
86
+
87
+ 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`
88
+ const RDS_ENDPOINT = await runCommand(rdsEndpointCommand)
67
89
 
68
- // Get the RDS credentials from Secrets Manager
69
- const secretsGetCommand = `aws secretsmanager get-secret-value --region ${REGION} --secret-id '${SECRET_NAME}' --query SecretString --output text`
70
- const secretsGetProcess = spawn('sh', ['-c', `${awsVaultExecCommand.join(' ')} ${secretsGetCommand}`])
71
-
72
- // Parse the JSON output of the secretsmanager get-secret-value command to get the RDS credentials
73
- secretsGetProcess.stdout.on('data', (data) => {
74
- const CREDENTIALS = JSON.parse(data.toString())
75
- const USERNAME = CREDENTIALS.username // Get the RDS username from the credentials
76
- const PASSWORD = CREDENTIALS.password // Get the RDS password from the credentials
77
-
78
- // Display connection credentials and connection string
79
- console.log(`Your connection string is: psql -h localhost -p ${portNumber} -U ${USERNAME} -d ${TABLE_NAME}`)
80
- console.log(`Use the password: ${PASSWORD}`)
81
-
82
- // Get the ID of the bastion instance in running state
83
- const instanceIdCommand = `aws ec2 describe-instances --region ${REGION} --filters "Name=tag:Name,Values='*bastion*'" "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].[InstanceId]" --output text`
84
- const instanceIdProcess = spawn('sh', ['-c', `${awsVaultExecCommand.join(' ')} ${instanceIdCommand}`])
85
-
86
- instanceIdProcess.stdout.on('data', (data) => {
87
- const INSTANCE_ID = data.toString().trim()
88
-
89
- if (!INSTANCE_ID) {
90
- console.error('Failed to find a running instance with tag Name=*bastion*.')
91
- return
92
- }
93
-
94
- // Get the endpoint of the RDS cluster
95
- const rdsEndpointCommand = `aws rds describe-db-clusters --region ${REGION} --query "DBClusters[?Status=='available' && ends_with(DBClusterIdentifier, '-rds-aurora')].Endpoint" --output text`
96
- const rdsEndpointProcess = spawn('sh', ['-c', `${awsVaultExecCommand.join(' ')} ${rdsEndpointCommand}`])
97
-
98
- rdsEndpointProcess.stdout.on('data', (data) => {
99
- const RDS_ENDPOINT = data.toString().trim()
100
-
101
- if (!RDS_ENDPOINT) {
102
- console.error('Failed to find the RDS endpoint.')
103
- return
104
- }
105
-
106
- // Start a port forwarding session to the RDS cluster
107
- const portForwardingCommand = `aws ssm start-session --target ${INSTANCE_ID} --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters "host=${RDS_ENDPOINT},portNumber='5432',localPortNumber='${portNumber}'" --cli-connect-timeout 0`
108
- const portForwardingProcess = spawn('sh', ['-c', `${awsVaultExecCommand.join(' ')} ${portForwardingCommand}`])
109
-
110
- portForwardingProcess.stdout.on('data', (data) => {
111
- console.log(data.toString().trim())
112
- })
113
-
114
- portForwardingProcess.stderr.on('data', (data) => {
115
- console.error(`Command execution error: ${data.toString()}`)
116
- })
117
- })
118
-
119
- rdsEndpointProcess.stderr.on('data', (data) => {
120
- console.error(`Command execution error: ${data.toString()}`)
121
- })
122
- })
123
-
124
- instanceIdProcess.stderr.on('data', (data) => {
125
- console.error(`Command execution error: ${data.toString()}`)
126
- })
127
- })
128
-
129
- secretsGetProcess.stderr.on('data', (data) => {
130
- console.error(`Command execution error: ${data.toString()}`)
131
- })
90
+ if (!RDS_ENDPOINT) {
91
+ console.error('Failed to find the RDS endpoint.')
92
+ return
93
+ }
94
+
95
+ const portForwardingCommand = `aws-vault exec ${ENV} -- aws ssm start-session --target ${INSTANCE_ID} --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters "host=${RDS_ENDPOINT},portNumber='5432',localPortNumber='${portNumber}'" --cli-connect-timeout 0`
96
+
97
+ console.log('Starting port forwarding session...')
98
+ const child = exec(portForwardingCommand)
99
+
100
+ child.stdout.on('data', (data) => {
101
+ console.log(data.toString())
132
102
  })
133
103
 
134
- secretsDescribeProcess.stderr.on('data', (data) => {
135
- console.error(`Command execution error: ${data.toString()}`)
104
+ child.stderr.on('data', (data) => {
105
+ console.error(data.toString())
136
106
  })
107
+
108
+ child.on('close', (code) => {
109
+ console.log(`Port forwarding session ended with code ${code}`)
110
+ })
111
+
112
+ console.log('Port forwarding session established. You can now connect to the database using the provided connection string.')
113
+ console.log('Press Ctrl+C to end the session.')
114
+ } catch (error) {
115
+ console.error(`Error: ${error.message}`)
116
+ throw error // Re-throw the error to be caught by the outer catch block
117
+ }
118
+ }
119
+
120
+ main().catch(error => {
121
+ console.error('Unhandled error in main function:', error)
122
+ console.error('Exiting due to unhandled error')
123
+ setImmediate(() => {
124
+ throw new Error('Forcing exit due to unhandled error')
137
125
  })
126
+ })
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "rds_ssm_connect",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@aws-sdk/client-ec2": "^3.495.0",
7
- "@aws-sdk/client-rds": "^3.495.0",
8
- "@aws-sdk/client-ssm": "^3.495.0",
9
- "inquirer": "^8.2.6"
6
+ "@aws-sdk/client-ec2": "^3.632.0",
7
+ "@aws-sdk/client-rds": "^3.632.0",
8
+ "@aws-sdk/client-ssm": "^3.632.0",
9
+ "glob": "^11.0.0",
10
+ "inquirer": "^8.2.6",
11
+ "process": "^0.11.10",
12
+ "rimraf": "^6.0.1"
10
13
  },
11
14
  "bin": {
12
15
  "rds_ssm_connect": "./connect.js"
@@ -16,7 +19,7 @@
16
19
  "lint:fix": "eslint . --fix"
17
20
  },
18
21
  "devDependencies": {
19
- "eslint": "^8.56.0",
22
+ "eslint": "^8.57.0",
20
23
  "eslint-config-standard": "^17.1.0",
21
24
  "eslint-plugin-import": "^2.29.1",
22
25
  "eslint-plugin-n": "^16.6.2",