From bf3cec2817516d5cab01216c64d2a44795e3389a Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Thu, 19 Oct 2023 15:33:42 -0700 Subject: [PATCH 01/17] =?UTF-8?q?`aws-sdk`=20=E2=86=92=20`@aws-lite/client?= =?UTF-8?q?`=20[Breaking=20change]=20Refactor=20AWS=20credential=20initial?= =?UTF-8?q?ization=20steps=20and=20mutation=20of=20`AWS=5FPROFILE`,=20`ARC?= =?UTF-8?q?=5FAWS=5FCREDS`=20Temporarily=20revert=20to=20`tap-spec`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- banner/_get-creds.js | 16 ++++ banner/cred-check.js | 44 +++++++++ banner/index.js | 21 +++-- banner/init-aws.js | 116 ----------------------- changelog.md | 11 +++ package.json | 8 +- test/banner/cred-check-test.js | 64 +++++++++++++ test/banner/init-aws-test.js | 162 --------------------------------- 8 files changed, 152 insertions(+), 290 deletions(-) create mode 100644 banner/_get-creds.js create mode 100644 banner/cred-check.js delete mode 100644 banner/init-aws.js create mode 100644 test/banner/cred-check-test.js delete mode 100644 test/banner/init-aws-test.js diff --git a/banner/_get-creds.js b/banner/_get-creds.js new file mode 100644 index 0000000..e1eaf37 --- /dev/null +++ b/banner/_get-creds.js @@ -0,0 +1,16 @@ +let awsLite = require('@aws-lite/client') +async function main () { + try { + let options = { autoloadPlugins: false, region: 'us-west-1' } + if (process.env._ARC_PROFILE) options.profile = process.env._ARC_PROFILE + await awsLite(options) + console.log(JSON.stringify({ ok: true })) + } + catch (err) { + console.log(JSON.stringify({ + error: err.message, + stack: err.stack, + })) + } +} +main() diff --git a/banner/cred-check.js b/banner/cred-check.js new file mode 100644 index 0000000..8cbe12b --- /dev/null +++ b/banner/cred-check.js @@ -0,0 +1,44 @@ +let { join } = require('path') + +/** + * Credential check and possible backstop + * - aws-lite requires credentials to initialize + * - Architect needs credentials for some things (e.g. Deploy), but also has a variety of offline workflows that interface with AWS service API emulators (e.g. Sandbox) + * - Thus, sometimes it's ok to use dummy creds, sometimes we need to halt + */ +module.exports = function credCheck ({ checkCreds = true, inventory, needsValidCreds = false }) { + if (!checkCreds) return + + // eslint-disable-next-line + let { execFileSync } = require('child_process') + let script = join(__dirname, '_get-creds.js') + function check () { + try { + let env = { ...process.env } + if (inventory.inv?.aws?.profile) { + env._ARC_PROFILE = inventory.inv?.aws?.profile + } + let result = execFileSync('node', [ script ], { env }) + return JSON.parse(result) + } + catch (err) { + console.error('Unknown credential check error') + throw err + } + } + + let creds = check() + if (creds.error && needsValidCreds) { + return Error('Valid credentials needed to run this command; missing or invalid credentials') + } + else if (creds.error) { + /** + * Backfill creds - any creds will do for local service emulation + * - Be sure we backfill Lambda's prepopulated env vars + * - sessionToken / AWS_SESSION_TOKEN is optional, skip so as not to introduce unintended side-effects + */ + process.env.ARC_AWS_CREDS = 'dummy' + process.env.AWS_ACCESS_KEY_ID = 'arc_dummy_access_key' + process.env.AWS_SECRET_ACCESS_KEY = 'arc_dummy_secret_key' + } +} diff --git a/banner/index.js b/banner/index.js index a5a5c83..eb5ee8d 100644 --- a/banner/index.js +++ b/banner/index.js @@ -1,10 +1,11 @@ let chalk = require('chalk') let chars = require('../chars') -let initAWS = require('./init-aws') +let credCheck = require('./cred-check') module.exports = function printBanner (params = {}) { let { cwd = process.cwd(), + checkCreds, inventory, disableBanner, disableRegion, @@ -25,22 +26,25 @@ module.exports = function printBanner (params = {}) { // Initialize config process.env.ARC_APP_NAME = inventory.inv.app - initAWS({ inventory, needsValidCreds }) + + // Check creds + let credError = credCheck({ checkCreds, inventory, needsValidCreds }) // App name let name = process.env.ARC_APP_NAME || 'Architect project manifest not found' log('App', name) // Region - let region = process.env.AWS_REGION || '@aws region / AWS_REGION not configured' + let region = inventory.inv?.aws?.region || process.env.AWS_REGION || 'Region not configured' if (!disableRegion) { log('Region', region) } // Profile - let profile = process.env.ARC_AWS_CREDS === 'env' + let profile = process.env.AWS_ACCESS_KEY_ID && + process.env.ARC_AWS_CREDS !== 'dummy' ? 'Set via environment' - : process.env.AWS_PROFILE || '@aws profile / AWS_PROFILE not configured' + : inventory.inv?.aws?.profile || process.env.AWS_PROFILE || 'Profile not configured' if (!disableProfile) { log('Profile', profile) } @@ -52,9 +56,10 @@ module.exports = function printBanner (params = {}) { // cwd log('cwd', cwd) + // Blow up (if necessary) after printing basic diagnostic stuff + if (credError) throw credError + // Space - if (!quiet) { - console.log() - } + if (!quiet) console.log() } } diff --git a/banner/init-aws.js b/banner/init-aws.js deleted file mode 100644 index 1e41bb3..0000000 --- a/banner/init-aws.js +++ /dev/null @@ -1,116 +0,0 @@ -let { existsSync: exists } = require('fs') -let homeDir = require('os').homedir() -let { join } = require('path') -let updater = require('../updater') - -/** - * Initialize AWS configuration, in order of preference: - * - @aws pragma + ~/.aws/credentials file - * - Environment variables - * - Dummy creds (if absolutely necessary) - */ -module.exports = function initAWS ({ inventory, needsValidCreds = true }) { - // AWS SDK intentionally not added to package deps; assume caller already has it - // eslint-disable-next-line - try { require('aws-sdk/lib/maintenance_mode_message').suppress = true } - catch { /* Noop */ } - // eslint-disable-next-line - let aws = require('aws-sdk') - let credentialsMethod = 'SharedIniFileCredentials' - let { inv } = inventory - try { - let defaultCredsPath = join(homeDir, '.aws', 'credentials') - let envCredsPath = process.env.AWS_SHARED_CREDENTIALS_FILE - let credsPath = envCredsPath || defaultCredsPath - let credsExists = exists(envCredsPath) || exists(defaultCredsPath) - // Inventory always sets a dfeault region if not specified - process.env.AWS_REGION = inv.aws.region - /** - * Always ensure we end with a final sanity check on loaded credentials - */ - // Allow local cred file to be overriden by env vars - let envOverride = process.env.ARC_AWS_CREDS === 'env' - if (credsExists && !envOverride) { - let profile = process.env.AWS_PROFILE - aws.config.credentials = [] - if (inv.aws && inv.aws.profile) { - process.env.AWS_PROFILE = profile = inv.aws.profile - } - - let init = new aws.IniLoader() - let opts = { - filename: credsPath - } - - let profileData = init.loadFrom(opts) - if (!profile) profile = process.env.AWS_PROFILE = 'default' - process.env.ARC_AWS_CREDS = 'missing' - - if (profileData[profile]) { - process.env.ARC_AWS_CREDS = 'profile' - - if (profileData[profile].credential_process) credentialsMethod = 'ProcessCredentials' - aws.config.credentials = new aws[credentialsMethod]({ - ...opts, - profile: process.env.AWS_PROFILE - }) - } - else { - delete process.env.AWS_PROFILE - } - } - else { - let hasEnvVars = process.env.AWS_ACCESS_KEY_ID && - process.env.AWS_SECRET_ACCESS_KEY - if (hasEnvVars) { - process.env.ARC_AWS_CREDS = 'env' - let params = { - accessKeyId: process.env.AWS_ACCESS_KEY_ID, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY - } - if (process.env.AWS_SESSION_TOKEN) { - params.sessionToken = process.env.AWS_SESSION_TOKEN - } - aws.config.credentials = new aws.Credentials(params) - } - } - credentialCheck() - /** - * Final credential check to ensure we meet the cred needs of Arc various packages - * - Packages that **need** valid creds should be made aware that none are available (ARC_AWS_CREDS = 'missing') - * - Others that **do not need** valid creds should work fine when supplied with dummy creds (or none at all, but we'll backfill dummy creds jic) - */ - function credentialCheck () { - let creds = aws.config.credentials - let invalidCreds = Array.isArray(creds) && !creds.length - let noCreds = !creds || invalidCreds || process.env.ARC_AWS_CREDS == 'missing' - if (noCreds && needsValidCreds) { - // Set missing creds flag and let consuming modules handle as necessary - process.env.ARC_AWS_CREDS = 'missing' - } - else if (noCreds && !needsValidCreds) { - /** - * Any creds will do! (e.g. Sandbox DynamoDB) - * - Be sure we backfill Lambda's prepopulated env vars - * - sessionToken / AWS_SESSION_TOKEN is optional, skip so as not to introduce unintended side-effects - */ - process.env.ARC_AWS_CREDS = 'dummy' - process.env.AWS_ACCESS_KEY_ID = 'xxx' - process.env.AWS_SECRET_ACCESS_KEY = 'xxx' - aws.config.credentials = new aws.Credentials({ - accessKeyId: 'xxx', - secretAccessKey: 'xxx' - }) - } - // If no creds, always unset profile to prevent misleading claims about profile state - if (noCreds) { - delete process.env.AWS_PROFILE - } - } - } - catch (e) { - // Don't exit process here; caller should be responsible - let update = updater('Startup') - update.err(e) - } -} diff --git a/changelog.md b/changelog.md index ca234d5..a410851 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,17 @@ --- +## [4.0.0] 2023-10-19 + +### Changed + +- Initializing the Architect banner is now significantly faster by way of relying on `aws-lite` (instead of `aws-sdk`) +- Breaking change: banner initialization no longer mutates `AWS_PROFILE`, or uses `ARC_AWS_CREDS` as a signal to other modules about credential loading + - While credential env vars (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) are still backfilled in certain circumstances, modules relying on the banner for credential-related operations must review the changes and refactor accordingly +- Breaking change: banner initialization now throws on invalid credentials + +--- + ## [3.1.7 - 3.1.9] 2023-04-22 ### Changed diff --git a/package.json b/package.json index b288993..8d2eca8 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ }, "scripts": { "test": "npm run lint && npm run test:unit:updater && npm run test:unit", - "test:unit": "cross-env tape 'test/**/*-test.js' | tap-arc", - "test:unit:updater": "cross-env tape test/updater/test.js | tap-arc", + "test:unit": "cross-env tape 'test/**/*-test.js' | tap-spec", + "test:unit:updater": "cross-env tape test/updater/test.js | tap-spec", "lint": "npx eslint . --fix", "rc": "npm version prerelease --preid RC" }, @@ -20,6 +20,7 @@ "author": "Brian LeRoux ", "license": "Apache-2.0", "dependencies": { + "@aws-lite/client": "0.11.1", "chalk": "4.1.2", "glob": "~10.2.2", "path-sort": "~0.1.0", @@ -31,12 +32,11 @@ "devDependencies": { "@architect/eslint-config": "~2.1.2", "@architect/inventory": "~3.4.3", - "aws-sdk": "^2.1363.0", "cross-env": "~7.0.3", "eslint": "~8.49.0", "proxyquire": "~2.1.3", "sinon": "~15.0.4", - "tap-arc": "~1.0.0", + "tap-spec": "5.0.0", "tape": "~5.6.6", "temp-write": "4.0.0" }, diff --git a/test/banner/cred-check-test.js b/test/banner/cred-check-test.js new file mode 100644 index 0000000..f456058 --- /dev/null +++ b/test/banner/cred-check-test.js @@ -0,0 +1,64 @@ +let test = require('tape') +let credCheck = require('../../banner/cred-check') + +function reset (t) { + let envVars = [ + 'ARC_AWS_CREDS', + 'AWS_PROFILE', + 'AWS_REGION', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_SESSION_TOKEN', + 'AWS_SHARED_CREDENTIALS_FILE', + ] + envVars.forEach(v => delete process.env[v]) + envVars.forEach(v => { + if (process.env[v]) t.fail(`Found errant env var: ${v}`) + }) +} + +let inventory = { inv: { aws: {} } } + +test('Set up env', t => { + t.plan(1) + t.ok(credCheck, 'Found credCheck') +}) + +test('Credential check is disabled', t => { + t.plan(2) + let err = credCheck({ checkCreds: false, inventory }) + t.notOk(err, 'No credential loading error reported') + t.notOk(process.env.ARC_AWS_CREDS, 'Did not mutate ARC_AWS_CREDS') + reset(t) +}) + +test('Credential checks', t => { + t.plan(3) + let err + + // Count on aws-lite finding creds (via env) + process.env.AWS_ACCESS_KEY_ID = 'yo' + process.env.AWS_SECRET_ACCESS_KEY = 'yo' + err = credCheck({ inventory }) + t.notOk(err, 'No credential loading error reported') + t.notOk(process.env.ARC_AWS_CREDS, 'Did not mutate ARC_AWS_CREDS') + + // Fail a cred check + reset(t) + process.env.AWS_PROFILE = 'random_profile_name_that_does_not_exist' + err = credCheck({ inventory, needsValidCreds: true }) + t.ok(err, 'Reported credential loading error') + console.log(err) + reset(t) +}) + +test('Credential backfill', t => { + t.plan(4) + process.env.AWS_PROFILE = 'random_profile_name_that_does_not_exist' + let err = credCheck({ inventory }) + t.notOk(err, 'No credential loading error reported') + t.equal(process.env.ARC_AWS_CREDS, 'dummy', 'Mutated ARC_AWS_CREDS') + t.equal(process.env.AWS_ACCESS_KEY_ID, 'arc_dummy_access_key', 'Mutated AWS_ACCESS_KEY_ID') + t.equal(process.env.AWS_SECRET_ACCESS_KEY, 'arc_dummy_secret_key', 'Mutated AWS_SECRET_ACCESS_KEY') + reset(t) +}) diff --git a/test/banner/init-aws-test.js b/test/banner/init-aws-test.js deleted file mode 100644 index ffe8279..0000000 --- a/test/banner/init-aws-test.js +++ /dev/null @@ -1,162 +0,0 @@ -let test = require('tape') -require('aws-sdk/lib/maintenance_mode_message').suppress = true -let aws = require('aws-sdk') -let tmpFile = require('temp-write') -let mockCredsContent = require('./mock-aws-creds-file') -let _inventory = require('@architect/inventory') -let mockCredsFile = tmpFile(mockCredsContent) -let credsExists = true -let fs = { - existsSync: function () { - return credsExists - } -} -let profile = 'architect' -let secret = 'so-secret' -let sessionToken = 'a-random-token' -let proxyquire = require('proxyquire').noCallThru() -let initAWS = proxyquire('../../banner/init-aws', { - 'fs': fs, - '@noCallThru': true -}) - -function reset (t) { - let envVars = [ - 'ARC_AWS_CREDS', - 'AWS_PROFILE', - 'AWS_REGION', - 'AWS_ACCESS_KEY_ID', - 'AWS_SECRET_ACCESS_KEY', - 'AWS_SESSION_TOKEN', - 'AWS_SHARED_CREDENTIALS_FILE' - ] - envVars.forEach(v => {delete process.env[v]}) - envVars.forEach(v => { - if (process.env[v]) t.fail(`Found errant env var: ${v}`) - }) - aws.config = { credentials: null } -} - -/** - * AWS credential initialization tests are focused on our credential population - * These aren't intended to be integration tests for aws-sdk credentials methods (`SharedIniFileCredentials` + `Credentials`, etc.) - */ -test('Set region', async t => { - reset(t) - t.plan(2) - let region = 'us-west-1' - let rawArc = `@app\nappname\n@aws\nregion ${region}` - let inventory = await _inventory({ rawArc }) - initAWS({ inventory }) - t.equal(process.env.AWS_REGION, region, `AWS region set by .arc`) - // Leave AWS_REGION env var set for next test - - rawArc = `@app\nappname` - inventory = await _inventory({ rawArc }) - initAWS({ inventory }) - t.equal(process.env.AWS_REGION, region, `AWS region set by env`) - reset(t) -}) - -test('Credentials supplied by ~/.aws/credentials', async t => { - t.plan(12) - let rawArc - let inventory - - // Profile found, no env override - // Profile specified by .arc - process.env.AWS_SHARED_CREDENTIALS_FILE = await mockCredsFile - rawArc = `@app\nappname\n@aws\nprofile ${profile}` - inventory = await _inventory({ rawArc }) - - initAWS({ inventory }) - t.equal(process.env.ARC_AWS_CREDS, 'profile', `ARC_AWS_CREDS set to 'profile'`) - t.equal(process.env.AWS_PROFILE, profile, `AWS_PROFILE set by .arc`) - t.equal(aws.config.credentials.accessKeyId, 'architect_mock_access_key', `AWS config not mutated`) - reset(t) - - // Profile specified by env - process.env.AWS_SHARED_CREDENTIALS_FILE = await mockCredsFile - process.env.AWS_PROFILE = profile - rawArc = `@app\nappname` - inventory = await _inventory({ rawArc }) - - initAWS({ inventory }) - t.equal(process.env.ARC_AWS_CREDS, 'profile', `ARC_AWS_CREDS set to 'profile'`) - t.equal(process.env.AWS_PROFILE, profile, `AWS_PROFILE set by env`) - t.equal(aws.config.credentials.accessKeyId, 'architect_mock_access_key', `AWS config not mutated`) - reset(t) - - // Process Credential Respected - process.env.AWS_SHARED_CREDENTIALS_FILE = await mockCredsFile - process.env.AWS_PROFILE = 'architect_process' - - initAWS({ inventory }) - t.equal(process.env.ARC_AWS_CREDS, 'profile', `ARC_AWS_CREDS set to 'profile'`) - t.equal(process.env.AWS_PROFILE, 'architect_process', `AWS_PROFILE set by env`) - console.log(aws.config.credentials) - t.notOk(aws.config.credentials.accessKeyId, `AWS access key not set via processCred`) - reset(t) - - // Profile defaulted - process.env.AWS_SHARED_CREDENTIALS_FILE = await mockCredsFile - - initAWS({ inventory }) - t.equal(process.env.ARC_AWS_CREDS, 'profile', `ARC_AWS_CREDS set to 'profile'`) - t.equal(process.env.AWS_PROFILE, 'default', `AWS_PROFILE set to default`) - t.equal(aws.config.credentials.accessKeyId, 'default_mock_access_key', `AWS config not mutated`) - reset(t) -}) - -test('Credentials supplied by env vars', async t => { - t.plan(10) - - // Credentials file found, env override - process.env.ARC_AWS_CREDS = 'env' - process.env.AWS_ACCESS_KEY_ID = profile - process.env.AWS_SECRET_ACCESS_KEY = 'so-secret' - let rawArc = `@app\nappname\n@aws\nprofile ${profile}` - let inventory = await _inventory({ rawArc }) - initAWS({ inventory }) - t.equal(process.env.ARC_AWS_CREDS, 'env', `ARC_AWS_CREDS set to 'env'`) - t.notOk(process.env.AWS_PROFILE, `AWS_PROFILE not set`) - t.equal(aws.config.credentials.accessKeyId, profile, `AWS config access key set`) - t.equal(aws.config.credentials.secretAccessKey, secret, `AWS config secret key set`) - t.notOk(aws.config.credentials.sessionToken, `AWS config sessionToken not set`) - reset(t) - - process.env.ARC_AWS_CREDS = 'env' - process.env.AWS_ACCESS_KEY_ID = profile - process.env.AWS_SECRET_ACCESS_KEY = 'so-secret' - process.env.AWS_SESSION_TOKEN = sessionToken - initAWS({ inventory }) - t.equal(process.env.ARC_AWS_CREDS, 'env', `ARC_AWS_CREDS set to 'env'`) - t.notOk(process.env.AWS_PROFILE, `AWS_PROFILE not set`) - t.equal(aws.config.credentials.accessKeyId, profile, `AWS config access key set`) - t.equal(aws.config.credentials.secretAccessKey, secret, `AWS config secret key set`) - t.equal(aws.config.credentials.sessionToken, sessionToken, `AWS config sessionToken set`) - reset(t) -}) - -test('Final credential check', async t => { - t.plan(9) - process.env.HIDE_HOME = true - process.env.AWS_SHARED_CREDENTIALS_FILE = await tmpFile('') - let rawArc = `@app\nappname\n@aws\nprofile does-not-exist` - let inventory = await _inventory({ rawArc }) - initAWS({ inventory }) - t.equal(process.env.ARC_AWS_CREDS, 'missing', `ARC_AWS_CREDS set to 'missing'`) - t.notOk(process.env.AWS_PROFILE, `AWS_PROFILE deleted`) - t.notOk(Object.keys(aws.config.credentials).length, `AWS config not present`) - reset(t) - - process.env.AWS_SHARED_CREDENTIALS_FILE = await tmpFile('') - initAWS({ inventory, needsValidCreds: false }) - t.equal(process.env.ARC_AWS_CREDS, 'dummy', `ARC_AWS_CREDS set to 'dummy'`) - t.equal(process.env.AWS_ACCESS_KEY_ID, 'xxx', `AWS_ACCESS_KEY_ID backfilled`) - t.equal(process.env.AWS_SECRET_ACCESS_KEY, 'xxx', `AWS_SECRET_ACCESS_KEY backfilled`) - t.notOk(process.env.AWS_PROFILE, `AWS_PROFILE deleted`) - t.equal(aws.config.credentials.accessKeyId, 'xxx', `AWS config.credentials.accessKeyId backfilled to 'xxx'`) - t.equal(aws.config.credentials.secretAccessKey, 'xxx', `AWS config.credentials.secretAccessKey backfilled to 'xxx'`) - reset(t) -}) From bd5c4f83754c5dc28c7382c9ae24988047d1eeb6 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Thu, 19 Oct 2023 15:56:52 -0700 Subject: [PATCH 02/17] Do not backfill AWS credential env vars during banner init --- banner/cred-check.js | 10 ---------- changelog.md | 2 +- test/banner/cred-check-test.js | 11 ----------- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/banner/cred-check.js b/banner/cred-check.js index 8cbe12b..e48ca51 100644 --- a/banner/cred-check.js +++ b/banner/cred-check.js @@ -31,14 +31,4 @@ module.exports = function credCheck ({ checkCreds = true, inventory, needsValidC if (creds.error && needsValidCreds) { return Error('Valid credentials needed to run this command; missing or invalid credentials') } - else if (creds.error) { - /** - * Backfill creds - any creds will do for local service emulation - * - Be sure we backfill Lambda's prepopulated env vars - * - sessionToken / AWS_SESSION_TOKEN is optional, skip so as not to introduce unintended side-effects - */ - process.env.ARC_AWS_CREDS = 'dummy' - process.env.AWS_ACCESS_KEY_ID = 'arc_dummy_access_key' - process.env.AWS_SECRET_ACCESS_KEY = 'arc_dummy_secret_key' - } } diff --git a/changelog.md b/changelog.md index a410851..e9ab5c3 100644 --- a/changelog.md +++ b/changelog.md @@ -8,7 +8,7 @@ - Initializing the Architect banner is now significantly faster by way of relying on `aws-lite` (instead of `aws-sdk`) - Breaking change: banner initialization no longer mutates `AWS_PROFILE`, or uses `ARC_AWS_CREDS` as a signal to other modules about credential loading - - While credential env vars (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) are still backfilled in certain circumstances, modules relying on the banner for credential-related operations must review the changes and refactor accordingly + - Modules relying on the banner for credential-related operations must review the changes and refactor accordingly - Breaking change: banner initialization now throws on invalid credentials --- diff --git a/test/banner/cred-check-test.js b/test/banner/cred-check-test.js index f456058..0b68d91 100644 --- a/test/banner/cred-check-test.js +++ b/test/banner/cred-check-test.js @@ -51,14 +51,3 @@ test('Credential checks', t => { console.log(err) reset(t) }) - -test('Credential backfill', t => { - t.plan(4) - process.env.AWS_PROFILE = 'random_profile_name_that_does_not_exist' - let err = credCheck({ inventory }) - t.notOk(err, 'No credential loading error reported') - t.equal(process.env.ARC_AWS_CREDS, 'dummy', 'Mutated ARC_AWS_CREDS') - t.equal(process.env.AWS_ACCESS_KEY_ID, 'arc_dummy_access_key', 'Mutated AWS_ACCESS_KEY_ID') - t.equal(process.env.AWS_SECRET_ACCESS_KEY, 'arc_dummy_secret_key', 'Mutated AWS_SECRET_ACCESS_KEY') - reset(t) -}) From 7dd623f0430f27f6875e5d0d478910e51c1f0006 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Thu, 19 Oct 2023 17:41:36 -0700 Subject: [PATCH 03/17] 4.0.0-RC.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d2eca8..4e19c25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@architect/utils", - "version": "3.1.9", + "version": "4.0.0-RC.0", "description": "Common utility functions", "main": "index.js", "repository": { From 12a202a400741c0ae5b591c70dba3c772ff81282 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 25 Oct 2023 14:42:55 -0700 Subject: [PATCH 04/17] Refactor credential checker into its own utility method --- banner/_get-creds.js | 16 ------- banner/cred-check.js | 34 --------------- banner/index.js | 80 ++++++++++++++-------------------- changelog.md | 12 +++-- check-creds/index.js | 29 ++++++++++++ index.js | 4 +- test/banner/cred-check-test.js | 53 +++++++++++++++------- 7 files changed, 110 insertions(+), 118 deletions(-) delete mode 100644 banner/_get-creds.js delete mode 100644 banner/cred-check.js create mode 100644 check-creds/index.js diff --git a/banner/_get-creds.js b/banner/_get-creds.js deleted file mode 100644 index e1eaf37..0000000 --- a/banner/_get-creds.js +++ /dev/null @@ -1,16 +0,0 @@ -let awsLite = require('@aws-lite/client') -async function main () { - try { - let options = { autoloadPlugins: false, region: 'us-west-1' } - if (process.env._ARC_PROFILE) options.profile = process.env._ARC_PROFILE - await awsLite(options) - console.log(JSON.stringify({ ok: true })) - } - catch (err) { - console.log(JSON.stringify({ - error: err.message, - stack: err.stack, - })) - } -} -main() diff --git a/banner/cred-check.js b/banner/cred-check.js deleted file mode 100644 index e48ca51..0000000 --- a/banner/cred-check.js +++ /dev/null @@ -1,34 +0,0 @@ -let { join } = require('path') - -/** - * Credential check and possible backstop - * - aws-lite requires credentials to initialize - * - Architect needs credentials for some things (e.g. Deploy), but also has a variety of offline workflows that interface with AWS service API emulators (e.g. Sandbox) - * - Thus, sometimes it's ok to use dummy creds, sometimes we need to halt - */ -module.exports = function credCheck ({ checkCreds = true, inventory, needsValidCreds = false }) { - if (!checkCreds) return - - // eslint-disable-next-line - let { execFileSync } = require('child_process') - let script = join(__dirname, '_get-creds.js') - function check () { - try { - let env = { ...process.env } - if (inventory.inv?.aws?.profile) { - env._ARC_PROFILE = inventory.inv?.aws?.profile - } - let result = execFileSync('node', [ script ], { env }) - return JSON.parse(result) - } - catch (err) { - console.error('Unknown credential check error') - throw err - } - } - - let creds = check() - if (creds.error && needsValidCreds) { - return Error('Valid credentials needed to run this command; missing or invalid credentials') - } -} diff --git a/banner/index.js b/banner/index.js index eb5ee8d..782f0c9 100644 --- a/banner/index.js +++ b/banner/index.js @@ -1,65 +1,49 @@ let chalk = require('chalk') let chars = require('../chars') -let credCheck = require('./cred-check') module.exports = function printBanner (params = {}) { let { cwd = process.cwd(), - checkCreds, inventory, disableBanner, - disableRegion, - disableProfile, - needsValidCreds, version = '–', } = params let quiet = process.env.ARC_QUIET || process.env.QUIET || params.quiet if (disableBanner) return - else { - // Boilerplate - let log = (label, value) => { - if (!quiet) { - console.log(chalk.grey(`${label.padStart(12)} ${chars.buzz}`), chalk.cyan(value)) - } - } - - // Initialize config - process.env.ARC_APP_NAME = inventory.inv.app - - // Check creds - let credError = credCheck({ checkCreds, inventory, needsValidCreds }) - - // App name - let name = process.env.ARC_APP_NAME || 'Architect project manifest not found' - log('App', name) - - // Region - let region = inventory.inv?.aws?.region || process.env.AWS_REGION || 'Region not configured' - if (!disableRegion) { - log('Region', region) - } - // Profile - let profile = process.env.AWS_ACCESS_KEY_ID && - process.env.ARC_AWS_CREDS !== 'dummy' - ? 'Set via environment' - : inventory.inv?.aws?.profile || process.env.AWS_PROFILE || 'Profile not configured' - if (!disableProfile) { - log('Profile', profile) + // Boilerplate + let log = (label, value) => { + if (!quiet) { + console.log(chalk.grey(`${label.padStart(12)} ${chars.buzz}`), chalk.cyan(value)) } - - // Caller version - // Also: set deprecation status for legacy Arc installs - log('Version', version) - - // cwd - log('cwd', cwd) - - // Blow up (if necessary) after printing basic diagnostic stuff - if (credError) throw credError - - // Space - if (!quiet) console.log() } + + // App name + let name = inventory.inv.app || 'Architect project manifest not found' + log('App', name) + + // Region + let region = inventory.inv?.aws?.region || + process.env.AWS_REGION || + 'Region not configured' + log('Region', region) + + // Profile + let credsViaEnv = process.env.AWS_ACCESS_KEY_ID ? 'Set via environment' : undefined + let profile = credsViaEnv || + inventory.inv?.aws?.profile || + process.env.AWS_PROFILE || + 'Not configured / default' + log('Profile', profile) + + // Caller version + // Also: set deprecation status for legacy Arc installs + log('Version', version) + + // cwd + log('cwd', cwd) + + // Space + if (!quiet) console.log() } diff --git a/changelog.md b/changelog.md index e9ab5c3..1358ceb 100644 --- a/changelog.md +++ b/changelog.md @@ -4,12 +4,18 @@ ## [4.0.0] 2023-10-19 +### Added + +- Added `checkCreds` method for manually performing basic AWS credential checks + + ### Changed -- Initializing the Architect banner is now significantly faster by way of relying on `aws-lite` (instead of `aws-sdk`) -- Breaking change: banner initialization no longer mutates `AWS_PROFILE`, or uses `ARC_AWS_CREDS` as a signal to other modules about credential loading +- Initializing the Architect banner is significantly faster, as it no longer has any interactions with `aws-sdk` +- Breaking change: banner initialization no longer has any direct responsibility for credential checking + - Related: banner initialization no longer mutates `AWS_PROFILE`, or uses `ARC_AWS_CREDS` as a signal to other modules about credential loading - Modules relying on the banner for credential-related operations must review the changes and refactor accordingly -- Breaking change: banner initialization now throws on invalid credentials +- Banner initialization no longer utilizes `disableRegion` or `disableProfile` params when printing --- diff --git a/check-creds/index.js b/check-creds/index.js new file mode 100644 index 0000000..9f74887 --- /dev/null +++ b/check-creds/index.js @@ -0,0 +1,29 @@ +/** + * Credential check + * - aws-lite requires credentials to initialize + * - Architect needs credentials for some things (e.g. Deploy), but also has a variety of offline workflows that interface with AWS service API emulators (e.g. Sandbox) + * - Thus, sometimes it's ok to use dummy creds, but sometimes we need to halt (via this util) + */ +module.exports = function checkAwsCredentials (params, callback) { + // eslint-disable-next-line + let awsLite = require('@aws-lite/client') + let { inventory } = params + + let promise + if (!callback) { + promise = new Promise((res, rej) => { + callback = (err, result) => err ? rej(err) : res(result) + }) + } + + let errMsg = 'Valid AWS credentials needed to continue; missing or invalid credentials' + awsLite({ + autoloadPlugins: false, + profile: inventory.inv?.aws?.profile, // aws-lite falls back to AWS_PROFILE or 'default' if undefined + region: 'us-west-1', + }) + .then(() => callback()) + .catch(() => callback(Error(errMsg))) + + return promise +} diff --git a/index.js b/index.js index eac2064..f6f5147 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ let banner = require('./banner') let chars = require('./chars') +let checkCreds = require('./check-creds') let deepFrozenCopy = require('./deep-frozen-copy') let fingerprint = require('./fingerprint') let getLambdaName = require('./get-lambda-name') @@ -8,8 +9,9 @@ let toLogicalID = require('./to-logical-id') let updater = require('./updater') module.exports = { - banner, // Prints banner and loads basic env vars and AWS creds + banner, // Prints banner chars, // Returns platform appropriate characters for CLI UI printing + checkCreds, // Performs basic AWS credential check deepFrozenCopy, // Fast deep frozen object copy fingerprint, // Generates public/static.json for `@static fingerprint true` getLambdaName, // Get Lambda name from Arc path diff --git a/test/banner/cred-check-test.js b/test/banner/cred-check-test.js index 0b68d91..43772cc 100644 --- a/test/banner/cred-check-test.js +++ b/test/banner/cred-check-test.js @@ -1,5 +1,5 @@ let test = require('tape') -let credCheck = require('../../banner/cred-check') +let checkCreds = require('../../check-creds') function reset (t) { let envVars = [ @@ -21,33 +21,54 @@ let inventory = { inv: { aws: {} } } test('Set up env', t => { t.plan(1) - t.ok(credCheck, 'Found credCheck') + t.ok(checkCreds, 'Found checkCreds') }) -test('Credential check is disabled', t => { +test('Credential checks (async)', async t => { t.plan(2) - let err = credCheck({ checkCreds: false, inventory }) - t.notOk(err, 'No credential loading error reported') - t.notOk(process.env.ARC_AWS_CREDS, 'Did not mutate ARC_AWS_CREDS') + + // Count on aws-lite finding creds (via env) + process.env.AWS_ACCESS_KEY_ID = 'yo' + process.env.AWS_SECRET_ACCESS_KEY = 'yo' + try { + await checkCreds({ inventory }) + t.pass('No credential loading error reported') + } + catch (err) { + t.fail(err) + } + + // Fail a cred check + reset(t) + process.env.AWS_PROFILE = 'random_profile_name_that_does_not_exist' + try { + await checkCreds({ inventory }) + t.fail('Should have errored') + } + catch (err) { + t.ok(err, 'Reported credential loading error') + } reset(t) }) -test('Credential checks', t => { - t.plan(3) - let err +test('Credential checks (callback)', t => { + t.plan(2) // Count on aws-lite finding creds (via env) process.env.AWS_ACCESS_KEY_ID = 'yo' process.env.AWS_SECRET_ACCESS_KEY = 'yo' - err = credCheck({ inventory }) - t.notOk(err, 'No credential loading error reported') - t.notOk(process.env.ARC_AWS_CREDS, 'Did not mutate ARC_AWS_CREDS') + checkCreds({ inventory }, err => { + reset(t) + if (err) t.fail(err) + else t.pass('No credential loading error reported') + }) // Fail a cred check reset(t) process.env.AWS_PROFILE = 'random_profile_name_that_does_not_exist' - err = credCheck({ inventory, needsValidCreds: true }) - t.ok(err, 'Reported credential loading error') - console.log(err) - reset(t) + checkCreds({ inventory }, err => { + reset(t) + if (err) t.ok(err, 'Reported credential loading error') + else t.fail('Should have errored') + }) }) From 48197f2e49c61a261545cb5cb96d9914c6cac497 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 25 Oct 2023 14:43:15 -0700 Subject: [PATCH 05/17] Change test filename --- test/banner/{cred-check-test.js => check-creds-test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/banner/{cred-check-test.js => check-creds-test.js} (100%) diff --git a/test/banner/cred-check-test.js b/test/banner/check-creds-test.js similarity index 100% rename from test/banner/cred-check-test.js rename to test/banner/check-creds-test.js From fcf9b8b6042c88821d550bbd5c6e1c04a04d107f Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 25 Oct 2023 14:50:54 -0700 Subject: [PATCH 06/17] Update deps --- package.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4e19c25..65bf6b7 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ }, "scripts": { "test": "npm run lint && npm run test:unit:updater && npm run test:unit", - "test:unit": "cross-env tape 'test/**/*-test.js' | tap-spec", - "test:unit:updater": "cross-env tape test/updater/test.js | tap-spec", + "test:unit": "cross-env tape 'test/**/*-test.js' | tap-arc", + "test:unit:updater": "cross-env tape test/updater/test.js | tap-arc", "lint": "npx eslint . --fix", "rc": "npm version prerelease --preid RC" }, @@ -36,9 +36,8 @@ "eslint": "~8.49.0", "proxyquire": "~2.1.3", "sinon": "~15.0.4", - "tap-spec": "5.0.0", - "tape": "~5.6.6", - "temp-write": "4.0.0" + "tap-arc": "1.1.0", + "tape": "~5.6.6" }, "eslintConfig": { "extends": "@architect/eslint-config" From 8ff2756ff06b527e45e7c28f1349afa9104921ba Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 25 Oct 2023 14:50:58 -0700 Subject: [PATCH 07/17] 4.0.0-RC.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65bf6b7..44aae58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@architect/utils", - "version": "4.0.0-RC.0", + "version": "4.0.0-RC.1", "description": "Common utility functions", "main": "index.js", "repository": { From 0f2b688a953728a051d7e1a578099c95ae6d3c50 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Mon, 8 Jan 2024 10:31:53 -0800 Subject: [PATCH 08/17] Add Node.js 20.x to test matrix --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9716bad..c03d750 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [ 14.x, 16.x, 18.x ] + node-version: [ 14.x, 16.x, 18.x, 20.x ] os: [ windows-latest, ubuntu-latest, macOS-latest ] # Go From 375ff3082ce39723836332ae61bd18697fef8053 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Mon, 8 Jan 2024 10:34:53 -0800 Subject: [PATCH 09/17] Remove Node.js 14.x from test matrix --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c03d750..16c995d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [ 14.x, 16.x, 18.x, 20.x ] + node-version: [ 16.x, 18.x, 20.x ] os: [ windows-latest, ubuntu-latest, macOS-latest ] # Go From da8e91314673833a94954b42fa77fb8c339781c9 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Mon, 8 Jan 2024 10:55:06 -0800 Subject: [PATCH 10/17] Update changelog --- changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog.md b/changelog.md index 1358ceb..0cb157c 100644 --- a/changelog.md +++ b/changelog.md @@ -16,6 +16,9 @@ - Related: banner initialization no longer mutates `AWS_PROFILE`, or uses `ARC_AWS_CREDS` as a signal to other modules about credential loading - Modules relying on the banner for credential-related operations must review the changes and refactor accordingly - Banner initialization no longer utilizes `disableRegion` or `disableProfile` params when printing +- Transitioned from `aws-sdk` to [`aws-lite`](https://aws-lite.org) +- Added Node.js 20.x to test matrix +- Breaking change: removed support for Node.js 14.x (now EOL, and no longer available to created in AWS Lambda) --- From aac5b24026efde23c816243373a7f9e2306502b7 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Mon, 8 Jan 2024 10:55:11 -0800 Subject: [PATCH 11/17] 4.0.0-RC.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 44aae58..1c5bf53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@architect/utils", - "version": "4.0.0-RC.1", + "version": "4.0.0-RC.2", "description": "Common utility functions", "main": "index.js", "repository": { From a308b8ab94622014ef13ba7c322929eae679656d Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Tue, 23 Jan 2024 18:46:40 -0800 Subject: [PATCH 12/17] Update CI --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16c995d..30ce786 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: - name: Notify uses: sarisia/actions-status-discord@v1 # Only fire alert once - if: github.ref == 'refs/heads/main' && failure() && matrix.node-version == '14.x' && matrix.os == 'ubuntu-latest' + if: github.ref == 'refs/heads/main' && failure() && matrix.node-version == '20.x' && matrix.os == 'ubuntu-latest' with: webhook: ${{ secrets.DISCORD_WEBHOOK }} title: "build and test" From e0e43ed8f0ef88367c56c2645a1470e03397480e Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 24 Jan 2024 07:28:10 -0800 Subject: [PATCH 13/17] Update deps --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1c5bf53..14bf7c8 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "author": "Brian LeRoux ", "license": "Apache-2.0", "dependencies": { - "@aws-lite/client": "0.11.1", + "@aws-lite/client": "~0.14.2", "chalk": "4.1.2", - "glob": "~10.2.2", + "glob": "~10.3.10", "path-sort": "~0.1.0", "restore-cursor": "3.1.0", "run-series": "~1.1.9", @@ -31,13 +31,13 @@ }, "devDependencies": { "@architect/eslint-config": "~2.1.2", - "@architect/inventory": "~3.4.3", + "@architect/inventory": "~3.6.5", "cross-env": "~7.0.3", - "eslint": "~8.49.0", + "eslint": "~8.56.0", "proxyquire": "~2.1.3", - "sinon": "~15.0.4", + "sinon": "~17.0.1", "tap-arc": "1.1.0", - "tape": "~5.6.6" + "tape": "~5.7.3" }, "eslintConfig": { "extends": "@architect/eslint-config" From 364eb7996fd6c42fd0c52e149c2e4c4b4dc0d316 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 24 Jan 2024 07:35:46 -0800 Subject: [PATCH 14/17] Fix async bug in cred check callback tests --- test/banner/check-creds-test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/banner/check-creds-test.js b/test/banner/check-creds-test.js index 43772cc..61198e1 100644 --- a/test/banner/check-creds-test.js +++ b/test/banner/check-creds-test.js @@ -51,8 +51,8 @@ test('Credential checks (async)', async t => { reset(t) }) -test('Credential checks (callback)', t => { - t.plan(2) +test('Credential checks (callback, via env)', t => { + t.plan(1) // Count on aws-lite finding creds (via env) process.env.AWS_ACCESS_KEY_ID = 'yo' @@ -62,7 +62,10 @@ test('Credential checks (callback)', t => { if (err) t.fail(err) else t.pass('No credential loading error reported') }) +}) +test('Credential checks (callback failure, via profile)', t => { + t.plan(1) // Fail a cred check reset(t) process.env.AWS_PROFILE = 'random_profile_name_that_does_not_exist' From 24a83430279b3f16d3f027cd30166b7ace77464a Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 24 Jan 2024 07:35:50 -0800 Subject: [PATCH 15/17] 4.0.0-RC.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 14bf7c8..cbad1ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@architect/utils", - "version": "4.0.0-RC.2", + "version": "4.0.0-RC.3", "description": "Common utility functions", "main": "index.js", "repository": { From 6b7f9b0f2552ab35c00047e5e3d7dbdbf21079be Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 31 Jan 2024 08:12:56 -0800 Subject: [PATCH 16/17] Update deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cbad1ac..a83a44b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "author": "Brian LeRoux ", "license": "Apache-2.0", "dependencies": { - "@aws-lite/client": "~0.14.2", + "@aws-lite/client": "^0.15.1", "chalk": "4.1.2", "glob": "~10.3.10", "path-sort": "~0.1.0", @@ -37,7 +37,7 @@ "proxyquire": "~2.1.3", "sinon": "~17.0.1", "tap-arc": "1.1.0", - "tape": "~5.7.3" + "tape": "~5.7.4" }, "eslintConfig": { "extends": "@architect/eslint-config" From 1d704b3a12819c6e1a0fe9df4414fd08bd61ef2f Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 31 Jan 2024 08:13:00 -0800 Subject: [PATCH 17/17] 4.0.0-RC.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a83a44b..9c6d0c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@architect/utils", - "version": "4.0.0-RC.3", + "version": "4.0.0-RC.4", "description": "Common utility functions", "main": "index.js", "repository": {