UNPKG

mira

Version:

NearForm Accelerator for Cloud Native Serverless AWS

274 lines (257 loc) 8.42 kB
/** * Deploys Custom::CDKBucketDeployment resources without using the CDK * toolchain. */ import AWS from 'aws-sdk' import assert from 'assert' import fs from 'fs' import colors from 'colors/safe' import cp from 'child_process' import config from 'config' import { assumeRole, getRoleArn } from '../assume-role' import { getInstanceResourcesByType } from '../sdk/cloudformation' import glob from 'glob' import { MiraApp } from './app' import { MiraConfig, Account } from '../config/mira-config' import { PromiseResult } from 'aws-sdk/lib/request' import { Bucket } from 'aws-sdk/clients/s3' let cdkFiles = fs.existsSync('cdk.out') ? fs.readdirSync('cdk.out') : [] let miraS3: AWS.S3 interface LooseObject { /* eslint-disable-next-line */ [key: string]: any } /** * Gets the files within an asset folder. */ export const getAssetFiles = async (id: string): Promise<Array<string>> => { assert(fs.existsSync(getAssetPrefix(id)) && fs.statSync(getAssetPrefix(id)).isDirectory(), 'A provided asset ID' + ' either did not exist or was not a directory. Was this intended?') return new Promise((resolve, reject) => { glob(`${getAssetPrefix(id)}/**/*`, (err: Error | null, matches: string[]) => { if (err) { reject(err) } resolve(matches.map((match: string) => match.substr(getAssetPrefix(id).length + 1))) }) }) } /** * Gets the asset prefix given some ID. */ export const getAssetPrefix = (id: string): string => `cdk.out/asset.${id}` /** * Gets the objects from a bucket. */ export const getBucketObjects = async (Bucket: string): Promise<PromiseResult<AWS.S3.ListObjectsOutput, AWS.AWSError>> => { const s3 = await getS3() return s3.listObjects({ Bucket }).promise() } interface CDKTemplateResource { Type: string Properties: { DestinationBucketName: { Ref: string } SourceBucketNames: Array<{Ref: string}> } } interface CDKTemplate { Resources: {[key: string]: CDKTemplateResource} } /** * Gets references for bucket. */ export const getBucketRefs = async (): Promise<LooseObject> => { const files = getTemplateFiles() const bucketsBySite = await getSiteBuckets() for (const file in files) { const template: CDKTemplate = files[file] as CDKTemplate if (!template.Resources) { continue } for (const name in template.Resources) { if (!template.Resources[name]) { continue } const { Type, Properties } = template.Resources[name] if (!Type) { continue } if (Type !== 'Custom::CDKBucketDeployment') { continue } if (!bucketsBySite[Properties.DestinationBucketName.Ref]) { // TODO: Throw an error or provide warning? console.warn('Something unexpected happened. Found a ' + 'Custom::CDKBucketDeployment with a DestinationBucketName' + ' that is unknown.', Properties.DestinationBucketName.Ref) continue } bucketsBySite[Properties.DestinationBucketName.Ref].assets = Properties.SourceBucketNames.map(({ Ref }: LooseObject) => { return Ref.split(/AssetParameters/g)[1].split(/S3Bucket/g)[0] }) } } return bucketsBySite } /** * Given some template JSON, grabs all resource objects that are of type * AWS::S3::Bucket. */ export const getBucketResources = (): LooseObject => { const files = getTemplateFiles() const bucketsByFile = {} as LooseObject for (const file in files) { const template = files[file] if (!template.Resources) { continue } for (const name in template.Resources) { const { Type } = template.Resources[name] if (!Type) { continue } if (Type !== 'AWS::S3::Bucket') { continue } if (!bucketsByFile[file]) { bucketsByFile[file] = {} } bucketsByFile[file][name] = template.Resources[name] } } return bucketsByFile } /** * Gets the environment for Mira. */ export const getEnvironment = (): Account => { const env = MiraConfig.getEnvironment() return env } /** * Gets the S3 object. */ export const getS3 = async (): Promise<AWS.S3> => { if (miraS3) { return miraS3 } const role = getRoleArn(config.get(`accounts.${getEnvironment().name}.profile`)) const awsConfig = await assumeRole(role) AWS.config = awsConfig miraS3 = new AWS.S3({ apiVersion: '2006-03-01' }) return miraS3 } /** * Gets S3 buckets beginning with a prefix. * @param {String} prefix * @param {String} siteName */ export const getS3Buckets = async (prefix: string, siteName: string): Promise<Bucket[]> => { const s3 = await getS3() const response: AWS.S3.ListBucketsOutput = await s3.listBuckets().promise() if (!response || !response.Buckets) { throw new Error('Failed to retrieve buckets.') } prefix = prefix.toLowerCase().slice(0, 30) siteName = siteName.toLowerCase() const bucketPrefix = `${prefix}-${siteName}` const targetBuckets = response.Buckets.filter(({ Name }: LooseObject) => { return Name.startsWith(bucketPrefix) }) return targetBuckets } /** * For a given template file, gets all site buckets. */ export const getSiteBuckets = async (): Promise<LooseObject> => { const files = getTemplateFiles() const siteBuckets = {} as LooseObject const bucketsByFile: LooseObject = getBucketResources() for (const file in files) { if (!bucketsByFile[file]) { continue } for (const name in bucketsByFile[file]) { const { Properties } = bucketsByFile[file][name] const { Value: stackName } = Properties.Tags.find(({ Key }: LooseObject) => Key === 'StackName') const s3Buckets = await getS3Buckets(stackName, name) siteBuckets[name] = { s3: s3Buckets.map(({ Name }: LooseObject) => Name) } } } return siteBuckets } /** * Gets the template files for the given CWD. */ export const getTemplateFiles = (): LooseObject => { const templateFiles = {} as LooseObject cdkFiles = cdkFiles.filter((file: string) => file.endsWith('.template.json')) for (const file of cdkFiles) { templateFiles[file] = JSON.parse(fs.readFileSync(`cdk.out/${file}`, 'utf8')) } return templateFiles } /** * Removes assets directories. */ export const removeAssetDirectories = (): void => { for (const dir of cdkFiles) { if (fs.statSync(`cdk.out/${dir}`).isDirectory()) { cp.execSync(`rm -rf ${dir}`, { cwd: `${process.cwd()}/cdk.out` }) } } } /** * Quickly deploys an asset bundle generated by CDK to an intended S3 bucket * as defined by a CDK generated Cfn template. */ export const quickDeploy = async (): Promise<void> => { const sites = await getBucketRefs() const s3 = await getS3() for (const site in sites) { const { s3: buckets, assets } = sites[site] for (const Bucket of buckets) { console.info(colors.yellow('Updating Bucket'), Bucket) for (const id of assets) { const files = await getAssetFiles(id) for (const file of files) { const obj = { ACL: 'public-read', Body: fs.readFileSync(`${getAssetPrefix(id)}/${file}`, 'utf8'), Bucket, ContentType: require('mime-types').lookup(file), Key: file } if (MiraApp.isVerbose()) { console.info(`Putting object: ${JSON.stringify(obj, null, 2)}`) } else { console.info(`\n${colors.yellow('Putting object:')}\n${file}`) } const result = await s3.putObject(obj).promise() if (MiraApp.isVerbose()) { console.info(`Put object: ${JSON.stringify(result, null, 2)}`) } console.info(`${colors.cyan('File Available at')}: https://${Bucket}.s3-${getEnvironment().env.region}.amazonaws.com/${file}`) } console.info(colors.green('Done Updating Bucket')) const stackResources = await getInstanceResourcesByType() for (const stackName in stackResources) { if (Object.keys(stackResources[stackName]).indexOf('AWS::CloudFront::Distribution') >= 0) { console.warn(colors.yellow('Deployment Notice'), 'Your stack includes a CloudFront. You may need to refresh your' + ' distribution\'s cache.') break } } } } } }