Skip to content

Commit

Permalink
fix: allow Lambda function config overrides without Lambda: property
Browse files Browse the repository at this point in the history
  • Loading branch information
eoinsha committed Jan 22, 2024
1 parent 443cdc0 commit a76755c
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 29 deletions.
24 changes: 9 additions & 15 deletions README.md
Expand Up @@ -440,10 +440,9 @@ functions:
dashboard:
enabled: false # No Lambda widgets will be created for this function
alarms:
Lambda:
Invocations:
Threshold: 2 # The invocation threshold is specific to
# this function's expected invocation count
Invocations:
Threshold: 2 # The invocation threshold is specific to
# this function's expected invocation count
```

To disable all alarms for any given function, use:
Expand All @@ -454,8 +453,7 @@ functions:
handler: basic-handler.hello
slicWatch:
alarms:
Lambda:
enabled: false
enabled: false
```

#### SAM/CloudFormation function-level configuration
Expand All @@ -468,9 +466,8 @@ Resources:
Metadata:
slicWatch:
alarms:
Lambda:
Invocations:
Threshold: 3
Invocations:
Threshold: 3
dashboard:
enabled: true
```
Expand All @@ -486,8 +483,7 @@ Resources:
Metadata:
slicWatch:
alarms:
Lambda:
enabled: false
enabled: false
```

#### CDK function-level configuration
Expand All @@ -497,10 +493,8 @@ const cfnFuncHello = hello.node.defaultChild as CfnResource;
cfnFuncHello.cfnOptions.metadata = {
slicWatch: {
alarms: {
Lambda: {
Invocations: {
Threshold: 2
}
Invocations: {
Threshold: 2
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions core/alarms/lambda.ts
Expand Up @@ -18,9 +18,9 @@ export type SlicWatchLambdaAlarmsConfig<T extends InputOutput> = T & {
const lambdaMetrics = ['Errors', 'ThrottlesPc', 'DurationPc', 'Invocations']

/**
* Add all required Lambda alarms to the provided CloudFormation templatebased on the Lambda resources found within
* Add all required Lambda alarms to the provided CloudFormation template based on the Lambda resources found within
*
* @param functionAlarmPropertiess The cascaded Lambda alarm configuration with function-specific overrides by function logical ID
* @param lambdaAlarmConfig Lambda-specific alarm configuration
* @param alarmActionsConfig Notification configuration for alarm status change events
* @compiledTemplate CloudFormation template object
*
Expand Down
18 changes: 15 additions & 3 deletions core/cf-template.ts
Expand Up @@ -9,7 +9,7 @@ import { cascade } from './inputs/cascading-config'
import { type SlicWatchMergedConfig } from './alarms/alarm-types'
import { type WidgetMetricProperties } from './dashboards/dashboard-types'
import { defaultConfig } from './inputs/default-config'
import { type ConfigType, cfTypeByConfigType } from './inputs/config-types'
import { ConfigType, cfTypeByConfigType } from './inputs/config-types'

const logger = getLogger()

Expand Down Expand Up @@ -69,7 +69,13 @@ export function getResourceAlarmConfigurationsByType<M extends SlicWatchMergedCo
const alarmConfigurations: Record<string, M> = {}
const resources = getResourcesByType(cfTypeByConfigType[type], template)
for (const [funcLogicalId, resource] of Object.entries(resources)) {
const resourceConfig = resource?.Metadata?.slicWatch?.alarms // Resource-specific overrides
let legacyFallbackResourceConfig
if (type === ConfigType.Lambda) {
// Older versions only allowed function resource overrides and required the `Lambda` property within the config object
// If this is still present in configuration, we take it from here
legacyFallbackResourceConfig = resource?.Metadata?.slicWatch?.alarms?.Lambda
}
const resourceConfig = legacyFallbackResourceConfig ?? resource?.Metadata?.slicWatch?.alarms // Resource-specific overrides
const defaultResourceConfig = defaultConfig.alarms?.[type] // Default configuration for the type's alarms
// Cascade the default resource's configuration into the resource-specific overrides
const cascadedResourceConfig = resourceConfig !== undefined ? cascade(merge({}, defaultResourceConfig, resourceConfig)) : {}
Expand Down Expand Up @@ -97,7 +103,13 @@ export function getResourceDashboardConfigurationsByType<T extends WidgetMetricP
const dashConfigurations: Record<string, T> = {}
const resources = getResourcesByType(cfTypeByConfigType[type], template)
for (const [logicalId, resource] of Object.entries(resources)) {
const resourceConfig = resource?.Metadata?.slicWatch?.dashboard // Resource-specific overrides
let legacyFallbackResourceConfig
if (type === ConfigType.Lambda) {
// Older versions only allowed function resource overrides and required the `Lambda` property within the config object
// If this is still present in configuration, we take it from here
legacyFallbackResourceConfig = resource?.Metadata?.slicWatch?.dashboard?.Lambda
}
const resourceConfig = legacyFallbackResourceConfig ?? resource?.Metadata?.slicWatch?.dashboard // Resource-specific overrides
const defaultResourceConfig = defaultConfig.dashboard?.widgets?.[type] // Default configuration for the widget
// Cascade the default resource's configuration into the resource-specific overrides
const cascadedResourceConfig = resourceConfig !== undefined ? cascade(merge({}, defaultResourceConfig, resourceConfig)) : {}
Expand Down
5 changes: 3 additions & 2 deletions serverless-plugin/serverless-plugin.ts
Expand Up @@ -42,8 +42,8 @@ class ServerlessPlugin {
}

/**
* Modify the CloudFormation template before the package is finalized
*/
* Modify the CloudFormation template before the package is finalized
*/
createSlicWatchResources () {
const slicWatchConfig: SlicWatchConfig = this.serverless.service.custom?.slicWatch ?? {}

Expand All @@ -65,6 +65,7 @@ class ServerlessPlugin {
// CloudFormation Metadata on the generate AWS::Lambda::Function resource
const allFunctions = this.serverless.service.getAllFunctions() as string[]
this.serverless.cli.log(`Setting SLIC Watch configuration for ${allFunctions}`)

for (const funcName of allFunctions) {
const func = this.serverless.service.getFunction(funcName) as any
const funcConfig = func.slicWatch ?? {}
Expand Down
134 changes: 133 additions & 1 deletion serverless-plugin/tests/index.test.ts
@@ -1,10 +1,14 @@
import { test } from 'tap'
import _ from 'lodash'
import ServerlessError from 'serverless/lib/serverless-error'
import type Template from 'cloudform-types/types/template'

import ServerlessPlugin from '../serverless-plugin'
import { getLogger } from 'slic-watch-core/logging'
import { createMockServerless, dummyLogger, pluginUtils, slsYaml } from '../../test-utils/sls-test-utils'
import { type SlsYaml, createMockServerless, dummyLogger, pluginUtils, slsYaml } from '../../test-utils/sls-test-utils'
import { type ResourceType } from 'slic-watch-core'
import { getDashboardFromTemplate, getDashboardWidgetsByTitle } from 'slic-watch-core/tests/testing-utils'
import { type MetricWidgetProperties } from 'cloudwatch-dashboard-types'

interface TestData {
schema?
Expand Down Expand Up @@ -48,6 +52,134 @@ test('index', t => {
t.end()
})

t.test('function-level overrides in serverless `functions` block take precedence', t => {
const compiledTemplate: Template = {
Resources: {
HelloLambdaFunction: {
Type: 'AWS::Lambda::Function',
Properties: {
FunctionName: 'serverless-test-project-dev-hello'
}
}
}
}
const slsConfig: SlsYaml = {
custom: {
slicWatch: {
alarms: {
Lambda: {
Invocations: {
enabled: true,
Threshold: 10
}
}
},
dashboard: {
widgets: {
Lambda: {
Invocations: {
yAxis: 'left'
}
}
}
}
}
},
functions: {
hello: {
slicWatch: {
enabled: true
}
}
}
}

t.test('Plugin succeeds without function-level overrides', t => {
const sls = createMockServerless(compiledTemplate, slsConfig)
const plugin = new ServerlessPlugin(sls, null, pluginUtils)
plugin.createSlicWatchResources()
const invocationAlarmProperties = (compiledTemplate.Resources as ResourceType).slicWatchLambdaInvocationsAlarmHelloLambdaFunction.Properties
t.equal(invocationAlarmProperties?.Threshold, 10)

const { dashboard } = getDashboardFromTemplate(compiledTemplate)
const widgets = getDashboardWidgetsByTitle(dashboard, /Lambda Invocations/)
t.equal(widgets.length, 1)
t.match((widgets[0].properties as MetricWidgetProperties).metrics, [
['AWS/Lambda', 'Invocations', 'FunctionName', '${HelloLambdaFunction}', { stat: 'Sum', yAxis: 'left' }]
])

t.end()
})

t.test('Plugin succeeds with function-level overrides', t => {
const modifiedSlsConfig = _.cloneDeep(slsConfig)
Object.assign(modifiedSlsConfig.functions.hello.slicWatch, {
alarms: {
Invocations: {
Threshold: 3,
enabled: true
}
},
dashboard: {
Invocations: {
yAxis: 'right'
}
}
})

const sls = createMockServerless(compiledTemplate, modifiedSlsConfig)
const plugin = new ServerlessPlugin(sls, null, pluginUtils)
plugin.createSlicWatchResources()
const invocationAlarmProperties = (compiledTemplate.Resources as ResourceType).slicWatchLambdaInvocationsAlarmHelloLambdaFunction.Properties
t.equal(invocationAlarmProperties?.Threshold, 3)

const { dashboard } = getDashboardFromTemplate(compiledTemplate)
const widgets = getDashboardWidgetsByTitle(dashboard, /Lambda Invocations/)
t.equal(widgets.length, 1)
t.match((widgets[0].properties as MetricWidgetProperties).metrics, [
['AWS/Lambda', 'Invocations', 'FunctionName', '${HelloLambdaFunction}', { stat: 'Sum', yAxis: 'right' }]
])
t.end()
})

t.test('Plugin succeeds with legacy function-level overrides', t => {
const modifiedSlsConfig = _.cloneDeep(slsConfig)
Object.assign(modifiedSlsConfig.functions.hello.slicWatch, {
alarms: {
Lambda: { // This extra property is the 'legacy' configuration bit
Invocations: {
Threshold: 4,
enabled: true
}
}
},
dashboard: {
Lambda: { // This extra property is the 'legacy' configuration bit
Invocations: {
yAxis: 'right'
}
}
}
})

const sls = createMockServerless(compiledTemplate, modifiedSlsConfig)
const plugin = new ServerlessPlugin(sls, null, pluginUtils)
plugin.createSlicWatchResources()
const invocationAlarmProperties = (compiledTemplate.Resources as ResourceType).slicWatchLambdaInvocationsAlarmHelloLambdaFunction.Properties
t.equal(invocationAlarmProperties?.Threshold, 4)

const { dashboard } = getDashboardFromTemplate(compiledTemplate)
const widgets = getDashboardWidgetsByTitle(dashboard, /Lambda Invocations/)
t.equal(widgets.length, 1)
t.match((widgets[0].properties as MetricWidgetProperties).metrics, [
['AWS/Lambda', 'Invocations', 'FunctionName', '${HelloLambdaFunction}', { stat: 'Sum', yAxis: 'right' }]
])
t.end()
})

t.end()
})

t.test('Plugin succeeds with no custom section', t => {
const plugin = new ServerlessPlugin({
...mockServerless,
Expand Down
7 changes: 1 addition & 6 deletions test-utils/sls-test-utils.ts
Expand Up @@ -46,12 +46,7 @@ export function createMockServerless (compiledTemplate: Template, slsConfig = sl
name: 'aws',
compiledCloudFormationTemplate: compiledTemplate
},
custom: {
slicWatch: {
enabled: true,
topicArn: 'test-topic'
}
},
custom: slsConfig.custom,
getAllFunctions: () => Object.keys(slsConfig.functions ?? {}),
getFunction: (funcRef) => slsConfig.functions[funcRef]
}
Expand Down

0 comments on commit a76755c

Please sign in to comment.