switch-profile 1.1.0 → 2.0.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.
- package/README.md +177 -94
- package/dist/cli.js +7921 -0
- package/index.js +12 -409
- package/package.json +31 -25
- package/.eslintrc.json +0 -39
- package/CHANGELOG.md +0 -120
- package/docs/architecture.md +0 -199
- package/docs/aws-profile-management.md +0 -325
- package/docs/cli-interface.md +0 -304
- package/docs/configuration-files.md +0 -240
- package/docs/development-guide.md +0 -168
- package/docs/gotchas.md +0 -82
- package/skills-lock.json +0 -73
- package/src/aws/index.js +0 -574
- package/src/aws/regions.js +0 -103
- package/src/core.js +0 -64
- package/src/fileHelper.js +0 -433
package/src/aws/index.js
DELETED
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
const { error: { catchErrors, wrapErrors }, promise: { delay } } = require('puffy')
|
|
2
|
-
const { homedir, EOL } = require('os')
|
|
3
|
-
const { join } = require('path')
|
|
4
|
-
const { exec, isCommandExist } = require('../core')
|
|
5
|
-
const { spawn } = require('child_process')
|
|
6
|
-
const fileHelper = require('../fileHelper')
|
|
7
|
-
const regions = require('./regions')
|
|
8
|
-
|
|
9
|
-
const IS_WINDOWS = process.platform === 'win32'
|
|
10
|
-
const LINE_SEP = IS_WINDOWS ? /\r?\n/ : EOL
|
|
11
|
-
const NL = IS_WINDOWS ? '\n' : EOL
|
|
12
|
-
const SSO_GET_CREDS_TIMEOUT = 5*60*1000 // 5 minutes to complete the SSO login
|
|
13
|
-
const AWS_CONFIG_FILE = join(homedir(), '.aws', 'config')
|
|
14
|
-
const AWS_CREDS_FILE = join(homedir(), '.aws', 'credentials')
|
|
15
|
-
const AWS_SSO_FOLDER = join(homedir(), '.aws', 'sso', 'cache')
|
|
16
|
-
const AWS_CLI_FOLDER = join(homedir(), '.aws', 'cli', 'cache')
|
|
17
|
-
const DEFAULT_CONFIG =
|
|
18
|
-
`[default]
|
|
19
|
-
region = ap-southeast-1
|
|
20
|
-
output = json
|
|
21
|
-
|
|
22
|
-
`
|
|
23
|
-
const DEFAULT_CREDS =
|
|
24
|
-
`[default]
|
|
25
|
-
aws_access_key_id = 1234_DUMMY
|
|
26
|
-
aws_secret_access_key = 4567_DUMMY
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
const awsExists = isCommandExist('aws')
|
|
31
|
-
const awsCliV2Exists = noFailIfMissing => catchErrors((async () => {
|
|
32
|
-
const [awsCliErrors] = await awsExists()
|
|
33
|
-
if (awsCliErrors) {
|
|
34
|
-
if (noFailIfMissing)
|
|
35
|
-
return false
|
|
36
|
-
throw wrapErrors('AWS CLI error', awsCliErrors)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const data = await exec('aws --version') || ''
|
|
41
|
-
const [majorVersion] = ((data.match(/aws-cli\/(.*?)\s/g)||[])[0]||'').replace('aws-cli/','').split('.')
|
|
42
|
-
const version = majorVersion*1
|
|
43
|
-
if (isNaN(version))
|
|
44
|
-
throw new Error('Fail to test the AWS CLI version. Please try to run "aws --version" manually to try to debug this issue.')
|
|
45
|
-
if (version <= 1)
|
|
46
|
-
throw new Error(`AWS CLI version ${version} is not supported. Please upgrade to AWS CLI v2 or greater.`)
|
|
47
|
-
|
|
48
|
-
return true
|
|
49
|
-
} catch(err) {
|
|
50
|
-
throw wrapErrors('Fail to check AWS CLI\'s version', [err])
|
|
51
|
-
}
|
|
52
|
-
})())
|
|
53
|
-
|
|
54
|
-
const getParam = (params, paramName) => {
|
|
55
|
-
if (!params || !params.length || !paramName)
|
|
56
|
-
return null
|
|
57
|
-
|
|
58
|
-
const regexp = new RegExp(`\\s*${paramName}\\s*=\\s*`)
|
|
59
|
-
return params.filter(p => regexp.test(p)).map(p => p.replace(regexp,'').trim())[0]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const getCredsFile = () => catchErrors((async () => {
|
|
63
|
-
const credsExist = await fileHelper.exists(AWS_CREDS_FILE)
|
|
64
|
-
if (!credsExist)
|
|
65
|
-
return ''
|
|
66
|
-
const credsStr = (await fileHelper.read(AWS_CREDS_FILE) || '').toString()
|
|
67
|
-
|
|
68
|
-
return credsStr
|
|
69
|
-
})())
|
|
70
|
-
|
|
71
|
-
const getConfigFile = () => catchErrors((async () => {
|
|
72
|
-
const credsExist = await fileHelper.exists(AWS_CONFIG_FILE)
|
|
73
|
-
if (!credsExist)
|
|
74
|
-
return ''
|
|
75
|
-
const credsStr = (await fileHelper.read(AWS_CONFIG_FILE) || '').toString()
|
|
76
|
-
|
|
77
|
-
return credsStr
|
|
78
|
-
})())
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Gets all the profiles stored in the ~/.aws/config file.
|
|
82
|
-
*
|
|
83
|
-
* @return {[Error]}
|
|
84
|
-
* @return {String} profiles[].name e.g., 'hello'
|
|
85
|
-
* @return {String} profiles[].friendlyName e.g., 'hello (SSO [role:"god" account:1234])'
|
|
86
|
-
* @return {String} profiles[].region e.g., 'us-weat-1'
|
|
87
|
-
* @return {String} profiles[].output e.g., 'json'
|
|
88
|
-
* @return {String} profiles[].sso_start_url e.g., 'https://cloudless.awsapps.com/start'
|
|
89
|
-
* @return {String} profiles[].sso_region e.g., 'us-weat-1'
|
|
90
|
-
* @return {String} profiles[].sso_account_id e.g., '1234'
|
|
91
|
-
* @return {String} profiles[].sso_role_name e.g., 'god'
|
|
92
|
-
*/
|
|
93
|
-
const listProfiles = () => catchErrors((async () => {
|
|
94
|
-
await awsCliV2Exists()
|
|
95
|
-
const [configStrErrors, configStr] = await getConfigFile()
|
|
96
|
-
if (configStrErrors)
|
|
97
|
-
throw wrapErrors('Fail to list AWS profiles', configStrErrors)
|
|
98
|
-
|
|
99
|
-
if (!configStr)
|
|
100
|
-
return []
|
|
101
|
-
|
|
102
|
-
const profiles = configStr.match(/\[(.*?)\]/g)
|
|
103
|
-
return profiles.map(profile => {
|
|
104
|
-
const [,rest=''] = configStr.split(profile)
|
|
105
|
-
const [config=''] = rest.split('[')
|
|
106
|
-
const params = config.split(LINE_SEP)
|
|
107
|
-
const p = {
|
|
108
|
-
name: profile.replace(/(^\[profile\s+|\[|\])/g, ''),
|
|
109
|
-
sso_start_url: getParam(params, 'sso_start_url'),
|
|
110
|
-
sso_region: getParam(params, 'sso_region'),
|
|
111
|
-
sso_account_id: getParam(params, 'sso_account_id'),
|
|
112
|
-
sso_role_name: getParam(params, 'sso_role_name'),
|
|
113
|
-
sso_session: getParam(params, 'sso_session'),
|
|
114
|
-
region: getParam(params, 'region'),
|
|
115
|
-
output: getParam(params, 'output')
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Handle newer AWS CLI v2 SSO session format where sso_start_url is in a separate [sso-session] section
|
|
119
|
-
if (p.sso_session && !p.sso_start_url) {
|
|
120
|
-
const ssoSessionSection = `[sso-session ${p.sso_session}]`
|
|
121
|
-
const ssoSessionMatch = configStr.split(ssoSessionSection)
|
|
122
|
-
if (ssoSessionMatch.length > 1) {
|
|
123
|
-
const [ssoSessionRest=''] = ssoSessionMatch[1].split('[')
|
|
124
|
-
const ssoSessionParams = ssoSessionRest.split(LINE_SEP)
|
|
125
|
-
p.sso_start_url = getParam(ssoSessionParams, 'sso_start_url')
|
|
126
|
-
// If sso_region is not in profile, check the session section
|
|
127
|
-
if (!p.sso_region) {
|
|
128
|
-
p.sso_region = getParam(ssoSessionParams, 'sso_region')
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (p.sso_region)
|
|
134
|
-
p.region = p.sso_region
|
|
135
|
-
|
|
136
|
-
p.friendlyName = `${p.name}${p.sso_start_url ? ` (SSO [role:${p.sso_role_name||'unknown'} - account:${p.sso_account_id||'unknown'}])` : ''}`
|
|
137
|
-
|
|
138
|
-
return p
|
|
139
|
-
}).filter(p => p.name != 'default' && !p.name.startsWith('sso-session '))
|
|
140
|
-
})())
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Gets the session details associated with this 'ssoUrl' from the local folder '.aws/sso/cache/'.
|
|
144
|
-
* If the session exists but has expired, null is returned.
|
|
145
|
-
*
|
|
146
|
-
* @param {String} ssoUrl e.g., 'https://cloudless.awsapps.com/start'
|
|
147
|
-
*
|
|
148
|
-
* @return {[Error]}
|
|
149
|
-
* @return {String} session.startUrl e.g., 'https://cloudless.awsapps.com/start'
|
|
150
|
-
* @return {String} session.region e.g., 'ap-southeast-2'
|
|
151
|
-
* @return {String} session.accessToken e.g., 'dwqdwdwqd...dwqdqw'
|
|
152
|
-
* @return {String} session.expiresAt e.g., '2021-07-17T11:33:12Z'
|
|
153
|
-
*/
|
|
154
|
-
const getSsoSession = ssoUrl => catchErrors((async () => {
|
|
155
|
-
const ssoHost = new URL(ssoUrl).host
|
|
156
|
-
if (!ssoHost)
|
|
157
|
-
throw new Error(`The SSO portal URL ${ssoUrl} is not a valid URL.`)
|
|
158
|
-
|
|
159
|
-
const ssoFolderExists = await fileHelper.exists(AWS_SSO_FOLDER)
|
|
160
|
-
if (!ssoFolderExists)
|
|
161
|
-
throw new Error(`AWS SSO folder ${AWS_SSO_FOLDER} not found.`)
|
|
162
|
-
const files = await fileHelper.list(AWS_SSO_FOLDER, { pattern:'*.json' })
|
|
163
|
-
if (!files || !files.length)
|
|
164
|
-
throw new Error(`AWS SSO folder ${AWS_SSO_FOLDER} contains no credentials.`)
|
|
165
|
-
let creds = null
|
|
166
|
-
for (let i=0;i<files.length;i++) {
|
|
167
|
-
const _creds = await fileHelper.json.get(files[i])
|
|
168
|
-
if (_creds.startUrl && _creds.expiresAt) {
|
|
169
|
-
const credHost = new URL(_creds.startUrl).host
|
|
170
|
-
const credStillValid = (Date.now() - 2*60*1000) < new Date(_creds.expiresAt).getTime()
|
|
171
|
-
if (credHost == ssoHost && credStillValid && _creds.accessToken) {
|
|
172
|
-
creds = _creds
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return creds
|
|
179
|
-
})())
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Gets the credentials details associated with the 'access_key_end' and 'secret_key_end' from the local folder '.aws/cli/cache/'.
|
|
183
|
-
* We need this function because unfortunatelly, the 'aws configure list' does not output the details in full. Instead, it stores
|
|
184
|
-
* them in the cache file.
|
|
185
|
-
*
|
|
186
|
-
* @param {String} access_key_end e.g., '1234'
|
|
187
|
-
* @param {String} secret_key_end e.g., '5678'
|
|
188
|
-
*
|
|
189
|
-
* @return {[Error]}
|
|
190
|
-
* @return {String} session.aws_access_key_id e.g., '********1234'
|
|
191
|
-
* @return {String} session.aws_secret_access_key e.g., '********5678'
|
|
192
|
-
* @return {String} session.aws_session_token e.g., 'dwqdwdwqd...dwqdqw'
|
|
193
|
-
* @return {Date} session.expiry_date e.g., 2021-07-17T11:33:12Z
|
|
194
|
-
*/
|
|
195
|
-
const getSsoCredsFromCacheFile = (access_key_end, secret_key_end) => catchErrors((async () => {
|
|
196
|
-
const errMsg = `Fail to get the local CLI credentials from folder ${AWS_CLI_FOLDER}`
|
|
197
|
-
if (!access_key_end)
|
|
198
|
-
throw wrapErrors(errMsg, [new Error('Missing required argument \'access_key_end\'.')])
|
|
199
|
-
if (!secret_key_end)
|
|
200
|
-
throw wrapErrors(errMsg, [new Error('Missing required argument \'secret_key_end\'.')])
|
|
201
|
-
|
|
202
|
-
const folderExists = await fileHelper.exists(AWS_CLI_FOLDER)
|
|
203
|
-
if (!folderExists)
|
|
204
|
-
return null
|
|
205
|
-
const files = await fileHelper.list(AWS_CLI_FOLDER, { pattern:'*.json' })
|
|
206
|
-
if (!files || !files.length)
|
|
207
|
-
return null
|
|
208
|
-
|
|
209
|
-
for (let i=0;i<files.length;i++) {
|
|
210
|
-
const _creds = await fileHelper.json.get(files[i])
|
|
211
|
-
|
|
212
|
-
// Creds must be SSO
|
|
213
|
-
if (!_creds || _creds.ProviderType != 'sso' || !_creds.Credentials)
|
|
214
|
-
continue
|
|
215
|
-
|
|
216
|
-
// Creds must still be valid for the next 2 minutes
|
|
217
|
-
if (_creds.Credentials.Expiration && (Date.now() - 2*60*1000) < new Date(_creds.Credentials.Expiration).getTime()) {
|
|
218
|
-
const credsMatch = (_creds.Credentials.AccessKeyId||'').slice(-4) == access_key_end &&
|
|
219
|
-
(_creds.Credentials.SecretAccessKey||'').slice(-4) == secret_key_end
|
|
220
|
-
|
|
221
|
-
if (credsMatch)
|
|
222
|
-
return {
|
|
223
|
-
aws_access_key_id: _creds.Credentials.AccessKeyId,
|
|
224
|
-
aws_secret_access_key: _creds.Credentials.SecretAccessKey,
|
|
225
|
-
aws_session_token: _creds.Credentials.SessionToken,
|
|
226
|
-
expiry_date: new Date(_creds.Credentials.Expiration)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return null
|
|
232
|
-
})())
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Gets SSO credentials. It executes the 'aws configure list --profile <PROFILE>' command, which
|
|
236
|
-
* checks the local cache first (i.e., '~/.aws/cli/cache'). If that cache for that profile does not exist or if it
|
|
237
|
-
* is expired, it goes to AWS to fetch new ones and refresh the cache.
|
|
238
|
-
*
|
|
239
|
-
* @param {String} profile e.g., 'sso-dev-cloudless'
|
|
240
|
-
*
|
|
241
|
-
* @return {[Error]}
|
|
242
|
-
* @return {String} session.aws_access_key_id e.g., '********1234'
|
|
243
|
-
* @return {String} session.aws_secret_access_key e.g., '********5678'
|
|
244
|
-
* @return {String} session.aws_session_token e.g., 'dwqdwdwqd...dwqdqw'
|
|
245
|
-
* @return {Date} session.expiry_date e.g., 2021-07-17T11:33:12Z
|
|
246
|
-
*/
|
|
247
|
-
const getSsoCredentials = profile => catchErrors((async () => {
|
|
248
|
-
await awsCliV2Exists()
|
|
249
|
-
const errMsg = `Fail to get AWS SSO credentials for profile ${profile}`
|
|
250
|
-
try {
|
|
251
|
-
if (!profile)
|
|
252
|
-
throw wrapErrors(errMsg, [new Error('Missing required \'profile\' argument')])
|
|
253
|
-
const data = await exec(`aws configure list --profile ${profile}`) || ''
|
|
254
|
-
const access_key_end = ((data.match(/access_key\s*\*+.{4}/g)||[])[0]||'').slice(-4)
|
|
255
|
-
const secret_key_end = ((data.match(/secret_key\s*\*+.{4}/g)||[])[0]||'').slice(-4)
|
|
256
|
-
|
|
257
|
-
if (!access_key_end || !secret_key_end)
|
|
258
|
-
return null
|
|
259
|
-
|
|
260
|
-
const [ssoCredsErrors, creds] = await getSsoCredsFromCacheFile(access_key_end, secret_key_end)
|
|
261
|
-
|
|
262
|
-
if (ssoCredsErrors)
|
|
263
|
-
throw wrapErrors(errMsg, ssoCredsErrors)
|
|
264
|
-
return creds
|
|
265
|
-
} catch(err) {
|
|
266
|
-
throw wrapErrors(errMsg, [err])
|
|
267
|
-
}
|
|
268
|
-
})())
|
|
269
|
-
|
|
270
|
-
const isSSOexpired = ssoSession => {
|
|
271
|
-
if (!ssoSession)
|
|
272
|
-
return true
|
|
273
|
-
|
|
274
|
-
return (new Date(ssoSession.expiresAt).getTime() - Date.now()) < (2*60*1000)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Makes sure the profile is using a valid (i.e., exists and not expired) local SSO session. If not, try
|
|
279
|
-
* to create one manually by redirecting the user to the SSO page.
|
|
280
|
-
*
|
|
281
|
-
* @param {String} profile e.g., 'sso-dev-cloudless'
|
|
282
|
-
* @param {String} ssoUrl e.g., 'https://cloudless.awsapps.com/start'
|
|
283
|
-
* @param {Boolean} options.force
|
|
284
|
-
*
|
|
285
|
-
* @return {[Error]}
|
|
286
|
-
* @return {String} session.aws_access_key_id e.g., '********1234'
|
|
287
|
-
* @return {String} session.aws_secret_access_key e.g., '********5678'
|
|
288
|
-
* @return {String} session.aws_session_token e.g., 'dwqdwdwqd...dwqdqw'
|
|
289
|
-
* @return {Date} session.expiry_date e.g., 2021-07-17T11:33:12Z
|
|
290
|
-
*/
|
|
291
|
-
const refreshSsoSession = (profile, ssoUrl, options) => catchErrors((async () => {
|
|
292
|
-
const errMsg = `Fail to refresh the SSO session for AWS profile ${profile}`
|
|
293
|
-
let [ssoSessionErrors, ssoSession] = await getSsoSession(ssoUrl)
|
|
294
|
-
if (ssoSessionErrors)
|
|
295
|
-
throw wrapErrors(errMsg, ssoSessionErrors)
|
|
296
|
-
|
|
297
|
-
const { force } = options || {}
|
|
298
|
-
|
|
299
|
-
try {
|
|
300
|
-
// No valid SSO session found. Manually get a new one via the SSO portal
|
|
301
|
-
const startTime = Date.now()
|
|
302
|
-
if (force || isSSOexpired(ssoSession)) {
|
|
303
|
-
await exec(`aws sso login --profile ${profile}`)
|
|
304
|
-
while (!ssoSession && Date.now() - startTime < SSO_GET_CREDS_TIMEOUT) {
|
|
305
|
-
const resp = await getSsoSession(ssoUrl)
|
|
306
|
-
if (resp[0])
|
|
307
|
-
throw wrapErrors(errMsg, resp[0])
|
|
308
|
-
ssoSession = resp[1]
|
|
309
|
-
if (!ssoSession)
|
|
310
|
-
await delay(2000)
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (ssoSession)
|
|
315
|
-
return ssoSession
|
|
316
|
-
else if (Date.now() - startTime > SSO_GET_CREDS_TIMEOUT)
|
|
317
|
-
throw wrapErrors(errMsg, [new Error(`Timeout - Time to wait for refreshing the SSO session for profile ${profile} exceeded ${SSO_GET_CREDS_TIMEOUT}ms.`)])
|
|
318
|
-
else
|
|
319
|
-
return null
|
|
320
|
-
} catch(err) {
|
|
321
|
-
throw wrapErrors(errMsg, [err])
|
|
322
|
-
}
|
|
323
|
-
})())
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Gets the AWS credentials for a specific profile. If that profile is an SSO profile, this function has a series of
|
|
327
|
-
* side-effects:
|
|
328
|
-
* - If the local SSO session stored under ~/.aws/sso/cache has expired, then it will redirect the user to the SSO portal and eventually refresh that ~/.aws/sso/cache.
|
|
329
|
-
* - If the local SSO creds stored under ~/.aws/cli/cache have expired (AWS_KEY, AWS_SECRET, AWS_SESSION), then they will be refreshed using the session stored under the ~/.aws/sso/cache.
|
|
330
|
-
*
|
|
331
|
-
* @param {String} profile e.g., 'sso-dev-cloudless'
|
|
332
|
-
* @param {String} ssoUrl e.g., 'https://cloudless.awsapps.com/start'
|
|
333
|
-
*
|
|
334
|
-
* @return {[Error]}
|
|
335
|
-
* @return {String} creds.aws_access_key_id e.g., '********1234'
|
|
336
|
-
* @return {String} creds.aws_secret_access_key e.g., '********5678'
|
|
337
|
-
* @return {String} creds.aws_session_token e.g., 'dwqdwdwqd...dwqdqw'
|
|
338
|
-
* @return {Date} creds.expiry_date e.g., 2021-07-17T11:33:12Z
|
|
339
|
-
*/
|
|
340
|
-
const getCredentials = (profile, ssoUrl) => catchErrors((async () => {
|
|
341
|
-
await awsCliV2Exists()
|
|
342
|
-
const errMsg = `Fail to get AWS credentials for profile ${profile}`
|
|
343
|
-
if (ssoUrl) {
|
|
344
|
-
await refreshSsoSession(profile, ssoUrl)
|
|
345
|
-
let [ssoCredsErrors, ssoCreds] = await getSsoCredentials(profile)
|
|
346
|
-
if (ssoCredsErrors) {
|
|
347
|
-
const ssoSessionIsInvalid = ssoCredsErrors.some(e => e.message && e.message.toLowerCase().indexOf('session associated with this profile has expired') >= 0)
|
|
348
|
-
if (ssoSessionIsInvalid) {
|
|
349
|
-
await refreshSsoSession(profile, ssoUrl, { force:true })
|
|
350
|
-
const resp = await getSsoCredentials(profile)
|
|
351
|
-
if (resp[0])
|
|
352
|
-
throw wrapErrors(errMsg, resp[0])
|
|
353
|
-
|
|
354
|
-
ssoCreds = resp[1]
|
|
355
|
-
} else
|
|
356
|
-
throw wrapErrors(errMsg, ssoCredsErrors)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (!ssoCreds) {
|
|
360
|
-
try {
|
|
361
|
-
const data = await exec(`aws configure export-credentials --profile ${profile} --format process`)
|
|
362
|
-
const parsed = JSON.parse(data)
|
|
363
|
-
if (parsed.AccessKeyId && parsed.SecretAccessKey) {
|
|
364
|
-
ssoCreds = {
|
|
365
|
-
aws_access_key_id: parsed.AccessKeyId,
|
|
366
|
-
aws_secret_access_key: parsed.SecretAccessKey,
|
|
367
|
-
aws_session_token: parsed.SessionToken,
|
|
368
|
-
expiry_date: parsed.Expiration ? new Date(parsed.Expiration) : null
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
} catch(e) {
|
|
372
|
-
// Fallback also failed
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (!ssoCreds)
|
|
377
|
-
throw new Error(errMsg)
|
|
378
|
-
|
|
379
|
-
return ssoCreds
|
|
380
|
-
} else {
|
|
381
|
-
const [errors, credsStr] = await getCredsFile()
|
|
382
|
-
if (errors)
|
|
383
|
-
throw wrapErrors(errMsg, errors)
|
|
384
|
-
|
|
385
|
-
const [, rest] = credsStr.split(`[${profile}]`)
|
|
386
|
-
if (!rest)
|
|
387
|
-
throw new Error(`Profile ${profile} not found in ${AWS_CREDS_FILE}. Standard profiles must have credentials defined in this file.`)
|
|
388
|
-
|
|
389
|
-
const [config=''] = rest.split('[')
|
|
390
|
-
const params = config.split(LINE_SEP)
|
|
391
|
-
const creds = {
|
|
392
|
-
aws_access_key_id: getParam(params, 'aws_access_key_id'),
|
|
393
|
-
aws_secret_access_key: getParam(params, 'aws_secret_access_key'),
|
|
394
|
-
aws_session_token: getParam(params, 'aws_session_token'),
|
|
395
|
-
expiry_date: null
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return creds
|
|
399
|
-
}
|
|
400
|
-
})())
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const updateDefaultProfile = ({ profile, region, expiry_date, aws_access_key_id, aws_secret_access_key, aws_session_token }) => catchErrors((async () => {
|
|
404
|
-
const errMsg = `Fail to update the ${AWS_CREDS_FILE} file`
|
|
405
|
-
const [configStrErrors, configStr] = await getConfigFile()
|
|
406
|
-
const [credsStrErrors, credsStr] = await getCredsFile()
|
|
407
|
-
if (credsStrErrors||configStrErrors)
|
|
408
|
-
throw wrapErrors(errMsg, credsStrErrors||configStrErrors)
|
|
409
|
-
|
|
410
|
-
const newDefaultCreds = '[default]'+NL+
|
|
411
|
-
`aws_access_key_id = ${aws_access_key_id}`+NL+
|
|
412
|
-
`aws_secret_access_key = ${aws_secret_access_key}`+NL+
|
|
413
|
-
(aws_session_token ? `aws_session_token = ${aws_session_token}`+NL : '') +
|
|
414
|
-
(expiry_date ? `expiry_date = ${expiry_date.toISOString()}`+NL : '') +
|
|
415
|
-
`profile = ${profile}`+NL+NL
|
|
416
|
-
|
|
417
|
-
const newDefaultConfig = '[default]'+NL+
|
|
418
|
-
`region = ${region}`+NL+
|
|
419
|
-
'output = json'+NL+NL
|
|
420
|
-
|
|
421
|
-
const defaultCredsSection = (credsStr.match(/\[default\]((.|\n|\r)*?)(\[|$)/)||[])[0]
|
|
422
|
-
const defaultConfigSection = (configStr.match(/\[default\]((.|\n|\r)*?)(\[|$)/)||[])[0]
|
|
423
|
-
|
|
424
|
-
let updatedCreds = ''
|
|
425
|
-
if (defaultCredsSection) {
|
|
426
|
-
const lastChar = defaultCredsSection.slice(-1)
|
|
427
|
-
updatedCreds = credsStr.replace(defaultCredsSection, newDefaultCreds+lastChar)
|
|
428
|
-
} else
|
|
429
|
-
updatedCreds = newDefaultCreds + credsStr
|
|
430
|
-
|
|
431
|
-
let updatedConfig = ''
|
|
432
|
-
if (defaultConfigSection) {
|
|
433
|
-
const lastChar = defaultConfigSection.slice(-1)
|
|
434
|
-
updatedConfig = configStr.replace(defaultConfigSection, newDefaultConfig+lastChar)
|
|
435
|
-
} else
|
|
436
|
-
updatedConfig = newDefaultConfig + configStr
|
|
437
|
-
|
|
438
|
-
await fileHelper.write(AWS_CREDS_FILE, updatedCreds)
|
|
439
|
-
await fileHelper.write(AWS_CONFIG_FILE, updatedConfig)
|
|
440
|
-
})())
|
|
441
|
-
|
|
442
|
-
const getDefaultProfile = () => catchErrors((async () => {
|
|
443
|
-
const errMsg = `Fail to get the default AWS profile in the ${AWS_CREDS_FILE} file.`
|
|
444
|
-
const [errors, credsStr] = await getCredsFile()
|
|
445
|
-
if (errors)
|
|
446
|
-
throw wrapErrors(errMsg, errors)
|
|
447
|
-
|
|
448
|
-
const params = ((credsStr.match(/\[default\]((.|\n|\r)*?)(\[|$)/)||[])[0]||'').split(LINE_SEP)
|
|
449
|
-
const creds = {
|
|
450
|
-
aws_access_key_id: getParam(params, 'aws_access_key_id'),
|
|
451
|
-
aws_secret_access_key: getParam(params, 'aws_secret_access_key'),
|
|
452
|
-
aws_session_token: getParam(params, 'aws_session_token'),
|
|
453
|
-
expiry_date: getParam(params, 'expiry_date'),
|
|
454
|
-
profile: getParam(params, 'profile')
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return creds
|
|
458
|
-
})())
|
|
459
|
-
|
|
460
|
-
const deleteProfileFromConfig = (profile, fileContent) => {
|
|
461
|
-
fileContent = fileContent || ''
|
|
462
|
-
const regExp = new RegExp(`\\[(profile\\s){0,1}${profile}\\]((.|\\n|\\r)*?)(\\[|$)`)
|
|
463
|
-
const profileMatch = (fileContent.match(regExp)||[])[0] || ''
|
|
464
|
-
|
|
465
|
-
if (!profileMatch)
|
|
466
|
-
return fileContent
|
|
467
|
-
|
|
468
|
-
const lastChar = profileMatch.slice(-1)
|
|
469
|
-
return fileContent.replace(profileMatch, lastChar)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const deleteProfileFromCreds = (profile, fileContent) => {
|
|
473
|
-
fileContent = fileContent || ''
|
|
474
|
-
const regExp = new RegExp(`\\[${profile}\\]((.|\\n|\\r)*?)(\\[|$)`)
|
|
475
|
-
const profileMatch = (fileContent.match(regExp)||[])[0] || ''
|
|
476
|
-
|
|
477
|
-
if (!profileMatch)
|
|
478
|
-
return fileContent
|
|
479
|
-
|
|
480
|
-
const lastChar = profileMatch.slice(-1)
|
|
481
|
-
return fileContent.replace(profileMatch, lastChar)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const deleteProfiles = profiles => catchErrors((async () => {
|
|
485
|
-
if (!profiles || !profiles.length)
|
|
486
|
-
return
|
|
487
|
-
|
|
488
|
-
const errMsg = 'Fail to delete AWS profiles'
|
|
489
|
-
|
|
490
|
-
if (profiles.some(p => p == 'default'))
|
|
491
|
-
throw wrapErrors(errMsg, [new Error('The \'default\' profile cannot be deleted.')])
|
|
492
|
-
|
|
493
|
-
let [configStrErrors, configStr] = await getConfigFile()
|
|
494
|
-
let [credsStrErrors, credsStr] = await getCredsFile()
|
|
495
|
-
|
|
496
|
-
if (configStrErrors || credsStrErrors)
|
|
497
|
-
throw wrapErrors(errMsg, configStrErrors || credsStrErrors)
|
|
498
|
-
|
|
499
|
-
const updateConfig = configStr
|
|
500
|
-
const updateCreds = credsStr
|
|
501
|
-
|
|
502
|
-
for (let i=0;i<profiles.length;i++) {
|
|
503
|
-
const profile = profiles[i]
|
|
504
|
-
configStr = deleteProfileFromConfig(profile, configStr)
|
|
505
|
-
credsStr = deleteProfileFromCreds(profile, credsStr)
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (updateConfig)
|
|
509
|
-
await fileHelper.write(AWS_CONFIG_FILE, configStr)
|
|
510
|
-
if (updateCreds)
|
|
511
|
-
await fileHelper.write(AWS_CREDS_FILE, credsStr)
|
|
512
|
-
})())
|
|
513
|
-
|
|
514
|
-
const createProfile = ({ name, aws_access_key_id, aws_secret_access_key, region }) => catchErrors((async () => {
|
|
515
|
-
await awsCliV2Exists()
|
|
516
|
-
|
|
517
|
-
const errMsg = 'Fail to create AWS profile'
|
|
518
|
-
if (!name)
|
|
519
|
-
throw wrapErrors(errMsg, [new Error('Missing required argument \'name\'.')])
|
|
520
|
-
if (!region)
|
|
521
|
-
throw wrapErrors(errMsg, [new Error('Missing required argument \'region\'.')])
|
|
522
|
-
if (!aws_access_key_id)
|
|
523
|
-
throw wrapErrors(errMsg, [new Error('Missing required argument \'aws_access_key_id\'.')])
|
|
524
|
-
if (!aws_secret_access_key)
|
|
525
|
-
throw wrapErrors(errMsg, [new Error('Missing required argument \'aws_secret_access_key\'.')])
|
|
526
|
-
|
|
527
|
-
let [configStrErrors, configStr] = await getConfigFile()
|
|
528
|
-
let [credsStrErrors, credsStr] = await getCredsFile()
|
|
529
|
-
if (configStrErrors||credsStrErrors)
|
|
530
|
-
throw wrapErrors(errMsg, configStrErrors||credsStrErrors)
|
|
531
|
-
|
|
532
|
-
const configProfiles = [`[profile ${name}]`+NL]
|
|
533
|
-
const credsProfiles = [`[${name}]`+NL]
|
|
534
|
-
|
|
535
|
-
credsProfiles.push(`aws_access_key_id = ${aws_access_key_id}`+NL)
|
|
536
|
-
credsProfiles.push(`aws_secret_access_key = ${aws_secret_access_key}`+NL+NL)
|
|
537
|
-
|
|
538
|
-
configProfiles.push(`region = ${region}`+NL)
|
|
539
|
-
configProfiles.push('output = json'+NL+NL)
|
|
540
|
-
|
|
541
|
-
const newCreds = credsProfiles.join('')
|
|
542
|
-
const newConfig = configProfiles.join('')
|
|
543
|
-
|
|
544
|
-
if (!configStr)
|
|
545
|
-
configStr = DEFAULT_CONFIG
|
|
546
|
-
if (!credsStr)
|
|
547
|
-
credsStr = DEFAULT_CREDS
|
|
548
|
-
|
|
549
|
-
await fileHelper.write(AWS_CONFIG_FILE, configStr+NL+newConfig)
|
|
550
|
-
await fileHelper.write(AWS_CREDS_FILE, credsStr+NL+newCreds)
|
|
551
|
-
})())
|
|
552
|
-
|
|
553
|
-
const createSsoProfile = name => catchErrors((async () => {
|
|
554
|
-
await awsCliV2Exists()
|
|
555
|
-
const exitCode = await new Promise(next => {
|
|
556
|
-
const child = spawn('aws', ['configure', 'sso', '--profile', name], { stdio: 'inherit', ...(IS_WINDOWS ? { shell: true } : {}) })
|
|
557
|
-
child.on('exit', code => next(code))
|
|
558
|
-
})
|
|
559
|
-
if (exitCode !== 0)
|
|
560
|
-
throw new Error(`'aws configure sso' exited with code ${exitCode}. The SSO profile may not have been created correctly.`)
|
|
561
|
-
})())
|
|
562
|
-
|
|
563
|
-
module.exports = {
|
|
564
|
-
listProfiles,
|
|
565
|
-
getCredentials,
|
|
566
|
-
getDefaultProfile,
|
|
567
|
-
updateDefaultProfile,
|
|
568
|
-
deleteProfiles,
|
|
569
|
-
createProfile,
|
|
570
|
-
createSsoProfile,
|
|
571
|
-
regions,
|
|
572
|
-
awsCliV2Exists
|
|
573
|
-
}
|
|
574
|
-
|
package/src/aws/regions.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
module.exports = [
|
|
2
|
-
{
|
|
3
|
-
name: 'US East (Ohio)',
|
|
4
|
-
code: 'us-east-2'
|
|
5
|
-
},
|
|
6
|
-
{
|
|
7
|
-
name: 'US East (N. Virginia)',
|
|
8
|
-
code: 'us-east-1'
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
name: 'US West (N. California)',
|
|
12
|
-
code: 'us-west-1'
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
name: 'US West (Oregon)',
|
|
16
|
-
code: 'us-west-2'
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
name: 'Africa (Cape Town)',
|
|
20
|
-
code: 'af-south-1'
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
name: 'Asia Pacific (Hong Kong)',
|
|
24
|
-
code: 'ap-east-1'
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: 'Asia Pacific (Mumbai)',
|
|
28
|
-
code: 'ap-south-1'
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: 'Asia Pacific (Osaka-Local)',
|
|
32
|
-
code: 'ap-northeast-3'
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: 'Asia Pacific (Seoul)',
|
|
36
|
-
code: 'ap-northeast-2'
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'Asia Pacific (Singapore)',
|
|
40
|
-
code: 'ap-southeast-1'
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: 'Asia Pacific (Sydney)',
|
|
44
|
-
code: 'ap-southeast-2'
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
name: 'Asia Pacific (Tokyo)',
|
|
48
|
-
code: 'ap-northeast-1'
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: 'Canada (Central)',
|
|
52
|
-
code: 'ca-central-1'
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: 'China (Beijing)',
|
|
56
|
-
code: 'cn-north-1'
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: 'China (Ningxia)',
|
|
60
|
-
code: 'cn-northwest-1'
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: 'EU (Frankfurt)',
|
|
64
|
-
code: 'eu-central-1'
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: 'EU (Ireland)',
|
|
68
|
-
code: 'eu-west-1'
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: 'EU (London)',
|
|
72
|
-
code: 'eu-west-2'
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: 'EU (Milan)',
|
|
76
|
-
code: 'eu-south-1'
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: 'EU (Paris)',
|
|
80
|
-
code: 'eu-west-3'
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
name: 'EU (Stockholm)',
|
|
84
|
-
code: 'eu-north-1'
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: 'Middle East (Bahrain)',
|
|
88
|
-
code: 'me-south-1'
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: 'South America (Sao Paulo)',
|
|
92
|
-
code: 'sa-east-1'
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
name: 'AWS GovCloud (US-East)',
|
|
96
|
-
code: 'us-gov-east-1'
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: 'AWS GovCloud (US-West)',
|
|
100
|
-
code: 'us-gov-west-1'
|
|
101
|
-
}
|
|
102
|
-
]
|
|
103
|
-
|