Skip to content

Commit

Permalink
feat(mgmt-lambda-update): introduce error codes
Browse files Browse the repository at this point in the history
  • Loading branch information
Orkuncakilkaya authored and Sergey Shelomentsev committed Feb 26, 2024
1 parent 2c5e2af commit 6929756
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 102 deletions.
15 changes: 9 additions & 6 deletions mgmt-lambda/app.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { APIGatewayProxyEventV2WithRequestContext, APIGatewayEventRequestContextV2 } from 'aws-lambda'
import { APIGatewayEventRequestContextV2, APIGatewayProxyEventV2WithRequestContext } from 'aws-lambda'
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'
import { getAuthSettings, retrieveAuthToken } from './auth'
import type { DeploymentSettings } from './model/DeploymentSettings'
import { handleNoAthentication, handleWrongConfiguration, handleNotFound } from './handlers/errorHandlers'
import { handleError, handleNoAuthentication, handleNotFound, handleWrongConfiguration } from './handlers/errorHandlers'
import { defaults } from './DefaultSettings'
import { handleStatus } from './handlers/statusHandler'
import { handleUpdate } from './handlers/updateHandler'
Expand All @@ -16,7 +16,7 @@ export async function handler(event: APIGatewayProxyEventV2WithRequestContext<AP
const authSettings = await getAuthSettings(secretManagerClient)
const authToken = retrieveAuthToken(event)
if (authToken !== authSettings.token) {
return handleNoAthentication()
return handleNoAuthentication()
}
} catch (error) {
return handleWrongConfiguration(error)
Expand All @@ -35,7 +35,11 @@ export async function handler(event: APIGatewayProxyEventV2WithRequestContext<AP
const cloudFrontClient = new CloudFrontClient({ region: defaults.AWS_REGION })

if (path.startsWith('/update') && method === 'POST') {
return handleUpdate(lambdaClient, cloudFrontClient, deploymentSettings)
try {
return handleUpdate(lambdaClient, cloudFrontClient, deploymentSettings)
} catch (e: any) {
return handleError(e)
}
}
if (path.startsWith('/status') && method === 'GET') {
return handleStatus(lambdaClient, deploymentSettings)
Expand Down Expand Up @@ -63,10 +67,9 @@ function loadDeploymentSettings(): DeploymentSettings {
throw new Error(`environment variables not found: ${vars}`)
}

const settings: DeploymentSettings = {
return {
CFDistributionId: cfDistributionId,
LambdaFunctionArn: lambdaFunctionArn,
LambdaFunctionName: lambdaFunctionName,
}
return settings
}
26 changes: 26 additions & 0 deletions mgmt-lambda/exceptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export enum ErrorCode {
UnknownError = 'E1000',
AWSResourceNotFound = 'E2100',
AWSAccessDenied = 'E2200',
LambdaFunctionNotFound = 'E3100',
LambdaFunctionAssociationNotFound = 'E6100',
CloudFrontDistributionNotFound = 'E4100',
CacheBehaviorNotFound = 'E5100',
CacheBehaviorPatternNotDefined = 'E5200',
FunctionARNNotFound = 'E7100',
}

export class ApiException extends Error {
protected _code: ErrorCode
constructor(code: ErrorCode = ErrorCode.UnknownError) {
super()
this._code = code
}

get code() {
return this._code
}
get name() {
return this._code
}
}
19 changes: 18 additions & 1 deletion mgmt-lambda/handlers/errorHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { APIGatewayProxyResult } from 'aws-lambda'
import { ErrorCode } from '../exceptions'
import { ResourceNotFoundException } from '@aws-sdk/client-lambda'

export async function handleNoAthentication(): Promise<APIGatewayProxyResult> {
export async function handleNoAuthentication(): Promise<APIGatewayProxyResult> {
const body = {
status: 'Token is not specified or not valid',
}
Expand Down Expand Up @@ -28,6 +30,21 @@ export async function handleWrongConfiguration(error: any): Promise<APIGatewayPr
}
}

export function handleError(error: any): APIGatewayProxyResult {
if (error.name?.includes('AccessDenied')) {
error.code = ErrorCode.AWSAccessDenied
} else if (error.name === ResourceNotFoundException.name) {
error.code = ErrorCode.AWSResourceNotFound
}
return {
statusCode: 500,
body: JSON.stringify({ status: 'Error occurred', errorCode: error.code || ErrorCode.UnknownError }),
headers: {
'content-type': 'application/json',
},
}
}

export async function handleNotFound(): Promise<APIGatewayProxyResult> {
const body = {
status: 'Path not found',
Expand Down
129 changes: 47 additions & 82 deletions mgmt-lambda/handlers/updateHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,50 @@ import {
UpdateDistributionCommandInput,
} from '@aws-sdk/client-cloudfront'
import {
LambdaClient,
GetFunctionCommand,
UpdateFunctionCodeCommand,
GetFunctionCommandInput,
GetFunctionCommandOutput,
LambdaClient,
UpdateFunctionCodeCommand,
} from '@aws-sdk/client-lambda'
import { ApiException, ErrorCode } from '../exceptions'

/**
* @throws {ApiException}
* @throws {import('@aws-sdk/client-lambda').LambdaServiceException}
*/
export async function handleUpdate(
lambdaClient: LambdaClient,
cloudFrontClient: CloudFrontClient,
settings: DeploymentSettings,
): Promise<APIGatewayProxyResult> {
console.info(`Going to upgrade Fingerprint Pro function association at CloudFront distbution.`)
console.info(`Going to upgrade Fingerprint Pro function association at CloudFront distribution.`)
console.info(`Settings: ${settings}`)

try {
const isLambdaFunctionExist = await checkLambdaFunctionExistence(lambdaClient, settings.LambdaFunctionName)
if (!isLambdaFunctionExist) {
return handleFailure(`Lambda function with name ${settings.LambdaFunctionName} not found`)
}

const functionVersionArn = await updateLambdaFunctionCode(lambdaClient, settings.LambdaFunctionName)
return updateCloudFrontConfig(
cloudFrontClient,
settings.CFDistributionId,
settings.LambdaFunctionName,
functionVersionArn,
)
} catch (error: any) {
if (error.name === 'ResourceNotFoundException') {
return handleException('Resource not found', error.message)
} else if (error.name === 'AccessDeniedException') {
return handleException('No permission', error.message)
} else {
return handleFailure(error)
}
const isLambdaFunctionExist = await checkIfLambdaFunctionWithNameExists(lambdaClient, settings.LambdaFunctionName)
if (!isLambdaFunctionExist) {
throw new ApiException(ErrorCode.LambdaFunctionNotFound)
}

const functionVersionArn = await updateLambdaFunctionCode(lambdaClient, settings.LambdaFunctionName)
await updateCloudFrontConfig(
cloudFrontClient,
settings.CFDistributionId,
settings.LambdaFunctionName,
functionVersionArn,
)

return {
statusCode: 200,
body: JSON.stringify({ status: 'Update completed' }),
headers: {
'content-type': 'application/json',
},
}
}

/**
* @throws {ApiException}
*/
async function updateCloudFrontConfig(
cloudFrontClient: CloudFrontClient,
cloudFrontDistributionId: string,
Expand All @@ -63,20 +68,20 @@ async function updateCloudFrontConfig(
const cfConfig: GetDistributionConfigCommandOutput = await cloudFrontClient.send(getConfigCommand)

if (!cfConfig.ETag || !cfConfig.DistributionConfig) {
return handleFailure('CloudFront distribution not found')
throw new ApiException(ErrorCode.CloudFrontDistributionNotFound)
}

const cacheBehaviors = cfConfig.DistributionConfig.CacheBehaviors
const fpCbs = cacheBehaviors?.Items?.filter((it) => it.TargetOriginId === 'fpcdn.io')
if (!fpCbs || fpCbs?.length === 0) {
return handleFailure('Cache behavior not found')
throw new ApiException(ErrorCode.CacheBehaviorNotFound)
}
const cacheBehavior = fpCbs[0]
const lambdas = cacheBehavior.LambdaFunctionAssociations?.Items?.filter(
(it) => it && it.EventType === 'origin-request' && it.LambdaFunctionARN?.includes(`${lambdaFunctionName}:`),
)
if (!lambdas || lambdas?.length === 0) {
return handleFailure('Lambda function association not found')
throw new ApiException(ErrorCode.LambdaFunctionAssociationNotFound)
}
const lambda = lambdas[0]
lambda.LambdaFunctionARN = latestFunctionArn
Expand All @@ -93,7 +98,7 @@ async function updateCloudFrontConfig(

console.info('Going to invalidate routes for upgraded cache behavior')
if (!cacheBehavior.PathPattern) {
return handleFailure('Path pattern is not defined')
throw new ApiException(ErrorCode.CacheBehaviorPatternNotDefined)
}

let pathPattern = cacheBehavior.PathPattern
Expand All @@ -114,9 +119,12 @@ async function updateCloudFrontConfig(
const invalidationCommand = new CreateInvalidationCommand(invalidationParams)
const invalidationResult = await cloudFrontClient.send(invalidationCommand)
console.info(`Invalidation has finished, ${JSON.stringify(invalidationResult)}`)
return handleSuccess()
}

/**
* @throws {import('@aws-sdk/client-lambda').LambdaServiceException}
* @throws {ApiException}
*/
async function updateLambdaFunctionCode(lambdaClient: LambdaClient, functionName: string): Promise<string> {
console.info('Preparing command to update function code')
const command = new UpdateFunctionCodeCommand({
Expand All @@ -127,65 +135,22 @@ async function updateLambdaFunctionCode(lambdaClient: LambdaClient, functionName
})
console.info('Sending update command to Lambda runtime')
const result = await lambdaClient.send(command)
console.info(`Got Lambda function update result, functionARN: ${result.FunctionArn}`)

if (!result.FunctionArn) {
throw new Error('Function ARN not found after update')
throw new ApiException(ErrorCode.FunctionARNNotFound)
}

console.info(`Got Lambda function update result, functionARN: ${result.FunctionArn}`)

return result.FunctionArn
}

async function checkLambdaFunctionExistence(client: LambdaClient, functionName: string): Promise<boolean> {
const params: GetFunctionCommandInput = {
FunctionName: functionName,
}
const command = new GetFunctionCommand(params)
/**
* @throws {import('@aws-sdk/client-lambda').LambdaServiceException}
*/
async function checkIfLambdaFunctionWithNameExists(client: LambdaClient, functionName: string): Promise<boolean> {
const command = new GetFunctionCommand({ FunctionName: functionName })
const result: GetFunctionCommandOutput = await client.send(command)
if (!result.Configuration?.FunctionArn) {
return false
}
return true
}

async function handleException(status: string, message: string): Promise<APIGatewayProxyResult> {
const body = {
status: status,
error: message,
}
return {
statusCode: 500,
body: JSON.stringify(body),
headers: {
'content-type': 'application/json',
},
}
}

async function handleFailure(message?: any): Promise<APIGatewayProxyResult> {
const body = {
status: 'Update failed',
error: message,
}
return {
statusCode: 500,
body: JSON.stringify(body),
headers: {
'content-type': 'application/json',
},
}
}

async function handleSuccess(): Promise<APIGatewayProxyResult> {
const body = {
status: 'Update completed',
error: null,
}
return {
statusCode: 200,
body: JSON.stringify(body),
headers: {
'content-type': 'application/json',
},
}
return typeof result.Configuration?.FunctionArn !== 'undefined'
}
36 changes: 23 additions & 13 deletions mgmt-lambda/test/handlers/handleUpdate.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { mockClient } from 'aws-sdk-client-mock'
import {
LambdaClient,
GetFunctionCommand,
GetFunctionResponse,
LambdaClient,
UpdateFunctionCodeCommand,
} from '@aws-sdk/client-lambda'
import {
Expand All @@ -17,6 +17,7 @@ import {
import { handleUpdate } from '../../handlers/updateHandler'
import type { DeploymentSettings } from '../../model/DeploymentSettings'
import 'aws-sdk-client-mock-jest'
import { ErrorCode } from '../../exceptions'

const lambdaMock = mockClient(LambdaClient)
const lambdaClient = new LambdaClient({ region: 'us-east-1' })
Expand Down Expand Up @@ -192,10 +193,13 @@ describe('Handle mgmt-update', () => {
})
.resolves({})

const result = await handleUpdate(lambdaClient, cloudFrontClient, settings)
expect(result.statusCode).toBe(500)
const error = JSON.parse(result.body)['error']
expect(error).toBe('Lambda function with name fingerprint-pro-lambda-function not found')
expect.assertions(6)

try {
await handleUpdate(lambdaClient, cloudFrontClient, settings)
} catch (e: any) {
expect(e.code).toEqual(ErrorCode.LambdaFunctionNotFound)
}

expect(lambdaMock).toHaveReceivedCommandTimes(GetFunctionCommand, 1)
expect(lambdaMock).toHaveReceivedCommandTimes(UpdateFunctionCodeCommand, 0)
Expand Down Expand Up @@ -270,10 +274,13 @@ describe('Handle mgmt-update', () => {
},
})

const result = await handleUpdate(lambdaClient, cloudFrontClient, settings)
expect(result.statusCode).toBe(500)
const error = JSON.parse(result.body)['error']
expect(error).toBe('Cache behavior not found')
expect.assertions(6)

try {
await handleUpdate(lambdaClient, cloudFrontClient, settings)
} catch (e: any) {
expect(e.code).toBe(ErrorCode.CacheBehaviorNotFound)
}

expect(lambdaMock).toHaveReceivedCommandTimes(GetFunctionCommand, 1)
expect(lambdaMock).toHaveReceivedCommandTimes(UpdateFunctionCodeCommand, 1)
Expand Down Expand Up @@ -338,10 +345,13 @@ describe('Handle mgmt-update', () => {
},
})

const result = await handleUpdate(lambdaClient, cloudFrontClient, settings)
expect(result.statusCode).toBe(500)
const error = JSON.parse(result.body)['error']
expect(error).toBe('Lambda function association not found')
expect.assertions(6)

try {
await handleUpdate(lambdaClient, cloudFrontClient, settings)
} catch (e: any) {
expect(e.code).toBe(ErrorCode.LambdaFunctionAssociationNotFound)
}

expect(lambdaMock).toHaveReceivedCommandTimes(GetFunctionCommand, 1)
expect(lambdaMock).toHaveReceivedCommandTimes(UpdateFunctionCodeCommand, 1)
Expand Down

0 comments on commit 6929756

Please sign in to comment.