From b37d16286b7c2ce83ae4e74ca81b6dea500164f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=A9?= Date: Mon, 21 Nov 2022 06:07:41 +0100 Subject: [PATCH] Define More Details Schema for Plugin Config (#49) * Define More Details Schema for Plugin Config * More tests and better regex * Fix comments --- README.md | 9 +- __tests__/helpers/config.ts | 10 ++ __tests__/helpers/invalidConfigs.ts | 47 ++++++ __tests__/schedule.test.ts | 229 ++++++++++++++++++++++++++++ package-lock.json | 194 +++++++++++++++-------- package.json | 3 +- src/plugin.ts | 9 +- src/schema/schema.ts | 135 ++++++++++++++++ 8 files changed, 561 insertions(+), 75 deletions(-) create mode 100644 __tests__/helpers/invalidConfigs.ts create mode 100644 __tests__/schedule.test.ts create mode 100644 src/schema/schema.ts diff --git a/README.md b/README.md index 234d1a2..fcdee82 100644 --- a/README.md +++ b/README.md @@ -33,17 +33,18 @@ Add `concurrencyAutoscaling` parameters under each function you wish to autoscal Add `customMetric: true` if you want to use `Maximum` instead of `Average` statistic. +### Minimal Configuration ```yaml -# minimal configuration - functions: hello: handler: handler.hello provisionedConcurrency: 1 concurrencyAutoscaling: true +``` - # full configuration - +### Full Configuration +```yaml +functions: world: handler: handler.world provisionedConcurrency: 1 diff --git a/__tests__/helpers/config.ts b/__tests__/helpers/config.ts index a4de13e..39a957b 100644 --- a/__tests__/helpers/config.ts +++ b/__tests__/helpers/config.ts @@ -65,3 +65,13 @@ export const configScheduledActions: AutoscalingConfig = { } as ScheduledAction ] as ScheduledAction[] } + +export const allConfigs = [ + configMin, + configPartial, + configZero, + configDefault, + configCustomMetricMin, + configCustomMetricDefault, + configScheduledActions, +] as AutoscalingConfig[] diff --git a/__tests__/helpers/invalidConfigs.ts b/__tests__/helpers/invalidConfigs.ts new file mode 100644 index 0000000..9fe152f --- /dev/null +++ b/__tests__/helpers/invalidConfigs.ts @@ -0,0 +1,47 @@ +import { configMin } from './config'; +import { AutoscalingConfig, CustomMetricConfig, ScheduledAction } from '../../src/@types'; + +export const userDefinedConfig = ( + enabled: unknown = true, + alias: unknown = 'provisioned', + maximum?: unknown, + minimum?: unknown, + usage?: unknown, + scaleInCooldown?: unknown, + scaleOutCooldown?: unknown +): AutoscalingConfig => ({ + ...configMin, + enabled, + alias, + maximum, + minimum, + usage, + scaleInCooldown, + scaleOutCooldown +} as unknown as AutoscalingConfig); + +export const customMetricConfig = ( + statistic?: unknown +): AutoscalingConfig => ({ + ...userDefinedConfig(), + customMetric: { + statistic + } as CustomMetricConfig +} as unknown as AutoscalingConfig); + +export const customScheduledActionsConfig = ( + scheduledActions: unknown +): AutoscalingConfig => ({ + ...configMin, + scheduledActions +} as unknown as AutoscalingConfig); + +export const customScheduledAction = ( + name: unknown = 'scheduledActionRequiredName', + schedule: unknown = 'cron(30 8 ? * 1-6 *)', + action: unknown = { minimum: 1 } +): ScheduledAction => ({ + name, + schedule, + action +} as unknown as ScheduledAction); diff --git a/__tests__/schedule.test.ts b/__tests__/schedule.test.ts new file mode 100644 index 0000000..77edb5f --- /dev/null +++ b/__tests__/schedule.test.ts @@ -0,0 +1,229 @@ +import { allConfigs, configMin } from './helpers/config'; +import { AutoscalingConfig, ConcurrencyFunction, ScalableTargetAction, ScheduledAction } from '../src/@types'; +import Ajv from 'ajv-draft-04'; +import { schema } from '../src/schema/schema'; +import { + customMetricConfig, + customScheduledAction, + customScheduledActionsConfig, + userDefinedConfig +} from './helpers/invalidConfigs'; + +describe('Schema Validation', (): void => { + const validate = new Ajv().compile(schema); + + describe('Positive Schema Validation', (): void => { + it.each([true, false])('All Boolean Configs are Valid', ( + config: boolean + ): void => { + const result = validate(functionConfig(config)); + expect(result).toEqual(true); + }); + + const generateAllCustomStatisticsTestCases = (): AutoscalingConfig[] => { + const testCases: AutoscalingConfig[] = []; + const statistics: string[] = [ + 'Average', 'Maximum', 'Minimum', 'SampleCount', 'Sum', + 'average', 'maximum', 'minimum', 'sampleCount', 'sum' + ]; + + for (const statistic of statistics) { + testCases.push(customMetricConfig(statistic)); + } + + return testCases; + }; + + it.each([ + ...allConfigs, + userDefinedConfig(), + userDefinedConfig(true, 'scaleOutTest', 10, 1, 0.75, 0, 0), + ...generateAllCustomStatisticsTestCases(), + customScheduledActionsConfig([customScheduledAction()]) + ])('All Test Configs Should be Valid', ( + config: AutoscalingConfig + ): void => { + const result = validate(functionConfig(config)); + expect(result).toEqual(true); + }); + + it.each([ + { minimum: 1 } as ScalableTargetAction, + { maximum: 1 } as ScalableTargetAction, + { minimum: 1, maximum: 2 } as ScalableTargetAction, + { minimum: 2, maximum: 1 } as ScalableTargetAction, + ])('Positive Schema Validation - Scheduled Actions', ( + action: ScalableTargetAction + ): void => { + const result = validate(schedulesActionConfig(action)); + expect(result).toEqual(true); + }); + + const generateValidScheduleTestCases = (): AutoscalingConfig[] => { + const testCases: AutoscalingConfig[] = []; + const schedules = [ + 'rate(1 minute)', 'rate(2 minutes)', 'rate(42 minutes)', + 'rate(1 hour)', 'rate(2 hours)', 'rate(42 hours)', + 'rate(1 day)', 'rate(2 days)', 'rate(42 days)', + 'at(2025-01-02T12:12:12)', 'at(2000-02-29T23:59:59)', + 'cron(30 17 ? * 1-6 *)', 'cron(/ / / / # /)', 'cron(/ / L / L /)', 'cron(/ / W / L /)', + 'cron(* * * * * *)', 'cron(* * ? * ? *)', 'cron(1 2 3 4 5 6)', 'cron(- - - - - -)', 'cron(, , , , , ,)', + 'cron(1-2 2-3 3-4 4-5 5-6 6-7)' + ]; + + for (const schedule of schedules) { + testCases.push(customScheduledActionsConfig([customScheduledAction('name', schedule, { minimum: 1 } as ScalableTargetAction)])); + } + + return testCases; + }; + + it.each(generateValidScheduleTestCases())('Positive Schedule Validation', (config: AutoscalingConfig): void => { + const result = validate(functionConfig(config)); + expect(result).toEqual(true); + }); + }); + + describe('Negative Schema Validation', (): void => { + const generateMinMaxInOutTestCases = (): AutoscalingConfig[] => { + const testCases: AutoscalingConfig[] = []; + const values = [-1, NaN, '42', { isNumber: false }, 0.75]; + + for (const value of values) { + testCases.push(userDefinedConfig(true, 'maxTest', value)); + testCases.push(userDefinedConfig(true, 'minTest', 10, value)); + testCases.push(userDefinedConfig(true, 'scaleInTest', 10, 1, 0.75, value)); + testCases.push(userDefinedConfig(true, 'scaleOutTest', 10, 1, 0.75, 0, value)); + } + + return testCases; + }; + + const generateUsageTestCases = (): AutoscalingConfig[] => { + const testCases: AutoscalingConfig[] = []; + const values = [-1, NaN, '42', { isObject: true }, 'random', '', null]; + + for (const value of values) { + testCases.push(userDefinedConfig(true, 'minTest', 10, 1, value)); + testCases.push(customMetricConfig(value)); + testCases.push(customScheduledActionsConfig(value)); + testCases.push(customScheduledActionsConfig([value])); + } + + return testCases; + }; + + const generateScheduledActionTestCases = (): AutoscalingConfig[] => { + const testCases: AutoscalingConfig[] = []; + const values = [-1, NaN, { isObject: true }, '', null]; + + testCases.push(customScheduledActionsConfig([])); + for (const value of values) { + testCases.push(customScheduledActionsConfig([customScheduledAction(value)])); + testCases.push(customScheduledActionsConfig([customScheduledAction('name', value)])); + testCases.push(customScheduledActionsConfig([customScheduledAction('name', 'cron(30 8 ? * 1-6 *)', value)])); + } + + return testCases; + }; + + it.each([ + userDefinedConfig('abc'), + userDefinedConfig(true, ''), + userDefinedConfig(true, 1), + userDefinedConfig(true, { some: 'object' }), + ...generateMinMaxInOutTestCases(), + ...generateUsageTestCases(), + customMetricConfig(), + customScheduledActionsConfig([]), + ...generateScheduledActionTestCases() + ])('Invalid Configs Fails Schema Validation', ( + config: AutoscalingConfig + ): void => { + const result = validate(functionConfig(config)); + expect(result).toEqual(false); + }); + + it.each([ + { minimum: 1, avg: 1 } as ScalableTargetAction, + { maximum: 1, avg: 1 } as ScalableTargetAction, + { minimum: 1, maximum: 1, avg: 1 } as ScalableTargetAction, + ])('Negative Schema Validation - Scheduled Actions', ( + action: ScalableTargetAction + ): void => { + const result = validate(schedulesActionConfig(action)); + expect(result).toEqual(false); + }); + + type StartEndTimeType = { startTime: string, endTime: string }; + const generateInvalidStartAndEndTimes = (): StartEndTimeType[] => { + const testCases: StartEndTimeType[] = []; + const values: string[] = ['', '1', 'random']; + + for (const value in values) { + testCases.push({ startTime: value } as StartEndTimeType); + testCases.push({ endTime: value } as StartEndTimeType); + testCases.push({ startTime: value, endTime: value } as StartEndTimeType); + } + + return testCases; + }; + + it.each(generateInvalidStartAndEndTimes())('123', ({ startTime, endTime }): void => { + const result = validate(schedulesActionConfigWithStartAndEndTime(startTime, endTime)); + expect(result).toEqual(false); + }); + + const generateInvalidScheduleTestCases = (): AutoscalingConfig[] => { + const testCases: AutoscalingConfig[] = []; + const schedules = [ + 'rate(0 minute)', 'rate(0 hour)', 'rate(0 day)', + 'rate(a minute)', 'rate(b hour)', 'rate(c day)', + 'rate(11 asd)', + 'at(205-01-02T12:12:12)', 'at(2000-022-29T23:59:59)', 'at(2000-02-298T23:59:59)', 'at(2000-02-29T232:59:59)', 'at(2000-02-29T23:591:59)', 'at(2000-02-29T23:59:591)', + 'at(20225-01-02T12:12:12)', 'at(2000-2-29T23:59:59)', 'at(2000-02-2T23:59:59)', 'at(2000-02-29T3:59:59)', 'at(2000-02-29T23:9:59)', 'at(2000-02-29T23:59:5)', + 'at(2000:02-29T23:59:59)', 'at(2000-02:29T23:59:59)', 'at(2000-02-29T23-59:59)', 'at(2000-02-29T23:59-59)', 'at(2000-02-29Z23:59-59)', + 'cron(/ / / # / /)', 'cron(/ / 1 L 2 /)', 'cron(/ / L / W /)', + 'cron(* * ? ? ? *)', 'cron(a 2 3 4 5 6)' + ]; + + for (const schedule of schedules) { + testCases.push(customScheduledActionsConfig([customScheduledAction('name', schedule, { minimum: 1 } as ScalableTargetAction)])); + } + + return testCases; + }; + + it.each(generateInvalidScheduleTestCases())('Negative Schedule Validation', (config: AutoscalingConfig): void => { + const result = validate(functionConfig(config)); + expect(result).toEqual(false); + }); + }); + + const functionConfig = (concurrencyAutoscaling: AutoscalingConfig | boolean): ConcurrencyFunction => ({ + concurrencyAutoscaling, + provisionedConcurrency: 1, + } as ConcurrencyFunction); + + const schedulesActionConfig = (action: ScalableTargetAction) => ({ + concurrencyAutoscaling: { + ...configMin, + scheduledActions: [ + customScheduledAction('name', 'cron(30 8 ? * 1-6 *)', action) + ] as ScheduledAction[] + }, + provisionedConcurrency: 1, + } as ConcurrencyFunction); + + const schedulesActionConfigWithStartAndEndTime = (startTime?: string, endTime?: string) => ({ + concurrencyAutoscaling: { + ...configMin, + scheduledActions: [{ + ...customScheduledAction('name', 'cron(30 8 ? * 1-6 *)', { minimum: 1 }), + startTime, + endTime + }] as ScheduledAction[] + }, + provisionedConcurrency: 1, + } as ConcurrencyFunction); +}); diff --git a/package-lock.json b/package-lock.json index 1dee5c7..30accd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/serverless": "^1.78.44", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", + "ajv-draft-04": "^1.0.0", "eslint": "^7.32.0", "eslint-config-prettier": "^6.11.0", "eslint-plugin-jest": "^23.20.0", @@ -556,6 +557,28 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", @@ -949,9 +972,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "17.0.22", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.22.tgz", - "integrity": "sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw==", + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", "dev": true }, "node_modules/@types/prettier": { @@ -1167,13 +1190,14 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" }, "funding": { @@ -1181,6 +1205,20 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-colors": { "version": "4.1.1", "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", @@ -2017,6 +2055,22 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", @@ -2067,6 +2121,12 @@ "node": ">=4.0" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/espree": { "version": "7.3.1", "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", @@ -3357,8 +3417,9 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { @@ -3760,9 +3821,9 @@ } }, "node_modules/prettier": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.0.tgz", - "integrity": "sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -4193,26 +4254,6 @@ "node": ">=10.0.0" } }, - "node_modules/table/node_modules/ajv": { - "version": "8.9.0", - "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/terminal-link": { "version": "2.1.1", "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", @@ -5054,6 +5095,26 @@ "js-yaml": "^3.13.1", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "@humanwhocodes/config-array": { @@ -5387,9 +5448,9 @@ "dev": true }, "@types/node": { - "version": "17.0.22", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.22.tgz", - "integrity": "sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw==", + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", "dev": true }, "@types/prettier": { @@ -5524,16 +5585,24 @@ } }, "ajv": { - "version": "6.12.6", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, + "ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "requires": {} + }, "ansi-colors": { "version": "4.1.1", "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", @@ -6093,6 +6162,18 @@ "@babel/highlight": "^7.10.4" } }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "escape-string-regexp": { "version": "4.0.0", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", @@ -6126,6 +6207,12 @@ "dev": true } } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true } } }, @@ -7154,8 +7241,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.4.1", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, "json-stable-stringify-without-jsonify": { @@ -7457,9 +7545,9 @@ "dev": true }, "prettier": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.0.tgz", - "integrity": "sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", "dev": true }, "pretty-format": { @@ -7756,24 +7844,6 @@ "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ajv": { - "version": "8.9.0", - "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } } }, "terminal-link": { diff --git a/package.json b/package.json index 439ac34..5885c0f 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "jest": "^27.5.1", "prettier": "^2.6.0", "ts-jest": "^27.1.3", - "typescript": "^3.9.10" + "typescript": "^3.9.10", + "ajv-draft-04": "^1.0.0" } } diff --git a/src/plugin.ts b/src/plugin.ts index 6bce21f..5decb77 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -11,6 +11,7 @@ import { Options, CustomMetricConfig, } from './@types' +import { schema } from './schema/schema'; const text = { CLI_DONE: 'Added Provisioned Concurrency Auto Scaling to CloudFormation!', @@ -22,14 +23,6 @@ const text = { ONLY_AWS_SUPPORT: 'Only supported for AWS provider', } -const schema = { - properties: { - concurrencyAutoscaling: { - anyOf: [{ type: 'boolean' }, { type: 'object' }], - }, - }, -} - export default class Plugin { serverless: Serverless hooks: Record = {} diff --git a/src/schema/schema.ts b/src/schema/schema.ts new file mode 100644 index 0000000..6110335 --- /dev/null +++ b/src/schema/schema.ts @@ -0,0 +1,135 @@ +export const schema = { + type: 'object', + properties: { + provisionedConcurrency: { + type: 'integer', + minimum: 1, + }, + concurrencyAutoscaling: { + anyOf: [ + { + type: 'boolean' + }, + { + type: 'object', + properties: { + function: { + type: 'string', + minLength: 1 + }, + name: { + type: 'string', + minLength: 1 + }, + enabled: { + type: 'boolean' + }, + alias: { + type: 'string', + minLength: 1 + }, + maximum: { + type: 'integer', + minimum: 0 + }, + minimum: { + type: 'integer', + minimum: 0 + }, + usage: { + type: 'number', + minimum: 0, + }, + scaleInCooldown: { + type: 'integer', + minimum: 0 + }, + scaleOutCooldown: { + type: 'integer', + minimum: 0 + }, + customMetric: { + type: 'object', + properties: { + statistic: { + type: 'string', + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalingpolicy-customizedmetricspecification.html#cfn-applicationautoscaling-scalingpolicy-customizedmetricspecification-statistic + enum: ['Average', 'Maximum', 'Minimum', 'SampleCount', 'Sum', + 'average', 'maximum', 'minimum', 'sampleCount', 'sum'] + }, + }, + additionalProperties: true, + required: ['statistic'] + }, + scheduledActions: { + type: 'array', + minItems: 1, + items: { + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalabletarget-scheduledaction.html + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1 + }, + startTime: { + type: 'string', + minLength: 1, + pattern: '^\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2]\\d|3[0-1])T(?:[0-1]\\d|2[0-3]):[0-5]\\d:[0-5]\\dZ$' + }, + endTime: { + type: 'string', + minLength: 1, + pattern: '^\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2]\\d|3[0-1])T(?:[0-1]\\d|2[0-3]):[0-5]\\d:[0-5]\\dZ$' + }, + timezone: { + type: 'string', + enum: ["Etc/GMT+12", "Etc/GMT+11", "Pacific/Midway", "Pacific/Niue", "Pacific/Pago_Pago", "America/Adak", "Etc/GMT+10", "HST", "Pacific/Honolulu", "Pacific/Rarotonga", "Pacific/Tahiti", "Pacific/Marquesas", "America/Anchorage", "America/Juneau", "America/Metlakatla", "America/Nome", "America/Sitka", "America/Yakutat", "Etc/GMT+9", "Pacific/Gambier", "America/Dawson", "America/Los_Angeles", "America/Tijuana", "America/Vancouver", "America/Whitehorse", "Etc/GMT+8", "PST8PDT", "Pacific/Pitcairn", "America/Boise", "America/Cambridge_Bay", "America/Chihuahua", "America/Creston", "America/Dawson_Creek", "America/Denver", "America/Edmonton", "America/Fort_Nelson", "America/Hermosillo", "America/Inuvik", "America/Mazatlan", "America/Ojinaga", "America/Phoenix", "America/Yellowknife", "Etc/GMT+7", "MST", "MST7MDT", "America/Bahia_Banderas", "America/Belize", "America/Chicago", "America/Costa_Rica", "America/El_Salvador", "America/Guatemala", "America/Indiana/Knox", "America/Indiana/Tell_City", "America/Managua", "America/Matamoros", "America/Menominee", "America/Merida", "America/Mexico_City", "America/Monterrey", "America/North_Dakota/Beulah", "America/North_Dakota/Center", "America/North_Dakota/New_Salem", "America/Rainy_River", "America/Rankin_Inlet", "America/Regina", "America/Resolute", "America/Swift_Current", "America/Tegucigalpa", "America/Winnipeg", "CST6CDT", "Etc/GMT+6", "Pacific/Easter", "Pacific/Galapagos", "America/Atikokan", "America/Bogota", "America/Cancun", "America/Cayman", "America/Detroit", "America/Eirunepe", "America/Grand_Turk", "America/Guayaquil", "America/Havana", "America/Indiana/Indianapolis", "America/Indiana/Marengo", "America/Indiana/Petersburg", "America/Indiana/Vevay", "America/Indiana/Vincennes", "America/Indiana/Winamac", "America/Iqaluit", "America/Jamaica", "America/Kentucky/Louisville", "America/Kentucky/Monticello", "America/Lima", "America/Nassau", "America/New_York", "America/Nipigon", "America/Panama", "America/Pangnirtung", "America/Port-au-Prince", "America/Rio_Branco", "America/Thunder_Bay", "America/Toronto", "EST", "EST5EDT", "Etc/GMT+5", "America/Anguilla", "America/Antigua", "America/Aruba", "America/Asuncion", "America/Barbados", "America/Blanc-Sablon", "America/Boa_Vista", "America/Campo_Grande", "America/Caracas", "America/Cuiaba", "America/Curacao", "America/Dominica", "America/Glace_Bay", "America/Goose_Bay", "America/Grenada", "America/Guadeloupe", "America/Guyana", "America/Halifax", "America/Kralendijk", "America/La_Paz", "America/Lower_Princes", "America/Manaus", "America/Marigot", "America/Martinique", "America/Moncton", "America/Montserrat", "America/Port_of_Spain", "America/Porto_Velho", "America/Puerto_Rico", "America/Santiago", "America/Santo_Domingo", "America/St_Barthelemy", "America/St_Kitts", "America/St_Lucia", "America/St_Thomas", "America/St_Vincent", "America/Thule", "America/Tortola", "Atlantic/Bermuda", "Etc/GMT+4", "America/St_Johns", "America/Araguaina", "America/Argentina/Buenos_Aires", "America/Argentina/Catamarca", "America/Argentina/Cordoba", "America/Argentina/Jujuy", "America/Argentina/La_Rioja", "America/Argentina/Mendoza", "America/Argentina/Rio_Gallegos", "America/Argentina/Salta", "America/Argentina/San_Juan", "America/Argentina/San_Luis", "America/Argentina/Tucuman", "America/Argentina/Ushuaia", "America/Bahia", "America/Belem", "America/Cayenne", "America/Fortaleza", "America/Godthab", "America/Maceio", "America/Miquelon", "America/Montevideo", "America/Paramaribo", "America/Punta_Arenas", "America/Recife", "America/Santarem", "America/Sao_Paulo", "Antarctica/Palmer", "Antarctica/Rothera", "Atlantic/Stanley", "Etc/GMT+3", "America/Noronha", "Atlantic/South_Georgia", "Etc/GMT+2", "America/Scoresbysund", "Atlantic/Azores", "Atlantic/Cape_Verde", "Etc/GMT+1", "Africa/Abidjan", "Africa/Accra", "Africa/Bamako", "Africa/Banjul", "Africa/Bissau", "Africa/Casablanca", "Africa/Conakry", "Africa/Dakar", "Africa/El_Aaiun", "Africa/Freetown", "Africa/Lome", "Africa/Monrovia", "Africa/Nouakchott", "Africa/Ouagadougou", "America/Danmarkshavn", "Antarctica/Troll", "Atlantic/Canary", "Atlantic/Faroe", "Atlantic/Madeira", "Atlantic/Reykjavik", "Atlantic/St_Helena", "Etc/GMT", "Etc/UCT", "Etc/UTC", "Europe/Dublin", "Europe/Guernsey", "Europe/Isle_of_Man", "Europe/Jersey", "Europe/Lisbon", "Europe/London", "UTC", "WET", "Africa/Algiers", "Africa/Bangui", "Africa/Brazzaville", "Africa/Ceuta", "Africa/Douala", "Africa/Kinshasa", "Africa/Lagos", "Africa/Libreville", "Africa/Luanda", "Africa/Malabo", "Africa/Ndjamena", "Africa/Niamey", "Africa/Porto-Novo", "Africa/Sao_Tome", "Africa/Tunis", "Africa/Windhoek", "Arctic/Longyearbyen", "CET", "Etc/GMT-1", "Europe/Amsterdam", "Europe/Andorra", "Europe/Belgrade", "Europe/Berlin", "Europe/Bratislava", "Europe/Brussels", "Europe/Budapest", "Europe/Busingen", "Europe/Copenhagen", "Europe/Gibraltar", "Europe/Ljubljana", "Europe/Luxembourg", "Europe/Madrid", "Europe/Malta", "Europe/Monaco", "Europe/Oslo", "Europe/Paris", "Europe/Podgorica", "Europe/Prague", "Europe/Rome", "Europe/San_Marino", "Europe/Sarajevo", "Europe/Skopje", "Europe/Stockholm", "Europe/Tirane", "Europe/Vaduz", "Europe/Vatican", "Europe/Vienna", "Europe/Warsaw", "Europe/Zagreb", "Europe/Zurich", "MET", "Africa/Blantyre", "Africa/Bujumbura", "Africa/Cairo", "Africa/Gaborone", "Africa/Harare", "Africa/Johannesburg", "Africa/Khartoum", "Africa/Kigali", "Africa/Lubumbashi", "Africa/Lusaka", "Africa/Maputo", "Africa/Maseru", "Africa/Mbabane", "Africa/Tripoli", "Asia/Amman", "Asia/Beirut", "Asia/Damascus", "Asia/Famagusta", "Asia/Gaza", "Asia/Hebron", "Asia/Jerusalem", "Asia/Nicosia", "EET", "Etc/GMT-2", "Europe/Athens", "Europe/Bucharest", "Europe/Chisinau", "Europe/Helsinki", "Europe/Kaliningrad", "Europe/Kiev", "Europe/Mariehamn", "Europe/Nicosia", "Europe/Riga", "Europe/Sofia", "Europe/Tallinn", "Europe/Uzhgorod", "Europe/Vilnius", "Europe/Zaporozhye", "Africa/Addis_Ababa", "Africa/Asmara", "Africa/Dar_es_Salaam", "Africa/Djibouti", "Africa/Juba", "Africa/Kampala", "Africa/Mogadishu", "Africa/Nairobi", "Antarctica/Syowa", "Asia/Aden", "Asia/Baghdad", "Asia/Bahrain", "Asia/Istanbul", "Asia/Kuwait", "Asia/Qatar", "Asia/Riyadh", "Etc/GMT-3", "Europe/Istanbul", "Europe/Kirov", "Europe/Minsk", "Europe/Moscow", "Europe/Simferopol", "Indian/Antananarivo", "Indian/Comoro", "Indian/Mayotte", "Asia/Tehran", "Asia/Baku", "Asia/Dubai", "Asia/Muscat", "Asia/Tbilisi", "Asia/Yerevan", "Etc/GMT-4", "Europe/Astrakhan", "Europe/Samara", "Europe/Saratov", "Europe/Ulyanovsk", "Europe/Volgograd", "Indian/Mahe", "Indian/Mauritius", "Indian/Reunion", "Asia/Kabul", "Antarctica/Mawson", "Asia/Aqtau", "Asia/Aqtobe", "Asia/Ashgabat", "Asia/Atyrau", "Asia/Dushanbe", "Asia/Karachi", "Asia/Oral", "Asia/Samarkand", "Asia/Tashkent", "Asia/Yekaterinburg", "Etc/GMT-5", "Indian/Kerguelen", "Indian/Maldives", "Asia/Colombo", "Asia/Kolkata", "Asia/Kathmandu", "Antarctica/Vostok", "Asia/Almaty", "Asia/Bishkek", "Asia/Dhaka", "Asia/Omsk", "Asia/Qyzylorda", "Asia/Thimphu", "Asia/Urumqi", "Etc/GMT-6", "Indian/Chagos", "Asia/Yangon", "Indian/Cocos", "Antarctica/Davis", "Asia/Bangkok", "Asia/Barnaul", "Asia/Ho_Chi_Minh", "Asia/Hovd", "Asia/Jakarta", "Asia/Krasnoyarsk", "Asia/Novokuznetsk", "Asia/Novosibirsk", "Asia/Phnom_Penh", "Asia/Pontianak", "Asia/Tomsk", "Asia/Vientiane", "Etc/GMT-7", "Indian/Christmas", "Antarctica/Casey", "Asia/Brunei", "Asia/Choibalsan", "Asia/Hong_Kong", "Asia/Irkutsk", "Asia/Kuala_Lumpur", "Asia/Kuching", "Asia/Macau", "Asia/Makassar", "Asia/Manila", "Asia/Shanghai", "Asia/Singapore", "Asia/Taipei", "Asia/Ulaanbaatar", "Australia/Perth", "Etc/GMT-8", "Australia/Eucla", "Asia/Chita", "Asia/Dili", "Asia/Jayapura", "Asia/Khandyga", "Asia/Pyongyang", "Asia/Seoul", "Asia/Tokyo", "Asia/Yakutsk", "Etc/GMT-9", "Pacific/Palau", "Australia/Adelaide", "Australia/Broken_Hill", "Australia/Darwin", "Antarctica/DumontDUrville", "Asia/Ust-Nera", "Asia/Vladivostok", "Australia/Brisbane", "Australia/Currie", "Australia/Hobart", "Australia/Lindeman", "Australia/Melbourne", "Australia/Sydney", "Etc/GMT-10", "Pacific/Chuuk", "Pacific/Guam", "Pacific/Port_Moresby", "Pacific/Saipan", "Australia/Lord_Howe", "Antarctica/Macquarie", "Asia/Magadan", "Asia/Sakhalin", "Asia/Srednekolymsk", "Etc/GMT-11", "Pacific/Bougainville", "Pacific/Efate", "Pacific/Guadalcanal", "Pacific/Kosrae", "Pacific/Norfolk", "Pacific/Noumea", "Pacific/Pohnpei", "Antarctica/McMurdo", "Asia/Anadyr", "Asia/Kamchatka", "Etc/GMT-12", "Pacific/Auckland", "Pacific/Fiji", "Pacific/Funafuti", "Pacific/Kwajalein", "Pacific/Majuro", "Pacific/Nauru", "Pacific/Tarawa", "Pacific/Wake", "Pacific/Wallis", "Pacific/Chatham", "Etc/GMT-13", "Pacific/Apia", "Pacific/Enderbury", "Pacific/Fakaofo", "Pacific/Tongatapu", "Etc/GMT-14", "Pacific/Kiritimati"] + }, + schedule: { + type: 'string', + minLength: 1, + oneOf: [{ + // todo: singular unit goes with 1; plural units go with non-1 value + pattern: '^rate\\([1-9]\\d*\\s(minute|minutes|hour|hours|day|days)\\)$' + },{ + // todo: start & end rage for all fields + pattern: '^at\\(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\)$' + },{ + // todo: gazillion more restriction, see AWS docs + pattern: '^cron\\((\\d+|/|\\*|,|-|\\d-\\d)\\s(\\d+|/|\\*|,|-|\\d-\\d)\\s(\\d+|/|\\?|L|W|\\*|,|-|\\d-\\d)\\s(\\d+|/|\\*|,|-|\\d-\\d)\\s(\\d+|\\?|\\*|,|-|L|#|\\d-\\d)\\s(\\d+|/|\\*|,|-|\\d-\\d)\\)$' + }] + }, + action: { + type: 'object', + properties: { + maximum: { + type: 'integer', + minimum: 0 + }, + minimum: { + type: 'integer', + minimum: 0 + }, + }, + additionalProperties: false, + anyOf: + [ + { "required": ["maximum", "minimum"]}, + { "required": ["maximum"] }, + { "required": ["minimum"] } + ] + }, + }, + required: [ 'name', 'schedule', 'action' ], + additionalProperties: false, + }, + }, + }, + additionalProperties: false + }, + ], + }, + }, + additionalProperties: true +}