Skip to content

Commit d76ee36

Browse files
authored
Lambda Create/Update handles lack of GetFunctionConfiguration (#448)
Users without lambda.GetFunctionConfiguration permissions will get warned and have a 5 second wait inserted into their AWS Lambda Deploy Function workflows.
1 parent d5306ae commit d76ee36

File tree

4 files changed

+105
-15
lines changed

4 files changed

+105
-15
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Lambda Deploy Function gracefully handles missing lambda:GetFunctionConfiguration with a warning and a 5-second wait"
4+
}

src/tasks/LambdaDeployFunction/TaskOperations.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as tl from 'azure-pipelines-task-lib/task'
99
import { SdkUtils } from 'lib/sdkutils'
1010
import { readFileSync } from 'fs'
1111
import { deployCodeAndConfig, deployCodeOnly, TaskParameters, updateFromLocalFile } from './TaskParameters'
12+
import { AWSError } from 'aws-sdk'
1213

1314
const FUNCTION_UPDATED = 'functionUpdated'
1415
const FUNCTION_ACTIVE = 'functionActive'
@@ -17,7 +18,8 @@ export class TaskOperations {
1718
public constructor(
1819
public readonly iamClient: IAM,
1920
public readonly lambdaClient: Lambda,
20-
public readonly taskParameters: TaskParameters
21+
public readonly taskParameters: TaskParameters,
22+
private readonly _timeoutSeconds: number = 5
2123
) {}
2224

2325
public async execute(): Promise<void> {
@@ -73,7 +75,7 @@ export class TaskOperations {
7375
throw new Error(tl.loc('NoFunctionArnReturned'))
7476
}
7577

76-
await this.waitForUpdate()
78+
await this.waitForStatus(FUNCTION_UPDATED)
7779

7880
return response.FunctionArn
7981
} catch (err) {
@@ -128,7 +130,7 @@ export class TaskOperations {
128130
throw new Error(tl.loc('NoFunctionArnReturned'))
129131
}
130132

131-
await this.waitForUpdate()
133+
await this.waitForStatus(FUNCTION_UPDATED)
132134

133135
// Update tags if we have them
134136
const tags = SdkUtils.getTagsDictonary<Lambda.Tags>(this.taskParameters.tags)
@@ -209,11 +211,7 @@ export class TaskOperations {
209211
throw new Error(tl.loc('NoFunctionArnReturned'))
210212
}
211213

212-
console.log(tl.loc('AwaitingStatus', this.taskParameters.functionName, FUNCTION_ACTIVE))
213-
await this.lambdaClient
214-
.waitFor(FUNCTION_ACTIVE, { FunctionName: this.taskParameters.functionName })
215-
.promise()
216-
console.log(tl.loc('AwaitingStatusComplete', this.taskParameters.functionName, FUNCTION_ACTIVE))
214+
await this.waitForStatus(FUNCTION_ACTIVE)
217215

218216
return response.FunctionArn
219217
} catch (err) {
@@ -235,9 +233,32 @@ export class TaskOperations {
235233
}
236234
}
237235

238-
private async waitForUpdate(): Promise<void> {
239-
console.log(tl.loc('AwaitingStatus', this.taskParameters.functionName, FUNCTION_UPDATED))
240-
await this.lambdaClient.waitFor(FUNCTION_UPDATED, { FunctionName: this.taskParameters.functionName }).promise()
241-
console.log(tl.loc('AwaitingStatusComplete', this.taskParameters.functionName, FUNCTION_UPDATED))
236+
private async waitForStatus(status: typeof FUNCTION_ACTIVE | typeof FUNCTION_UPDATED): Promise<void> {
237+
try {
238+
console.log(tl.loc('AwaitingStatus', this.taskParameters.functionName, status))
239+
// make the compiler happy: waitFor has different signatures depending on status...
240+
if (status === FUNCTION_ACTIVE) {
241+
await this.lambdaClient.waitFor(status, { FunctionName: this.taskParameters.functionName }).promise()
242+
} else {
243+
await this.lambdaClient.waitFor(status, { FunctionName: this.taskParameters.functionName }).promise()
244+
}
245+
console.log(tl.loc('AwaitingStatusComplete', this.taskParameters.functionName, status))
246+
} catch (e) {
247+
const err = e as AWSError
248+
// waitFor lacking permissions: surface warning and sanely wait 5 seconds
249+
if (
250+
(err.originalError as AWSError | undefined)?.code === 'AccessDeniedException' &&
251+
err.originalError?.message.includes('lambda:GetFunctionConfiguration')
252+
) {
253+
tl.warning(
254+
tl.loc('CantWaitWarning', this._timeoutSeconds.toString(), 'lambda:GetFunctionConfiguration')
255+
)
256+
await new Promise(resolve => {
257+
setTimeout(resolve, this._timeoutSeconds * 1000)
258+
})
259+
} else {
260+
throw err
261+
}
262+
}
242263
}
243264
}

src/tasks/LambdaDeployFunction/task.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"friendlyName": "AWS Lambda Deploy Function",
55
"description": "General purpose deployment of AWS Lambda functions for all supported language runtimes.",
66
"author": "Amazon Web Services",
7-
"helpMarkDown": "Please refer to [AWS Lambda Developer Guide](https://docs.aws.amazon.com/lambda/latest/dg/) for more information on working with AWS Lambda.\n\nMore information on this task can be found in the [task reference](https://docs.aws.amazon.com/vsts/latest/userguide/lambda-deploy.html).\n\n####Task Permissions\nThis task requires permissions to call the following AWS service APIs (depending on selected task options, not all APIs may be used):\n* lambda:CreateFunction\n* lambda:GetFunction\n* lambda:UpdateFunctionCode\n* lambda:UpdateFunctionConfiguration",
7+
"helpMarkDown": "Please refer to [AWS Lambda Developer Guide](https://docs.aws.amazon.com/lambda/latest/dg/) for more information on working with AWS Lambda.\n\nMore information on this task can be found in the [task reference](https://docs.aws.amazon.com/vsts/latest/userguide/lambda-deploy.html).\n\n####Task Permissions\nThis task requires permissions to call the following AWS service APIs (depending on selected task options, not all APIs may be used):\n* lambda:CreateFunction\n* lambda:GetFunction\n* lambda:GetFunctionConfiguration\n* lambda:UpdateFunctionCode\n* lambda:UpdateFunctionConfiguration",
88
"category": "Deploy",
99
"visibility": ["Build", "Release"],
1010
"demands": [],
@@ -318,6 +318,7 @@
318318
"NoFunctionArnReturned": "No function ARN returned from the service! Deployment failed!",
319319
"SettingOutputVariable": "Setting output variable %s with the function output",
320320
"AwaitingStatus": "Waiting for function %s to reach %s state...",
321-
"AwaitingStatusComplete": "Function %s has reached %s state"
321+
"AwaitingStatusComplete": "Function %s has reached %s state",
322+
"CantWaitWarning": "Role doesn't have Lambda States waiting permissions; will wait %s seconds and proceed. To avoid this in the future, grant %s permissions to the task role!"
322323
}
323324
}

tests/taskTests/lambdaDeployFunction/lambdaDeployFunction-test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: MIT
44
*/
55

6-
import { IAM, Lambda } from 'aws-sdk'
6+
import { AWSError, IAM, Lambda } from 'aws-sdk'
77
import { SdkUtils } from 'lib/sdkutils'
88
import { TaskOperations } from 'tasks/LambdaDeployFunction/TaskOperations'
99
import { deployCodeAndConfig, deployCodeOnly, TaskParameters } from 'tasks/LambdaDeployFunction/TaskParameters'
@@ -84,6 +84,20 @@ const waitForFails = {
8484
}
8585
}
8686

87+
const waitForPermissionsGetConfigurationFails = {
88+
promise: function() {
89+
const e = new Error() as AWSError
90+
e.code = 'ResourceNotReady'
91+
e.message = 'Resource is not in the state of denial'
92+
e.originalError = {
93+
code: 'AccessDeniedException',
94+
message:
95+
'User: youHaveNoPowerHere is not authorized to perform: lambda:GetFunctionConfiguration on resource: resource with an explicit deny in an identity-based policy'
96+
} as AWSError
97+
throw e
98+
}
99+
}
100+
87101
describe('Lambda Deploy Function', () => {
88102
// TODO https://github.com/aws/aws-vsts-tools/issues/167
89103
beforeAll(() => {
@@ -147,6 +161,22 @@ describe('Lambda Deploy Function', () => {
147161
expect(lambda.waitFor).toBeCalledTimes(1)
148162
})
149163

164+
test('Deploy only Function exists calls wait and still updates if wait fails due to waiter permissions', async () => {
165+
expect.assertions(3)
166+
const taskParameters = { ...baseTaskParameters }
167+
taskParameters.deploymentMode = deployCodeOnly
168+
taskParameters.roleARN = 'arn:yes'
169+
const lambda = new Lambda() as any
170+
lambda.getFunction = jest.fn(() => getFunctionSucceeds)
171+
lambda.updateFunctionCode = jest.fn(() => updateFunctionSucceeds)
172+
lambda.waitFor = jest.fn(() => waitForPermissionsGetConfigurationFails)
173+
const taskOperations = new TaskOperations(new IAM(), lambda, taskParameters, 0)
174+
await taskOperations.execute()
175+
expect(lambda.getFunction).toBeCalledTimes(1)
176+
expect(lambda.updateFunctionCode).toBeCalledTimes(1)
177+
expect(lambda.waitFor).toBeCalledTimes(1)
178+
})
179+
150180
test('Deploy only Function exists calls update but fails if status does not update', async () => {
151181
expect.assertions(4)
152182
const taskParameters = { ...baseTaskParameters }
@@ -179,6 +209,22 @@ describe('Lambda Deploy Function', () => {
179209
expect(lambda.waitFor).toBeCalledTimes(1)
180210
})
181211

212+
test('Deploy and config does not exist calls create and still updates if wait fails due to waiter permissions', async () => {
213+
expect.assertions(3)
214+
const taskParameters = { ...baseTaskParameters }
215+
taskParameters.deploymentMode = deployCodeAndConfig
216+
taskParameters.roleARN = 'arn:yes'
217+
const lambda = new Lambda() as any
218+
lambda.getFunction = jest.fn(() => getFunctionFails)
219+
lambda.createFunction = jest.fn(() => updateFunctionSucceeds)
220+
lambda.waitFor = jest.fn(() => waitForPermissionsGetConfigurationFails)
221+
const taskOperations = new TaskOperations(new IAM(), lambda, taskParameters, 0)
222+
await taskOperations.execute()
223+
expect(lambda.getFunction).toBeCalledTimes(1)
224+
expect(lambda.createFunction).toBeCalledTimes(1)
225+
expect(lambda.waitFor).toBeCalledTimes(1)
226+
})
227+
182228
test('Deploy and config does not exist calls create but fails if status does not update', async () => {
183229
expect.assertions(4)
184230
const taskParameters = { ...baseTaskParameters }
@@ -213,6 +259,24 @@ describe('Lambda Deploy Function', () => {
213259
expect(lambda.waitFor).toBeCalledTimes(2)
214260
})
215261

262+
test('Deploy and config exists calls update and still updates if wait fails due to waiter permissions', async () => {
263+
expect.assertions(4)
264+
const taskParameters = { ...baseTaskParameters }
265+
taskParameters.deploymentMode = deployCodeAndConfig
266+
taskParameters.roleARN = 'arn:yes'
267+
const lambda = new Lambda() as any
268+
lambda.getFunction = jest.fn(() => getFunctionSucceeds)
269+
lambda.updateFunctionCode = jest.fn(() => updateFunctionSucceeds)
270+
lambda.updateFunctionConfiguration = jest.fn(() => updateFunctionSucceeds)
271+
lambda.waitFor = jest.fn(() => waitForPermissionsGetConfigurationFails)
272+
const taskOperations = new TaskOperations(new IAM(), lambda, taskParameters, 0)
273+
await taskOperations.execute()
274+
expect(lambda.getFunction).toBeCalledTimes(1)
275+
expect(lambda.updateFunctionCode).toBeCalledTimes(1)
276+
expect(lambda.updateFunctionConfiguration).toBeCalledTimes(1)
277+
expect(lambda.waitFor).toBeCalledTimes(2)
278+
})
279+
216280
test('Deploy and config exists calls update but fails if status does not update after config update', async () => {
217281
expect.assertions(5)
218282
const taskParameters = { ...baseTaskParameters }

0 commit comments

Comments
 (0)