Skip to content

Commit

Permalink
Use AWS SDK to invalidate using pulumi beforeExit hook
Browse files Browse the repository at this point in the history
  • Loading branch information
H0R5E committed May 17, 2024
1 parent e61b868 commit 8c5ec05
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 103 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"vitest": "^1.6.0"
},
"dependencies": {
"@aws-sdk/client-cloudfront": "^3.577.0",
"@pulumi/aws": "^6.36.0",
"@pulumi/command": "^0.10.0",
"@pulumi/pulumi": "^3.116.1",
Expand Down
22 changes: 11 additions & 11 deletions stacks/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,36 @@ import {
buildStatic,
buildCDN,
createAliasRecord,
buildInvalidator,
createInvalidation,
} from './resources.js'

const pulumiConfig = new pulumi.Config()
const edgePath = pulumiConfig.get('edgePath')
const staticPath = pulumiConfig.get('staticPath')
const prerenderedPath = pulumiConfig.get('prerenderedPath')
const edgePath = pulumiConfig.require('edgePath')
const staticPath = pulumiConfig.require('staticPath')
const prerenderedPath = pulumiConfig.require('prerenderedPath')
const serverArn = pulumiConfig.require('serverArn')
const optionsArn = pulumiConfig.require('optionsArn')
const FQDN = pulumiConfig.get('FQDN')
const serverHeadersStr = pulumiConfig.get('serverHeaders')
const serverArn = pulumiConfig.get('serverArn')
const optionsArn = pulumiConfig.get('optionsArn')

let serverHeaders: string[] = []

if (serverHeadersStr) {
serverHeaders = JSON.parse(serverHeadersStr)
}

const iamForLambda = getLambdaRole([serverArn!, optionsArn!])
const routerHandler = buildRouter(iamForLambda, edgePath!)
const iamForLambda = getLambdaRole([serverArn, optionsArn])
const routerHandler = buildRouter(iamForLambda, edgePath)

let certificateArn: pulumi.Input<string> | undefined

if (FQDN) {
const [_, zoneName, ...MLDs] = FQDN.split('.')
const domainName = [zoneName, ...MLDs].join('.')
certificateArn = validateCertificate(FQDN!, domainName)
certificateArn = validateCertificate(FQDN, domainName)
}

const bucket = buildStatic(staticPath!, prerenderedPath!)
const bucket = buildStatic(staticPath, prerenderedPath)
const distribution = buildCDN(
routerHandler,
bucket,
Expand All @@ -54,7 +54,7 @@ var getOrigins: (string | pulumi.Output<string>)[] = [
]
FQDN && getOrigins.push(`https://${FQDN}`)

buildInvalidator(distribution, staticPath!, prerenderedPath!)
distribution.id.apply((id) => createInvalidation(id))

export const allowedOrigins = getOrigins
export const appUrl = FQDN
Expand Down
113 changes: 35 additions & 78 deletions stacks/main/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import * as fs from 'fs'
import * as path from 'path'
import * as mime from 'mime-types'

import * as cloudfront from '@aws-sdk/client-cloudfront'
import * as aws from '@pulumi/aws'
import * as pulumi from '@pulumi/pulumi'
import { local } from '@pulumi/command'

import { NameRegister } from '../utils.js'

const pulumiConfig = new pulumi.Config('aws')

const nameRegister = NameRegister.getInstance()
let registerName = (name: string): string => {
return nameRegister.registerName(name)
Expand Down Expand Up @@ -401,85 +403,40 @@ export function getDomainAndSubdomain(domain: string): {
}
}

export function buildInvalidator(
distribution: aws.cloudfront.Distribution,
staticPath: string,
prerenderedPath: string,
) {
interface PathHashResourceInputs {
path: pulumi.Input<string>
}

interface PathHashInputs {
path: string
}

interface PathHashOutputs {
hash: string
// Source: https://www.pulumi.com/blog/next-level-iac-pulumi-runtime-logic/
export function createInvalidation(id: string) {
// Only invalidate after a deployment.
if (pulumi.runtime.isDryRun()) {
console.log('This is a Pulumi preview, so skipping cache invalidation.')
return
}

const pathHashProvider: pulumi.dynamic.ResourceProvider = {
async create(inputs: PathHashInputs) {
const folderHash = await import('folder-hash')
const pathHash = await folderHash.hashElement(inputs.path)
return { id: inputs.path, outs: { hash: pathHash.toString() } }
},
async diff(
id: string,
previousOutput: PathHashOutputs,
news: PathHashInputs,
): Promise<pulumi.dynamic.DiffResult> {
const replaces: string[] = []
let changes = true

const oldHash = previousOutput.hash
const folderHash = await import('folder-hash')
const newHash = await folderHash.hashElement(news.path)

if (oldHash === newHash.toString()) {
changes = false
}

return {
deleteBeforeReplace: false,
replaces: replaces,
changes: changes,
}
},
async update(id, olds: PathHashInputs, news: PathHashInputs) {
const folderHash = await import('folder-hash')
const pathHash = await folderHash.hashElement(news.path)
return { outs: { hash: pathHash.toString() } }
},
}

class PathHash extends pulumi.dynamic.Resource {
public readonly hash!: pulumi.Output<string>
constructor(
name: string,
args: PathHashResourceInputs,
opts?: pulumi.CustomResourceOptions,
) {
super(pathHashProvider, name, { hash: undefined, ...args }, opts)
}
}

let staticHash = new PathHash(registerName('StaticHash'), {
path: staticPath,
})
const region = pulumiConfig.require('region')

process.on('beforeExit', () => {
const client = new cloudfront.CloudFrontClient({ region })
const command = new cloudfront.CreateInvalidationCommand({
DistributionId: id,
InvalidationBatch: {
CallerReference: `invalidation-${Date.now()}`,
Paths: {
Quantity: 1,
Items: ['/*'],
},
},
})

let prerenderedHash = new PathHash(registerName('PrerenderedHash'), {
path: prerenderedPath!,
client
.send(command)
.then((result) => {
console.log(
`Invalidation status for ${id}: ${result.Invalidation?.Status}.`,
)
process.exit(0)
})
.catch((error) => {
console.error(error)
process.exit(1)
})
})

const invalidationCommand = new local.Command(
registerName('Invalidate'),
{
create: pulumi.interpolate`aws cloudfront create-invalidation --distribution-id ${distribution.id} --paths /\*`,
triggers: [staticHash.hash, prerenderedHash.hash],
},
{
dependsOn: [distribution],
},
)
}
16 changes: 8 additions & 8 deletions stacks/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { getLambdaRole, buildLambda } from './resources.js'
import { getEnvironment } from '../utils.js'

const pulumiConfig = new pulumi.Config()
const projectPath = pulumiConfig.get('projectPath')
const serverPath = pulumiConfig.get('serverPath')
const optionsPath = pulumiConfig.get('optionsPath')
const memorySizeStr = pulumiConfig.get('memorySize')
const projectPath = pulumiConfig.require('projectPath')
const serverPath = pulumiConfig.require('serverPath')
const optionsPath = pulumiConfig.require('optionsPath')
const memorySizeStr = pulumiConfig.require('memorySize')
const allowedOriginsStr = pulumiConfig.get('allowedOrigins')
let serverInvokeMode = pulumiConfig.get('serverInvokeMode')

const memorySize = Number(memorySizeStr!)
const memorySize = Number(memorySizeStr)

let optionsEnv: any = {}

Expand All @@ -24,12 +24,12 @@ if (!serverInvokeMode) {
}

const iamForLambda = getLambdaRole()
const environment = getEnvironment(projectPath!)
const environment = getEnvironment(projectPath)

const serverURL = buildLambda(
'LambdaServer',
iamForLambda,
serverPath!,
serverPath,
environment.parsed,
memorySize,
serverInvokeMode,
Expand All @@ -38,7 +38,7 @@ const serverURL = buildLambda(
const optionsURL = buildLambda(
'LambdaOptions',
iamForLambda,
optionsPath!,
optionsPath,
optionsEnv,
)

Expand Down
17 changes: 14 additions & 3 deletions tests/stacks.main.index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,24 @@ describe('stacks/main/index.ts', () => {
})

it('Without FQDN', async () => {
let applyMethod: any
;(resources.buildRouter as any).mockImplementation(() => {
return 'mock'
})
;(resources.buildCDN as any).mockImplementation(() => {
return {
domainName: 'example.com',
id: { apply: (x: any) => (applyMethod = x) },
}
})
// @ts-ignore
pulumi.Config = vi.fn(() => {
return {
get: vi.fn((x) => {
return ''
}),
require: vi.fn((x) => {
console.log(x)
if (x === 'serverHeaders') {
return '{"mock": "mock"}'
}
Expand All @@ -49,7 +55,7 @@ describe('stacks/main/index.ts', () => {
expect(resources.buildStatic).toHaveBeenCalledTimes(1)
expect(resources.buildCDN).toHaveBeenCalledTimes(1)
expect(resources.createAliasRecord).toHaveBeenCalledTimes(0)
expect(resources.buildInvalidator).toHaveBeenCalledTimes(1)
expect(applyMethod).toBeTypeOf('function')

const allowedOrigin = await promiseOf(
infra.allowedOrigins[0] as pulumi.Output<string>,
Expand All @@ -67,16 +73,21 @@ describe('stacks/main/index.ts', () => {
;(resources.buildCDN as any).mockImplementation(() => {
return {
domainName: 'example.com',
id: { apply: (x: any) => null },
}
})
// @ts-ignore
pulumi.Config = vi.fn(() => {
return {
get: vi.fn((x) => {
if (x === 'FQDN') {
return fqdn
}
return ''
}),
require: vi.fn((x) => {
if (x === 'serverHeaders') {
return '{"mock": "mock"}'
} else if (x === 'FQDN') {
return fqdn
}
return ''
}),
Expand Down
12 changes: 12 additions & 0 deletions tests/stacks.main.resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ describe('stacks/main/resources.ts', () => {
vi.resetModules()
mocks = new MyMocks()
pulumi.runtime.setMocks(mocks)

// @ts-ignore
pulumi.Config = vi.fn(() => {
return {
require: vi.fn((x) => {
if (x === 'aws:region') {
return '{"aws:region": "mock"}'
}
}),
}
})

infra = await import('../stacks/main/resources.js')
})

Expand Down
12 changes: 9 additions & 3 deletions tests/stacks.server.index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ describe('stacks/server/index.ts', () => {
pulumi.Config = vi.fn(() => {
return {
get: vi.fn((x) => {
if (x === 'projectPath') {
return tmpDir
}
if (x === 'allowedOrigins') {
return '[example.com]'
}
return ''
}),
require: vi.fn((x) => {
if (x === 'projectPath') {
return tmpDir
}
if (x === 'memorySize') {
return '256'
}
Expand Down Expand Up @@ -107,6 +110,9 @@ describe('stacks/server/index.ts', () => {
pulumi.Config = vi.fn(() => {
return {
get: vi.fn((x) => {
return ''
}),
require: vi.fn((x) => {
if (x === 'projectPath') {
return tmpDir
}
Expand Down

0 comments on commit 8c5ec05

Please sign in to comment.