@architect/inventory 2.1.0 → 2.1.3

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/changelog.md CHANGED
@@ -2,12 +2,51 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## [2.1.3] 2021-11-04
6
+
7
+ ### Fixed
8
+
9
+ - Hardened runtime validation by ensuring non-string values will fail gracefully
10
+
11
+ ---
12
+
13
+ ## [2.1.2] 2021-10-28
14
+
15
+ ### Added
16
+
17
+ - Added memory / timeout configuration validation
18
+
19
+
20
+ ### Changed
21
+
22
+ - Improved layer validation error formatting
23
+
24
+ ---
25
+
26
+ ## [2.1.1] 2021-10-13
27
+
28
+ ### Added
29
+
30
+ - Added `config.runtimeAlias` property to Lambdas whose `config.runtime` is interpolated by way of latest-runtime aliasing (e.g. `node` or `py`)
31
+
32
+
33
+ ### Changed
34
+
35
+ - Internal change: implement [Lambda runtimes module](https://www.npmjs.com/package/lambda-runtimes) instead of maintaining valid runtime list in Inventory
36
+
37
+
38
+ ### Fixed
39
+
40
+ - Fixed `@scheduled` parsing in `app.json` + `package.json` > `arc.scheduled`
41
+
42
+ ---
43
+
5
44
  ## [2.1.0] 2021-10-11
6
45
 
7
46
  ### Added
8
47
 
9
- - Added latest-runtime version pinning
10
- - Example: if you always want your app to run the latest Lambda version of Python, instead of specifying `python3.9` (and changing it every time a new version of Python is released), instead simply specify `python` or `py`
48
+ - Added latest-runtime version aliasing
49
+ - Example: you want your app to always run the latest Lambda version of Python (instead of specifying `python3.9`(and changing it every time a new version of Python is released); now you can specify `python` or `py`
11
50
  - Valid shortcuts: Node.js: `node`, `nodejs`, `node.js`; Python: `python`, `py`; Ruby: `ruby`, `rb`; Java: `java`; Go: `go`, `golang`; .NET: `dotnet`, `.net`; and custom runtimes: `custom`
12
51
  - Added runtime validation
13
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@architect/inventory",
3
- "version": "2.1.0",
3
+ "version": "2.1.3",
4
4
  "description": "Architect project resource enumeration utility",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -22,14 +22,15 @@
22
22
  "dependencies": {
23
23
  "@architect/asap": "~4.1.0",
24
24
  "@architect/parser": "~5.0.2",
25
- "@architect/utils": "~3.0.3"
25
+ "@architect/utils": "~3.0.4",
26
+ "lambda-runtimes": "~1.0.1"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@architect/eslint-config": "~2.0.1",
29
30
  "aws-sdk": "2.880.0",
30
- "aws-sdk-mock": "5.4.0",
31
+ "aws-sdk-mock": "~5.4.0",
31
32
  "cross-env": "~7.0.3",
32
- "eslint": "~8.0.0",
33
+ "eslint": "~8.0.1",
33
34
  "mock-fs": "~5.1.1",
34
35
  "mock-require": "~3.0.3",
35
36
  "nyc": "~15.1.0",
@@ -38,5 +39,12 @@
38
39
  },
39
40
  "eslintConfig": {
40
41
  "extends": "@architect/eslint-config"
42
+ },
43
+ "nyc": {
44
+ "check-coverage": true,
45
+ "branches": 100,
46
+ "lines": 100,
47
+ "functions": 100,
48
+ "statements": 100
41
49
  }
42
50
  }
package/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- [<img src="https://s3-us-west-2.amazonaws.com/arc.codes/architect-logo-500b@2x.png" width=500>](https://www.npmjs.com/package/@architect/inventory)
1
+ [<img src="https://assets.arc.codes/architect-logo-500b@2x.png" width=500>](https://www.npmjs.com/package/@architect/inventory)
2
2
 
3
3
  ## [`@architect/inventory`](https://www.npmjs.com/package/@architect/inventory)
4
4
 
@@ -1,5 +1,4 @@
1
1
  let is = require('../lib/is')
2
- let runtimes = require('../lib/runtimes')
3
2
 
4
3
  /**
5
4
  * Overlay / append properties onto an existing config object
@@ -60,9 +59,6 @@ module.exports = function upsertProps (config, newConfig) {
60
59
  else if (name === 'policies' && !!(value)) {
61
60
  policies = policies.concat(value)
62
61
  }
63
- else if (name === 'runtime' && !!(value)) {
64
- props.runtime = runtimes(value[0])
65
- }
66
62
  else {
67
63
  props[name] = value[0]
68
64
  }
@@ -33,7 +33,7 @@ let get = {
33
33
  module.exports = function populateScheduled ({ item, dir, cwd, errors }) {
34
34
  let rate = null
35
35
  let cron = null
36
- if (is.array(item) && item.length >= 3) {
36
+ if (is.array(item)) {
37
37
  let name = item[0]
38
38
 
39
39
  // Hacky but it works
@@ -52,9 +52,17 @@ module.exports = function populateScheduled ({ item, dir, cwd, errors }) {
52
52
  else if (is.object(item)) {
53
53
  let name = Object.keys(item)[0]
54
54
 
55
- // Handle rate + cron
56
- if (item[name].rate) rate = get.rate(item[name].rate.join(' '))
57
- if (item[name].cron) cron = get.cron(item[name].cron.join(' '))
55
+ // Handle rate + cron props
56
+ if (item[name].rate) {
57
+ let itemRate = item[name].rate
58
+ let exp = is.array(itemRate) ? itemRate.join(' ') : itemRate
59
+ rate = get.rate(exp)
60
+ }
61
+ if (item[name].cron) {
62
+ let itemCron = item[name].cron
63
+ let exp = is.array(itemCron) ? itemCron.join(' ') : itemCron
64
+ cron = get.cron(exp)
65
+ }
58
66
 
59
67
  let src = item[name].src
60
68
  ? join(cwd, item[name].src)
@@ -0,0 +1,27 @@
1
+ let is = require('../../../lib/is')
2
+ let { aliases, runtimes, runtimeList } = require('lambda-runtimes')
3
+
4
+ // Runtime interpolater
5
+ module.exports = function getRuntime (config) {
6
+ let { runtime } = config
7
+
8
+ if (runtimeList.includes(runtime) || runtime === 'deno') {
9
+ return config
10
+ }
11
+
12
+ if (typeof runtime === 'string') {
13
+ runtime = runtime.toLowerCase()
14
+
15
+ // Runtime is not actually an AWS value, but a shorthand/aliased name
16
+ if (aliases[runtime]) {
17
+ let aliased = aliases[runtime]
18
+ config.runtime = runtimes[aliased][0]
19
+ config.runtimeAlias = runtime
20
+ }
21
+ }
22
+ else if (is.defined(runtime)) {
23
+ // Someone did something funky like specify a number or bool, so coerce and let it fail validation
24
+ config.runtime = `${config.runtime}`
25
+ }
26
+ return config
27
+ }
@@ -1,4 +1,5 @@
1
1
  let read = require('../../../read')
2
+ let getRuntime = require('./get-runtime')
2
3
  let getHandler = require('./get-handler')
3
4
  let upsert = require('../../_upsert')
4
5
  let is = require('../../../lib/is')
@@ -58,6 +59,9 @@ function populateLambda (type, pragma, inventory, errors) {
58
59
  config = upsert(config, arcConfig.arc)
59
60
  }
60
61
 
62
+ // Interpolate runtimes
63
+ config = getRuntime(config)
64
+
61
65
  // Tidy up any irrelevant params
62
66
  if (type !== 'http') {
63
67
  delete config.apigateway
@@ -53,7 +53,6 @@ function validateCron (schedule, errors) {
53
53
  if (!year.toString().match(minHrYr)) expErr('year', year)
54
54
  }
55
55
 
56
-
57
56
  let singular = [ 'minute', 'hour', 'day' ]
58
57
  let plural = [ 'minutes', 'hours', 'days' ]
59
58
  function validateRate (schedule, errors) {
@@ -69,11 +68,7 @@ function validateRate (schedule, errors) {
69
68
 
70
69
  // Value must be a >0 number
71
70
  if (!is.number(value) || !(value > 0)) {
72
- expErr('rate value must be a number greater than 0', value)
73
- }
74
- // Value must be a whole number
75
- else if (!value.toString().match(/^\d+$/)) {
76
- expErr('rate value must be a whole number', value)
71
+ expErr('rate value must be a whole number greater than 0', value)
77
72
  }
78
73
  // Interval must be a string
79
74
  if (!is.string(interval)) {
package/src/lib/is.js CHANGED
@@ -4,9 +4,10 @@ module.exports = {
4
4
  // Types
5
5
  array: item => Array.isArray(item),
6
6
  bool: item => typeof item === 'boolean',
7
- number: item => typeof item === 'number',
7
+ number: item => Number.isInteger(item),
8
8
  object: item => typeof item === 'object' && !Array.isArray(item),
9
9
  string: item => typeof item === 'string',
10
+ defined: item => typeof item !== 'undefined' && item !== null,
10
11
  // Filesystem
11
12
  exists: path => existsSync(path),
12
13
  folder: path => existsSync(path) && lstatSync(path).isDirectory(),
@@ -0,0 +1,64 @@
1
+ let is = require('../lib/is')
2
+ let { lambdas } = require('../lib/pragmas')
3
+ let { aliases, runtimeList } = require('lambda-runtimes')
4
+ let allRuntimes = runtimeList.concat([ 'deno' ])
5
+
6
+ /**
7
+ * Configuration validator
8
+ */
9
+ module.exports = function configValidator (params, inventory, errors) {
10
+ let { runtime: globalRuntime, memory: globalMemory, timeout: globalTimeout } = inventory.aws
11
+
12
+ /**
13
+ * Global config
14
+ */
15
+ // Memory
16
+ if (is.defined(globalMemory) && invalidMemory(globalMemory)) {
17
+ errors.push(invalidMemoryMsg(`${globalMemory} MB (@aws)`))
18
+ }
19
+ // Runtime
20
+ if ((globalRuntime && !is.string(globalRuntime)) ||
21
+ (globalRuntime && !allRuntimes.includes(globalRuntime) &&
22
+ !aliases[globalRuntime] && !aliases[globalRuntime.toLowerCase()])) {
23
+ errors.push(`Invalid project-level runtime: ${globalRuntime}`)
24
+ }
25
+ // Timeout
26
+ if (is.defined(globalTimeout) && invalidTimeout(globalTimeout)) {
27
+ errors.push(invalidTimeoutMsg(`${globalTimeout} seconds (@aws)`))
28
+ }
29
+
30
+ /**
31
+ * Lambda config
32
+ */
33
+ lambdas.forEach(p => {
34
+ let pragma = inventory[p]
35
+ if (pragma) pragma.forEach(({ name, config }) => {
36
+ let { memory, runtime, timeout } = config
37
+
38
+ // Memory
39
+ if (invalidMemory(memory) && memory !== globalMemory) {
40
+ errors.push(invalidMemoryMsg(`${memory} MB (@${p} ${name})`))
41
+ }
42
+ // Runtime
43
+ if (!allRuntimes.includes(runtime) && runtime !== globalRuntime) {
44
+ errors.push(`Invalid runtime: ${runtime} (@${p} ${name})`)
45
+ }
46
+ // Timeout
47
+ if (invalidTimeout(timeout) && timeout !== globalTimeout) {
48
+ errors.push(invalidTimeoutMsg(`${timeout} seconds (@${p} ${name})`))
49
+ }
50
+ })
51
+ })
52
+ }
53
+
54
+ // Memory
55
+ let minMemory = 128
56
+ let maxMemory = 10240
57
+ let invalidMemory = memory => !is.number(memory) || (memory < minMemory) || (memory > maxMemory)
58
+ let invalidMemoryMsg = info => `Invalid Lambda memory setting: ${info}, memory must be between ${minMemory} - ${maxMemory} MB`
59
+
60
+ // Timeout
61
+ let minTimeout = 1
62
+ let maxTimeout = 1 * 60 * 15 // 15 mins
63
+ let invalidTimeout = timeout => !is.number(timeout) || (timeout < minTimeout) || (timeout > maxTimeout)
64
+ let invalidTimeoutMsg = info => `Invalid Lambda timeout setting: ${info}, timeout must be between ${minTimeout} - ${maxTimeout} seconds`
@@ -1,4 +1,4 @@
1
- let runtimes = require('./runtimes')
1
+ let config = require('./config')
2
2
  let layers = require('./layers')
3
3
  let tablesChildren = require('./tables-children')
4
4
  let errorFmt = require('../lib/error-fmt')
@@ -12,9 +12,8 @@ module.exports = function finalValidation (params, inventory) {
12
12
  /**
13
13
  * Deal with vendor configuration errors
14
14
  */
15
-
16
- // Blow up on any non-matching runtimes
17
- runtimes(params, inventory, errors)
15
+ // Analyze function configuration
16
+ config(params, inventory, errors)
18
17
 
19
18
  // Ensure layer configuration will work, AWS blows up with awful errors on this
20
19
  layers(params, inventory, errors)
@@ -1,12 +1,14 @@
1
1
  let { sep } = require('path')
2
2
  let { lambdas } = require('../lib/pragmas')
3
- let validateARN = require('./arn')
3
+ let is = require('../lib/is')
4
+ let plural = arr => arr.length > 1 ? 's' : ''
4
5
 
5
6
  /**
6
7
  * Layer validator
7
8
  */
8
9
  module.exports = function layerValidator (params, inventory, errors) {
9
- let { region } = inventory.aws
10
+ let { _project } = inventory
11
+ let { region, layers: globalLayers } = inventory.aws
10
12
  let { cwd, validateLayers = true } = params
11
13
 
12
14
  // Shouldn't be possible because we backfill region, but jic
@@ -15,39 +17,69 @@ module.exports = function layerValidator (params, inventory, errors) {
15
17
  // Allow for manual opt-out of layer validation
16
18
  if (!validateLayers) return
17
19
 
18
- // Walk the tree of layer configs, starting with @aws
19
- Object.entries(inventory).forEach(([ i ]) => {
20
- let item = inventory[i]
21
- if (i === 'aws') {
22
- let location = inventory._project.manifest &&
23
- inventory._project.manifest.replace(cwd, '')
24
- let layers = item.layers
25
- validateLayer({ layers, region, location })
26
- }
27
- else if (lambdas.includes(i) && item) {
28
- item.forEach(entry => {
29
- // Probably unnecessary if no configFile is present but why not, let's be extra safe
30
- let location = entry.configFile && entry.configFile.replace(cwd, '')
31
- let layers = entry.config.layers
32
- validateLayer({ layers, region, location })
33
- })
34
- }
20
+ /**
21
+ * Global config
22
+ */
23
+ let location = _project?.manifest?.replace(cwd, '')
24
+ validateLayer({ layers: globalLayers, region, location, errors })
25
+
26
+ /**
27
+ * Lambda config
28
+ */
29
+ lambdas.forEach(p => {
30
+ let pragma = inventory[p]
31
+ if (pragma) pragma.forEach(({ config, configFile }) => {
32
+ let location = configFile?.replace(cwd, '')
33
+ validateLayer({ layers: config.layers, region, location, errors })
34
+ })
35
35
  })
36
+ }
36
37
 
37
- function validateLayer ({ layers, region, location }) {
38
- let loc = location && location.startsWith(sep) ? location.substr(1) : location
39
- let lambda = loc ? ` - Lambda: ${loc}\n` : ''
40
- if (!layers || !layers.length) return
41
- else {
42
- if (layers.length > 5) {
43
- let list = ` - Layers:\n ${layers.map(l => ` - ${l}`).join('\n')}`
44
- errors.push(`Lambda can only be configured with up to 5 layers\n${lambda}${list}`)
45
- }
46
- // CloudFormation fails without a helpful error if any layers aren't in the same region as the app because CloudFormation
47
- for (let arn of layers) {
48
- let arnError = validateARN({ arn, region, loc })
49
- if (arnError) errors.push(arnError)
50
- }
38
+ function validateLayer ({ layers, region, location, errors }) {
39
+ let loc = location && location.startsWith(sep) ? location.substr(1) : location
40
+ let config = loc ? ` (${loc})` : ''
41
+ if (!layers || !layers.length) return
42
+ else {
43
+ if (layers.length > 5) {
44
+ let list = `${layers.map(l => ` - ${l}`).join('\n')}`
45
+ errors.push(`Lambdas can only be configured with up to 5 layers, got ${layers.length} layers${config}:\n${list}`)
51
46
  }
47
+ // CloudFormation fails without a helpful error if any layers aren't in the same region as the app because CloudFormation
48
+ let arnErrors = validateARN({ layers, region, config })
49
+ if (arnErrors) errors.push(arnErrors)
50
+ }
51
+ }
52
+
53
+ // Validates Lambda layer / policy ARNs, prob can't be used for other kinds of ARN
54
+ function validateARN ({ layers, region, config }) {
55
+ let invalidArns = []
56
+ let badRegions = []
57
+ layers.forEach(arn => {
58
+ let parts = is.string(arn) && arn.split(':')
59
+ // Invalid
60
+ if (!is.string(arn) ||
61
+ !arn.startsWith('arn:') ||
62
+ parts.length !== 8) {
63
+ return invalidArns.push(` - ${arn}`)
64
+ }
65
+ // Bad region
66
+ let layerRegion = parts[3]
67
+ if (region !== layerRegion) {
68
+ badRegions.push(
69
+ ` - Layer ARN: ${arn}\n` +
70
+ ` - Layer region: ${layerRegion}`
71
+ )
72
+ }
73
+ })
74
+ let err = ''
75
+ if (invalidArns.length) {
76
+ err += `Invalid ARN${plural(invalidArns)}${config}:\n` +
77
+ invalidArns.join('\n')
78
+ }
79
+ if (badRegions.length) {
80
+ err += `Layer${plural(badRegions)} ` +
81
+ `not in app's region of ${region}${config}:\n` +
82
+ badRegions.join('\n')
52
83
  }
84
+ if (err) return err
53
85
  }
@@ -1,84 +0,0 @@
1
- // Canonical runtime list: https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html
2
- // Array order matters, newest (or most preferable) must always be at the top
3
- let runtimes = {
4
- node: [
5
- 'nodejs14.x',
6
- 'nodejs12.x',
7
- 'nodejs10.x',
8
- ],
9
- python: [
10
- 'python3.9',
11
- 'python3.8',
12
- 'python3.7',
13
- 'python3.6',
14
- 'python2.7',
15
- ],
16
- ruby: [
17
- 'ruby2.7',
18
- 'ruby2.5',
19
- ],
20
- java: [
21
- 'java11',
22
- 'java8.al2',
23
- 'java8',
24
- ],
25
- go: [
26
- 'go1.x',
27
- ],
28
- dotnet: [
29
- 'dotnetcore3.1',
30
- 'dotnetcore2.1',
31
- ],
32
- custom: [
33
- 'provided.al2',
34
- 'provided',
35
- ],
36
- // Arc specific
37
- deno: [
38
- 'deno'
39
- ],
40
- }
41
-
42
- // Human friendly shortcuts
43
- let nodes = [ 'node', 'nodejs', 'node.js' ]
44
- let pythons = [ 'python', 'py' ]
45
- let rubies = [ 'ruby', 'rb' ]
46
- let javas = [ 'java' ]
47
- let gos = [ 'go', 'golang' ]
48
- let dotnets = [ 'dotnet', '.net' ]
49
- let customs = [ 'custom' ]
50
-
51
- // Runtime interpolater
52
- function getRuntime (name) {
53
- if (typeof name === 'string') {
54
- name = name.toLowerCase()
55
-
56
- if (nodes.includes(name)) return runtimes.node[0]
57
- if (runtimes.node.includes(name)) return name
58
-
59
- if (pythons.includes(name)) return runtimes.python[0]
60
- if (runtimes.python.includes(name)) return name
61
-
62
- if (rubies.includes(name)) return runtimes.ruby[0]
63
- if (runtimes.ruby.includes(name)) return name
64
-
65
- if (javas.includes(name)) return runtimes.java[0]
66
- if (runtimes.java.includes(name)) return name
67
-
68
- if (gos.includes(name)) return runtimes.go[0]
69
- if (runtimes.go.includes(name)) return name
70
-
71
- if (dotnets.includes(name)) return runtimes.dotnet[0]
72
- if (runtimes.dotnet.includes(name)) return name
73
-
74
- if (customs.includes(name)) return runtimes.custom[0]
75
- if (runtimes.custom.includes(name)) return name
76
-
77
- if (runtimes.deno.includes(name)) return name
78
-
79
- return name // Will be validated later
80
- }
81
- }
82
-
83
- getRuntime.runtimes = runtimes
84
- module.exports = getRuntime
@@ -1,24 +0,0 @@
1
- let is = require('../lib/is')
2
-
3
- // Validates Lambda layer / policy ARNs, prob can't be used for other kinds of ARN
4
- module.exports = function validateARN ({ arn, region, loc }) {
5
- if (!is.string(arn) ||
6
- !arn.startsWith('arn:') ||
7
- arn.split(':').length !== 8) {
8
- /* istanbul ignore next */
9
- let lambda = loc ? `in ${loc}` : ''
10
- return `Invalid ARN${lambda}: ${arn}`
11
- }
12
-
13
- let parts = arn.split(':')
14
- let layerRegion = parts[3]
15
- if (region !== layerRegion) {
16
- /* istanbul ignore next */
17
- let lambda = loc ? ` - Lambda: ${loc}\n` : ''
18
- let err = `Lambda layers must be in the same region as app\n` + lambda +
19
- ` - App region: ${region}\n` +
20
- ` - Layer ARN: ${arn}\n` +
21
- ` - Layer region: ${layerRegion}`
22
- return err
23
- }
24
- }
@@ -1,28 +0,0 @@
1
- let { lambdas } = require('../lib/pragmas')
2
- let { runtimes } = require('../lib/runtimes')
3
-
4
- /**
5
- * Runtime validator
6
- */
7
- module.exports = function runtimeValidator (params, inventory, errors) {
8
-
9
- let allRuntimes = Object.keys(runtimes).reduce((a, k) => a.concat(runtimes[k]), [])
10
- let globalRuntime = inventory.aws?.runtime
11
- if (globalRuntime && !allRuntimes.includes(globalRuntime)) {
12
- errors.push(`Invalid project-level runtime: ${globalRuntime}`)
13
- }
14
-
15
- // Walk the tree of layer configs, starting with @aws
16
- Object.keys(inventory).forEach(i => {
17
- let item = inventory[i]
18
- if (lambdas.includes(i) && item) {
19
- item.forEach(entry => {
20
- let runtime = entry.config.runtime
21
- if (runtime === globalRuntime) return
22
- if (!allRuntimes.includes(runtime)) {
23
- errors.push(`Invalid runtime: ${runtime} (@${i} ${entry.name})`)
24
- }
25
- })
26
- }
27
- })
28
- }