Home Reference Source

src/index.js

  1. import _ from 'lodash'
  2. import AWS from 'aws-sdk'
  3. import childProcess from 'child_process'
  4. import winston from 'winston'
  5. import Credstash from 'credstash'
  6. import deasyncPromise from 'deasync-promise'
  7.  
  8. const CREDSTASH_PREFIX = 'credstash'
  9.  
  10. function _fetchCred(name, credstash) {
  11. return new Promise((resolve, reject) => {
  12. credstash.get(name, (err, secret) => {
  13. if (err) {
  14. reject(err)
  15. } else {
  16. resolve(secret)
  17. }
  18. })
  19. })
  20. }
  21.  
  22. class ServerlessDeployEnvironment {
  23. constructor(serverless, options) {
  24. this.serverless = serverless
  25. this.credstash = new Credstash()
  26.  
  27. // Only meaningful for AWS
  28. this.provider = 'aws'
  29. this.config = serverless.service.custom.deployEnvironment
  30. this.options = options
  31. this.commands = {
  32. runWithEnvironment: {
  33. usage: 'Runs the specified command with the serverless environment variables set',
  34. lifecycleEvents: ['run'],
  35. options: {
  36. command: {
  37. usage: 'The command to run',
  38. shortcut: 'c',
  39. type: 'string'
  40. },
  41. stage: {
  42. usage: 'The stage to use for stage-specific variables',
  43. shortcut: 's',
  44. type: 'string'
  45. },
  46. args: {
  47. usage: 'Extra arguments to pass through to the subprocess',
  48. shortcut: 'a',
  49. type: 'multiple'
  50. }
  51. }
  52. }
  53. }
  54. // Run automatically as part of the deploy
  55. this.hooks = {
  56. // Hook before deploying the function
  57. 'before:deploy:createDeploymentArtifacts': () => this._addDeployEnvironment(),
  58. // Hook before running sls offline
  59. 'before:offline:start:init': () => this._addDeployEnvironment(),
  60. // Hook before running sls webpack invoke
  61. 'before:webpack:invoke:invoke': () => this._addDeployEnvironment(),
  62. // Command hook
  63. 'runWithEnvironment:run': () => this._runWithEnvironment()
  64. }
  65.  
  66. const stage = options.stage || _.get(serverless, 'service.custom.defaults.stage')
  67. if (!stage) {
  68. throw new Error('No stage found for serverless-plugin-deploy-environment')
  69. }
  70. winston.debug(`Getting deploy variables for stage ${stage}`)
  71.  
  72. // TODO: This doesn't belong here, but we need to set the options before populating the new properties.
  73. serverless.variables.options = options // eslint-disable-line
  74.  
  75. // Allow credstash variables to be resolved
  76. // TODO(msills): Break into a separate plugin
  77. const delegate = serverless.variables.getValueFromSource.bind(serverless.variables)
  78. const credstash = this.credstash
  79. serverless.variables.getValueFromSource = function getValueFromSource(variableString) { // eslint-disable-line no-param-reassign, max-len
  80. if (variableString.startsWith(`${CREDSTASH_PREFIX}:`)) {
  81. // If we are not to resolve credstash variables here, just write the variable through unchanged
  82. if (options.credstash && options.credstash !== 'true') {
  83. winston.info(`Skipping credstash resolution for variable '${variableString}'`)
  84. return Promise.resolve(variableString)
  85. }
  86.  
  87. // Configure the AWS region
  88. const region = serverless.service.provider.region
  89. if (!region) {
  90. return Promise.reject(new Error('Cannot hydrate Credstash variables without a region'))
  91. }
  92. AWS.config.update({ region })
  93.  
  94. const key = variableString.split(`${CREDSTASH_PREFIX}:`)[1]
  95. return _fetchCred(key, credstash)
  96. }
  97.  
  98. return delegate(variableString)
  99. }
  100.  
  101. if (!serverless.service.custom.deploy) {
  102. winston.warn('No deploy object found in custom, even though the serverless-deploy-environment plugin is loaded.')
  103. }
  104.  
  105. const deployVariables = _.get(serverless, 'service.custom.deploy.variables', { })
  106. const deployEnvironment = _.get(serverless, 'service.custom.deploy.environments', { })
  107.  
  108. // Explicitly load the variable syntax, so that calls to populateProperty work
  109. // TODO(msills): Figure out how to avoid this. For now, it seems safe.
  110. serverless.variables.loadVariableSyntax()
  111. // Explicitly resolve these here, so that we can apply any transformations that we want
  112. const vars = deasyncPromise(serverless.variables.populateProperty(deployVariables, false))
  113. serverless.service.deployVariables = _.merge({}, vars.default || {}, vars[stage]) // eslint-disable-line
  114. const envs = deasyncPromise(serverless.variables.populateProperty(deployEnvironment, false)) // eslint-disable-line
  115. serverless.service.deployEnvironment = _.merge({}, envs.default || {}, envs[stage]) // eslint-disable-line
  116. }
  117.  
  118. async _resolveDeployEnvironment() {
  119. return this.serverless.service.deployEnvironment
  120. }
  121.  
  122. async _addDeployEnvironment() {
  123. const env = await this._resolveDeployEnvironment()
  124. // Make sure that the environment exists (if no environment is specified, it's undefined), and augment it with the
  125. // scoped environment
  126. this.serverless.service.provider.environment = _.extend(this.serverless.service.provider.environment, env)
  127. }
  128.  
  129. async _runWithEnvironment() {
  130. const deployEnv = await this._resolveDeployEnvironment()
  131. const env = _.merge({}, process.env, deployEnv) // Merge the current environment, overridden with the deploy environment
  132. const args = this.options.args || ''
  133. const output = childProcess.execSync(`${this.options.command} ${args}`, { env, cwd: process.cwd() }).toString()
  134. for (const line of output.split('\n')) {
  135. winston.info(`[COMMAND OUTPUT]: ${line}`)
  136. }
  137. }
  138. }
  139.  
  140. module.exports = ServerlessDeployEnvironment