seagull-js/seagull

View on GitHub
packages/deploy-aws/src/templates/seagull_project.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { FS } from '@seagull/commands-fs'
import { SDK } from 'aws-cdk'
import * as dotenv from 'dotenv'
import 'ts-node/register'
import * as aws from '../aws_sdk_handler'
import { emptyBucket, S3Handler } from '../aws_sdk_handler'
import * as lib from '../lib'
import { ProvideAssetFolder } from '../provide_asset_folder'
import { SeagullApp } from '../seagull_app'
import { setCredsByProfile } from '../set_aws_credentials'
import { Rule } from '../types'

interface SeagullProjectProps {
  account: string
  appPath: string
  branch: string
  stage: string
  profile: string
  region: string
  vpcId: string
  subnetIds: string
  handlers?: {
    acmHandler?: aws.ACMHandler
    cloudfrontHandler?: aws.CloudfrontHandler
    stsHandler?: aws.STSHandler
  }
}

export class SeagullProject {
  account: string
  appPath: string
  branch: string
  stage: string
  pkgJson: any
  profile: string
  region: string
  vpcId: string
  subnetIds: string[]
  acm: aws.ACMHandler
  cloudfront: aws.CloudfrontHandler
  sts: aws.STSHandler

  constructor(props: SeagullProjectProps) {
    this.account = props.account
    this.appPath = props.appPath
    this.branch = props.branch
    this.stage = props.stage
    this.profile = props.profile
    this.region = props.region
    this.pkgJson = require(`${this.appPath}/package.json`)
    this.vpcId = props.vpcId
    this.subnetIds = props.subnetIds && props.subnetIds.split(',') || []
    setCredsByProfile(this.profile)
    const propsACMHandler = props.handlers && props.handlers.acmHandler
    const propsCFHandler = props.handlers && props.handlers.cloudfrontHandler
    const propsSTSHandler = props.handlers && props.handlers.stsHandler
    this.acm = propsACMHandler || new aws.ACMHandler()
    this.cloudfront = propsCFHandler || new aws.CloudfrontHandler()
    this.sts = propsSTSHandler || new aws.STSHandler()
  }

  async createSeagullApp() {
    // preparations for deployment
    const name = this.getAppName()
    const globalProps = {
      accountId: (await aws.getAccountId(this.sts)) || '',
      branch: this.branch,
      projectName: this.pkgJson.name,
      region: this.region,
      stage: this.stage,
    }
    const itemProps = Object.assign({}, globalProps, { topic: 'items' })
    const logsProps = Object.assign({}, globalProps, { topic: 'logs' })
    const errorProps = Object.assign({}, globalProps, { topic: 'errors' })
    const itemBucketName = lib.getBucketName(itemProps)
    const logBucketName = lib.getBucketName(logsProps, true)
    const errorBucketName = lib.getBucketName(errorProps, true)
    const actions: string[] = [
      'sts:AssumeRole',
      'logs:*',
      'lambda:InvokeFunction',
      'lambda:InvokeAsync',
      'ses:*',
      's3:*',
      'ec2:*',
      'events:*',
      'cloudwatch:*',
    ]
    const aliasConfig = await aws.checkForAliasConfig(this.pkgJson, this.acm)

    // create the asset folder
    await addResources(this.appPath, itemBucketName)

    // construct the stack and the app
    const app = await this.createBareApp()
    const role = app.stack.addIAMRole('role', 'lambda.amazonaws.com', actions)
    app.role = role
    const env = await getEnv(name, this.appPath, this.stage, logBucketName)
    const logBucket = app.stack.addS3(logBucketName, role, false)
    const errorBucket = app.stack.addS3(errorBucketName, role, false)
    errorBucket.grantPublicAccess()
    const vpc = app.stack.addVPC('vpc', this.vpcId, this.subnetIds, [ this.region ])
    const lambda = app.stack.addLambda('lambda', this.appPath, role, vpc, env)
    const apiGW = app.stack.addUniversalApiGateway('apiGW', lambda, this.stage)
    const cloudfrontConfig = {
      aliasConfig,
      apiGateway: apiGW,
      errorBucket,
      logBucket,
    }
    app.stack.addCloudfront('cloudfront', cloudfrontConfig)
    const s3DeploymentNeeded = this.stage === 'prod' || this.branch === 'master'
    const importS3 = () => app.stack.importS3(itemBucketName, role)
    const addS3 = () => app.stack.addS3(itemBucketName, role)
    s3DeploymentNeeded ? addS3() : importS3()
    app.stack.addLogGroup(`/aws/lambda/${name}-lambda-handler`)
    app.stack.addLogGroup(`/${name}/data-log`)
    const addCrons = this.stage === 'prod' || this.branch === 'master'
    const cronJson = addCrons ? await buildCronJson(this.appPath) : []
    const addRule = (rule: Rule) => app.stack.addEventRule(rule, lambda)
    cronJson.forEach(addRule)

    return app
  }

  async createBareApp() {
    const account = this.account || (await new SDK({}).defaultAccount())
    const itemsProps = {
      accountId: (await aws.getAccountId(this.sts)) || '',
      branch: this.branch,
      projectName: this.pkgJson.name,
      region: this.region,
      stage: this.stage,
      topic: 'items',
    }
    const appProps = {
      addAssets: true,
      appPath: this.appPath,
      itemsBucket: lib.getBucketName(itemsProps),
      projectName: this.getAppName(),
      stackProps: { env: { account, region: this.region } },
    }
    return new SeagullApp(appProps)
  }

  async customizeStack(app: SeagullApp) {
    const extensionPath = `${this.appPath}/infrastructure-aws.ts`
    const hasExtensionFile = await new FS.Exists(extensionPath).execute()
    return hasExtensionFile && (await this.loadAndExecute(extensionPath, app))
  }

  async loadAndExecute(path: string, app: SeagullApp) {
    const extensionFkt = (await import(`${path}`)).default
    await extensionFkt(app)
  }

  getAppName() {
    const suffix = this.stage === 'test' ? `-${this.branch}-${this.stage}` : ''
    return `${this.pkgJson.name}${suffix}`.replace(/[^0-9A-Za-z-]/g, '')
  }

  async deployProject() {
    this.validate()
    const app = await this.createSeagullApp()
    await this.customizeStack(app)
    await app.deployStack()
    const url = (await aws.getCFURL(this.getAppName(), this.cloudfront)) || ''
    await new FS.WriteFile('/tmp/cfurl.txt', url).execute()
    console.info('cloudfront-url', url)
  }

  async destroyProject() {
    this.validate()
    const app = await this.createBareApp()
    const logsProps = {
      accountId: (await aws.getAccountId(this.sts)) || '',
      branch: this.branch,
      projectName: this.pkgJson.name,
      region: this.region,
      stage: this.stage,
      topic: 'logs',
    }
    const logBucket = await lib.getBucketName(logsProps, true)
    await emptyBucket(new S3Handler(), logBucket)
    await app.destroyStack()
  }

  async diffProject() {
    const app = await this.createSeagullApp()
    await this.customizeStack(app)
    await app.diffStack()
  }

  validate() {
    const hasValidProfile = setCredsByProfile(this.profile)
    // tslint:disable-next-line:no-unused-expression
    !hasValidProfile && lib.noCredentialsSet()
  }
}

async function addResources(appPath: string, itemsBucket?: string) {
  const provideAssetFolder = new ProvideAssetFolder(appPath)
  await provideAssetFolder.execute()
  // tslint:disable-next-line:no-unused-expression
  itemsBucket && (await replaceS3BucketName(appPath, itemsBucket))
}

async function replaceS3BucketName(appPath: string, itemsBucket: string) {
  const lambdaPath = `.seagull/deploy/dist/assets/backend/lambda.js`
  const lambdaFile = `${appPath}/${lambdaPath}`
  const lambda = await new FS.ReadFile(lambdaFile).execute()
  const lambdaWithBucketName = lambda.replace('demo-bucket', itemsBucket)
  await new FS.WriteFile(lambdaFile, lambdaWithBucketName).execute()
}

async function buildCronJson(appPath: string) {
  const cronPath = `${appPath}/dist/cron.json`
  const cronFile = await new FS.ReadFile(cronPath).execute()
  return cronFile && cronFile !== '' ? JSON.parse(cronFile) : []
}

async function getEnv(
  name: string,
  appPath: string,
  stage: string,
  logBucket: string
) {
  const env: any = {
    APP: name,
    LOG_BUCKET: logBucket,
    MODE: 'cloud',
    NODE_ENV: 'production',
    STAGE: stage,
  }
  const configPath = `${appPath}/.env.${stage}`
  const config: string = await new FS.ReadFile(configPath).execute()
  const configEnv = dotenv.parse(config)
  return {
    ...env,
    ...configEnv,
  }
}