Skip to content

Commit

Permalink
feat: add endpoints structure and error handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey Shelomentsev committed Dec 19, 2023
1 parent 850ca61 commit f5b3709
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 153 deletions.
164 changes: 11 additions & 153 deletions mgmt-lambda/app.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,38 @@
import { APIGatewayProxyEventV2WithRequestContext, APIGatewayEventRequestContextV2, Context } from 'aws-lambda'
import {
CloudFrontClient,
CreateInvalidationCommand,
CreateInvalidationCommandInput,
GetDistributionConfigCommand,
GetDistributionConfigCommandOutput,
UpdateDistributionCommand,
UpdateDistributionCommandInput,
} from '@aws-sdk/client-cloudfront'
import {
LambdaClient,
ListVersionsByFunctionCommand,
ListVersionsByFunctionCommandInput,
ListVersionsByFunctionCommandOutput,
} from '@aws-sdk/client-lambda'
import { APIGatewayProxyEventV2WithRequestContext, APIGatewayEventRequestContextV2 } from 'aws-lambda'
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'
import type { AuthSettings } from './model/AuthSettings'
import type { DeploymentSettings } from './model/DeploymentSettings'
import { handleNoAthentication, handleWrongConfiguration, handleNotFound } from './handlers/errorHandlers'
import { handleStatus } from './handlers/statusHandler'
import { handleUpdate } from './handlers/updateHandler'

const REGION = 'us-east-1'

export async function handler(
event: APIGatewayProxyEventV2WithRequestContext<APIGatewayEventRequestContextV2>,
_: Context,
callback: any,
) {
export async function handler(event: APIGatewayProxyEventV2WithRequestContext<APIGatewayEventRequestContextV2>) {
console.info(JSON.stringify(event))

const authSettings = await getAuthSettings()
console.info(authSettings)

const authorization = event.headers['authorization']
if (authorization !== authSettings.token) {
const notAuthResponse = {
statusCode: 401,
}
callback(null, notAuthResponse)
return handleNoAthentication()
}

let deploymentSettings: DeploymentSettings
try {
deploymentSettings = loadDeploymentSettings()
} catch (error) {
const wrongEnv = {
statusCode: 500,
body: {
error: error,
},
}
callback(null, wrongEnv)
return handleWrongConfiguration(error)
}

const path = event.rawPath
console.info(`path = ${path}`)
if (path.startsWith('/update')) {
handleUpdate(deploymentSettings)
return handleUpdate(deploymentSettings)
} else if (path.startsWith('/status')) {
return handleStatus(deploymentSettings)
} else {
return handleNotFound()
}

const okResp = {
statusCode: 200,
body: JSON.stringify({
message: 'OK',
}),
}

callback(null, okResp)
}

async function getAuthSettings(): Promise<AuthSettings> {
Expand Down Expand Up @@ -116,112 +83,3 @@ function loadDeploymentSettings(): DeploymentSettings {
}
return settings
}

async function handleUpdate(settings: DeploymentSettings) {
console.info(`Going to upgrade Fingerprint Pro function association at CloudFront distbution.`)
console.info(`Settings: ${settings}`)

const latestFunctionArn = await getLambdaLatestVersionArn(settings.LambdaFunctionName)
if (!latestFunctionArn) {
return publishJobFailure('No lambda versions')
}

if (latestFunctionArn.length === 1) {
console.info('No updates yet')
return publishJobSuccess()
}

updateCloudFrontConfig(settings.CFDistributionId, settings.LambdaFunctionName, latestFunctionArn)
}

async function updateCloudFrontConfig(
cloudFrontDistributionId: string,
lambdaFunctionName: string,
latestFunctionArn: string,
) {
const cloudFrontClient = new CloudFrontClient({ region: REGION })

const configParams = {
Id: cloudFrontDistributionId,
}
const getConfigCommand = new GetDistributionConfigCommand(configParams)
const cfConfig: GetDistributionConfigCommandOutput = await cloudFrontClient.send(getConfigCommand)

if (!cfConfig.ETag || !cfConfig.DistributionConfig) {
return publishJobFailure('CloudFront distribution not found')
}

const cacheBehaviors = cfConfig.DistributionConfig.CacheBehaviors
const fpCbs = cacheBehaviors?.Items?.filter((it) => it.TargetOriginId === 'fpcdn.io')
if (!fpCbs || fpCbs?.length === 0) {
return publishJobFailure('Cache behavior not found')
}
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 publishJobFailure('Lambda function association not found')
}
const lambda = lambdas[0]
lambda.LambdaFunctionARN = latestFunctionArn

const updateParams: UpdateDistributionCommandInput = {
DistributionConfig: cfConfig.DistributionConfig,
Id: cloudFrontDistributionId,
IfMatch: cfConfig.ETag,
}

const updateConfigCommand = new UpdateDistributionCommand(updateParams)
const updateCFResult = await cloudFrontClient.send(updateConfigCommand)
console.info(`CloudFront update has finished, ${JSON.stringify(updateCFResult)}`)

console.info('Going to invalidate routes for upgraded cache behavior')
if (!cacheBehavior.PathPattern) {
return publishJobFailure('Path pattern is not defined')
}

let pathPattern = cacheBehavior.PathPattern
if (!pathPattern.startsWith('/')) {
pathPattern = '/' + pathPattern
}

const invalidationParams: CreateInvalidationCommandInput = {
DistributionId: cloudFrontDistributionId,
InvalidationBatch: {
Paths: {
Quantity: 1,
Items: [pathPattern],
},
CallerReference: 'fingerprint-pro-management-lambda-function',
},
}
const invalidationCommand = new CreateInvalidationCommand(invalidationParams)
const invalidationResult = await cloudFrontClient.send(invalidationCommand)
console.info(`Invalidation has finished, ${JSON.stringify(invalidationResult)}`)
}

async function getLambdaLatestVersionArn(functionName: string): Promise<string | undefined> {
const client = new LambdaClient({ region: REGION })
const params: ListVersionsByFunctionCommandInput = {
FunctionName: functionName,
}
const command = new ListVersionsByFunctionCommand(params)
const result: ListVersionsByFunctionCommandOutput = await client.send(command)
if (!result.Versions || result.Versions?.length === 0) {
return Promise.resolve(undefined)
}

const latest = result.Versions.filter((it) => it.Version && Number.isFinite(Number.parseInt(it.Version))).sort(
(a, b) => Number.parseInt(b.Version!!) - Number.parseInt(a.Version!!),
)[0]
return Promise.resolve(latest.FunctionArn)
}

async function publishJobSuccess() {
console.info(`Job successfully finished`)
}

async function publishJobFailure(message: string) {
console.info(`Job failed with ${message}`)
}
33 changes: 33 additions & 0 deletions mgmt-lambda/handlers/errorHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { APIGatewayProxyResult } from 'aws-lambda'

export async function handleNoAthentication(): Promise<APIGatewayProxyResult> {
const body = {
status: 'Token is not specified or not valid',
}
return {
statusCode: 401,
body: JSON.stringify(body),
}
}

export async function handleWrongConfiguration(error: any): Promise<APIGatewayProxyResult> {
const body = {
status:
'Wrong function configuration. Check environment variables for Lambda@Edge function and CloudFront Distribution id',
error: error,
}
return {
statusCode: 500,
body: JSON.stringify(body),
}
}

export async function handleNotFound(): Promise<APIGatewayProxyResult> {
const body = {
status: 'Path not found',
}
return {
statusCode: 404,
body: JSON.stringify(body),
}
}
16 changes: 16 additions & 0 deletions mgmt-lambda/handlers/statusHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { DeploymentSettings } from '../model/DeploymentSettings'
import { LambdaClient, GetFunctionCommand } from '@aws-sdk/client-lambda'

export async function handleStatus(settings: DeploymentSettings) {
const client = new LambdaClient({})
const command = new GetFunctionCommand({ FunctionName: settings.LambdaFunctionName })
const functionResult = await client.send(command)

return {
status: '200',
body: functionResult,
headers: {
'content-type': 'application/json',
},
}
}
126 changes: 126 additions & 0 deletions mgmt-lambda/handlers/updateHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { DeploymentSettings } from '../model/DeploymentSettings'
import { defaults } from '../model/DefaultSettings'
import {
CloudFrontClient,
CreateInvalidationCommand,
CreateInvalidationCommandInput,
GetDistributionConfigCommand,
GetDistributionConfigCommandOutput,
UpdateDistributionCommand,
UpdateDistributionCommandInput,
} from '@aws-sdk/client-cloudfront'
import {
LambdaClient,
ListVersionsByFunctionCommand,
ListVersionsByFunctionCommandInput,
ListVersionsByFunctionCommandOutput,
} from '@aws-sdk/client-lambda'

export async function handleUpdate(settings: DeploymentSettings) {
console.info(`Going to upgrade Fingerprint Pro function association at CloudFront distbution.`)
console.info(`Settings: ${settings}`)

const latestFunctionArn = await getLambdaLatestVersionArn(settings.LambdaFunctionName)
if (!latestFunctionArn) {
return publishJobFailure('No lambda versions')
}

if (latestFunctionArn.length === 1) {
console.info('No updates yet')
return publishJobSuccess()
}

updateCloudFrontConfig(settings.CFDistributionId, settings.LambdaFunctionName, latestFunctionArn)
}

async function updateCloudFrontConfig(
cloudFrontDistributionId: string,
lambdaFunctionName: string,
latestFunctionArn: string,
) {
const cloudFrontClient = new CloudFrontClient({ region: defaults.AWS_REGION })

const configParams = {
Id: cloudFrontDistributionId,
}
const getConfigCommand = new GetDistributionConfigCommand(configParams)
const cfConfig: GetDistributionConfigCommandOutput = await cloudFrontClient.send(getConfigCommand)

if (!cfConfig.ETag || !cfConfig.DistributionConfig) {
return publishJobFailure('CloudFront distribution not found')
}

const cacheBehaviors = cfConfig.DistributionConfig.CacheBehaviors
const fpCbs = cacheBehaviors?.Items?.filter((it) => it.TargetOriginId === 'fpcdn.io')
if (!fpCbs || fpCbs?.length === 0) {
return publishJobFailure('Cache behavior not found')
}
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 publishJobFailure('Lambda function association not found')
}
const lambda = lambdas[0]
lambda.LambdaFunctionARN = latestFunctionArn

const updateParams: UpdateDistributionCommandInput = {
DistributionConfig: cfConfig.DistributionConfig,
Id: cloudFrontDistributionId,
IfMatch: cfConfig.ETag,
}

const updateConfigCommand = new UpdateDistributionCommand(updateParams)
const updateCFResult = await cloudFrontClient.send(updateConfigCommand)
console.info(`CloudFront update has finished, ${JSON.stringify(updateCFResult)}`)

console.info('Going to invalidate routes for upgraded cache behavior')
if (!cacheBehavior.PathPattern) {
return publishJobFailure('Path pattern is not defined')
}

let pathPattern = cacheBehavior.PathPattern
if (!pathPattern.startsWith('/')) {
pathPattern = '/' + pathPattern
}

const invalidationParams: CreateInvalidationCommandInput = {
DistributionId: cloudFrontDistributionId,
InvalidationBatch: {
Paths: {
Quantity: 1,
Items: [pathPattern],
},
CallerReference: 'fingerprint-pro-management-lambda-function',
},
}
const invalidationCommand = new CreateInvalidationCommand(invalidationParams)
const invalidationResult = await cloudFrontClient.send(invalidationCommand)
console.info(`Invalidation has finished, ${JSON.stringify(invalidationResult)}`)
}

async function getLambdaLatestVersionArn(functionName: string): Promise<string | undefined> {
const client = new LambdaClient({ region: REGION })
const params: ListVersionsByFunctionCommandInput = {
FunctionName: functionName,
}
const command = new ListVersionsByFunctionCommand(params)
const result: ListVersionsByFunctionCommandOutput = await client.send(command)
if (!result.Versions || result.Versions?.length === 0) {
return Promise.resolve(undefined)
}

const latest = result.Versions.filter((it) => it.Version && Number.isFinite(Number.parseInt(it.Version))).sort(
(a, b) => Number.parseInt(b.Version!!) - Number.parseInt(a.Version!!),
)[0]
return Promise.resolve(latest.FunctionArn)
}

async function publishJobSuccess() {
console.info(`Job successfully finished`)
}

async function publishJobFailure(message: string) {
console.info(`Job failed with ${message}`)
}
3 changes: 3 additions & 0 deletions mgmt-lambda/model/DefaultSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const defaults = {
AWS_REGION: 'us-east-1',
}

0 comments on commit f5b3709

Please sign in to comment.