Home Reference Source

src/index.js

import _ from 'lodash'
import AWS from 'aws-sdk'
import childProcess from 'child_process'
import winston from 'winston'
import Credstash from 'credstash'
import deasyncPromise from 'deasync-promise'

const CREDSTASH_PREFIX = 'credstash'

function _fetchCred(name, credstash) {
  return new Promise((resolve, reject) => {
    credstash.get(name, (err, secret) => {
      if (err) {
        reject(err)
      } else {
        resolve(secret)
      }
    })
  })
}

class ServerlessDeployEnvironment {
  constructor(serverless, options) {
    this.serverless = serverless
    this.credstash = new Credstash()

    // Only meaningful for AWS
    this.provider = 'aws'
    this.config = serverless.service.custom.deployEnvironment
    this.options = options
    this.commands = {
      runWithEnvironment: {
        usage: 'Runs the specified command with the serverless environment variables set',
        lifecycleEvents: ['run'],
        options: {
          command: {
            usage: 'The command to run',
            shortcut: 'c',
            type: 'string'
          },
          stage: {
            usage: 'The stage to use for stage-specific variables',
            shortcut: 's',
            type: 'string'
          },
          args: {
            usage: 'Extra arguments to pass through to the subprocess',
            shortcut: 'a',
            type: 'multiple'
          }
        }
      }
    }
    // Run automatically as part of the deploy
    this.hooks = {
      // Hook before deploying the function
      'before:deploy:createDeploymentArtifacts': () => this._addDeployEnvironment(),
      // Hook before running sls offline
      'before:offline:start:init': () => this._addDeployEnvironment(),
      // Hook before running sls webpack invoke
      'before:webpack:invoke:invoke': () => this._addDeployEnvironment(),
      // Command hook
      'runWithEnvironment:run': () => this._runWithEnvironment()
    }

    const stage = options.stage || _.get(serverless, 'service.custom.defaults.stage')
    if (!stage) {
      throw new Error('No stage found for serverless-plugin-deploy-environment')
    }
    winston.debug(`Getting deploy variables for stage ${stage}`)

    // TODO: This doesn't belong here, but we need to set the options before populating the new properties.
    serverless.variables.options = options // eslint-disable-line

    // Allow credstash variables to be resolved
    // TODO(msills): Break into a separate plugin
    const delegate = serverless.variables.getValueFromSource.bind(serverless.variables)
    const credstash = this.credstash
    serverless.variables.getValueFromSource = function getValueFromSource(variableString) { // eslint-disable-line no-param-reassign, max-len
      if (variableString.startsWith(`${CREDSTASH_PREFIX}:`)) {
        // If we are not to resolve credstash variables here, just write the variable through unchanged
        if (options.credstash && options.credstash !== 'true') {
          winston.info(`Skipping credstash resolution for variable '${variableString}'`)
          return Promise.resolve(variableString)
        }

        // Configure the AWS region
        const region = serverless.service.provider.region
        if (!region) {
          return Promise.reject(new Error('Cannot hydrate Credstash variables without a region'))
        }
        AWS.config.update({ region })

        const key = variableString.split(`${CREDSTASH_PREFIX}:`)[1]
        return _fetchCred(key, credstash)
      }

      return delegate(variableString)
    }

    if (!serverless.service.custom.deploy) {
      winston.warn('No deploy object found in custom, even though the serverless-deploy-environment plugin is loaded.')
    }

    const deployVariables = _.get(serverless, 'service.custom.deploy.variables', { })
    const deployEnvironment = _.get(serverless, 'service.custom.deploy.environments', { })

    // Explicitly load the variable syntax, so that calls to populateProperty work
    // TODO(msills): Figure out how to avoid this. For now, it seems safe.
    serverless.variables.loadVariableSyntax()
    // Explicitly resolve these here, so that we can apply any transformations that we want
    const vars = deasyncPromise(serverless.variables.populateProperty(deployVariables, false))
    serverless.service.deployVariables = _.merge({}, vars.default || {}, vars[stage]) // eslint-disable-line
    const envs = deasyncPromise(serverless.variables.populateProperty(deployEnvironment, false)) // eslint-disable-line
    serverless.service.deployEnvironment = _.merge({}, envs.default || {}, envs[stage]) // eslint-disable-line
  }

  async _resolveDeployEnvironment() {
    return this.serverless.service.deployEnvironment
  }

  async _addDeployEnvironment() {
    const env = await this._resolveDeployEnvironment()
    // Make sure that the environment exists (if no environment is specified, it's undefined), and augment it with the
    // scoped environment
    this.serverless.service.provider.environment = _.extend(this.serverless.service.provider.environment, env)
  }

  async _runWithEnvironment() {
    const deployEnv = await this._resolveDeployEnvironment()
    const env = _.merge({}, process.env, deployEnv) // Merge the current environment, overridden with the deploy environment
    const args = this.options.args || ''
    const output = childProcess.execSync(`${this.options.command} ${args}`, { env, cwd: process.cwd() }).toString()
    for (const line of output.split('\n')) {
      winston.info(`[COMMAND OUTPUT]: ${line}`)
    }
  }
}

module.exports = ServerlessDeployEnvironment