Esempio n. 1
0
const nodejsBuilder = async (dir, desc, { overrides = {} } = {}) => {
	const files = await readdir(dir)
	const tmpDirName = `now-nodejs-build-${await uid(20)}`
	const targetPath = join(tmpdir(), tmpDirName)

	debug('init nodejs project build stage in', targetPath)

	// 1. Create the temp folder that will hold the files to be zipped.
	await mkdir(targetPath)

	// produce hard links of the source files in the target dir
	await Promise.all(
		files
			.filter(name => name !== 'node_modules' && !(name in overrides))
			.map(file => {
				debug('making copy for %s', file)
				return copy(join(dir, file), join(targetPath, file))
			})
	)

	const archive = archiver('zip')

	// trigger an install if needed
	if (desc.packageJSON) {
		let buildCommand = ''

		if (existsSync(join(targetPath, 'package-lock.json'))) {
			buildCommand = 'npm install'
		} else if (existsSync(join(targetPath, 'yarn.lock'))) {
			buildCommand = 'yarn install'
		} else {
			buildCommand = 'npm install'
		}

		try {
			debug('executing %s in %s', buildCommand, targetPath)
			await exec(buildCommand, {
				cwd: targetPath,
				/*eslint-disable */
				env: Object.assign({}, process.env, {
					/*eslint-enable */
					// we set this so that we make the installers ignore
					// dev dependencies. in the future, we can add a flag
					// to ignore this behavior, or set different envs
					NODE_ENV: 'production'
				})
			})
		} catch (err) {
			throw new Error(
				`The build command ${buildCommand} failed for ${dir}: ${err.message}`
			)
		}
	} else {
		debug('ignoring build step, no manifests found')
	}

	const buffer = toBuffer(archive)

	archive.on('warning', err => {
		console.error('Warning while creating zip file', err)
	})

	for (const name in overrides) {
		archive.append(overrides[name], { name })
	}

	// we read again to get the results of the build process
	const filesToZip = await readdir(targetPath)
	await Promise.all(
		filesToZip.map(async file => {
			const path = join(targetPath, file)
			const stats = await stat(path)
			debug('adding', path)
			return stats.isDirectory()
				? archive.directory(path, file)
				: archive.file(path, { name: file })
		})
	)

	archive.finalize()

	// buffer promise
	return buffer
}
Esempio n. 2
0
const deploy = async ({
  config,
  authConfig,
  argv: argv_
}: {
  config: any,
  authConfig: any,
  argv: Array<string>
}) => {
  const argv = mri(argv_, {
    boolean: ['help'],
    alias: {
      help: 'h'
    }
  })

  // `now [provider] [deploy] [target]`
  const [cmdOrTarget = null, target_ = null] = argv._.slice(2).slice(-2)

  let target

  if (cmdOrTarget === 'aws' || cmdOrTarget === 'deploy') {
    target = target_ === null ? process.cwd() : target_
  } else {
    if (target_) {
      console.error(error('Unexpected number of arguments for deploy command'))
      return 1
    } else {
      target = cmdOrTarget === null ? process.cwd() : cmdOrTarget
    }
  }

  const start = Date.now()
  const resolved = await resolve(target)

  if (resolved === null) {
    console.error(error(`Could not resolve deployment target ${param(target)}`))
    return 1
  }

  let desc

  try {
    desc = await describeProject(resolved)
  } catch (err) {
    if (err.code === 'AMBIGOUS_CONFIG') {
      console.error(
        error(`There is more than one source of \`now\` config: ${err.files}`)
      )
      return 1
    } else {
      throw err
    }
  }

  // a set of files that we personalize for this build
  const overrides = {
    '__now_handler.js': getLambdaHandler(desc)
  }

  // initialize aws client
  const aws = getAWS(authConfig)
  const region = aws.config.region || 'us-west-1'

  console.log(
    info(
      `Deploying ${param(humanPath(resolved))} ${gray('(aws)')} ${gray(
        `(${region})`
      )}`
    )
  )
  const buildStart = Date.now()
  const stopBuildSpinner = wait('Building and bundling your app…')
  const zipFile = await build(resolved, desc, { overrides })
  stopBuildSpinner()

  // lambda limits to 50mb
  if (zipFile.length > 50 * 1024 * 1024) {
    console.error(error('The build exceeds the 50mb AWS Lambda limit'))
    return 1
  }

  console.log(
    ok(
      `Build generated a ${bold(bytes(zipFile.length))} zip ${gray(
        `[${ms(Date.now() - buildStart)}]`
      )}`
    )
  )

  const iam = new aws.IAM({ apiVersion: '2010-05-08' })

  const gateway = new aws.APIGateway({
    apiVersion: '2015-07-09',
    region
  })

  const lambda = new aws.Lambda({
    apiVersion: '2015-03-31',
    region
  })

  let role

  try {
    role = await getRole(iam, { RoleName: NOW_DEFAULT_IAM_ROLE })
  } catch (err) {
    if ('NoSuchEntity' === err.code) {
      const iamStart = Date.now()
      role = await createRole(iam, {
        AssumeRolePolicyDocument: JSON.stringify(IAM_POLICY_DOCUMENT),
        RoleName: NOW_DEFAULT_IAM_ROLE
      })
      console.log(
        ok(
          `Initialized IAM role ${param(NOW_DEFAULT_IAM_ROLE)} ${gray(
            `[${ms(iamStart - Date.now())}]`
          )}`
        )
      )
    } else {
      throw err
    }
  }

  const deploymentId = 'now-' + desc.name + '-' + (await uid(10))

  const resourcesStart = Date.now()
  const stopResourcesSpinner = wait('Creating API resources')

  debug('initializing lambda function')
  const λ = await retry(
    async bail => {
      try {
        return await createFunction(lambda, {
          Code: {
            ZipFile: zipFile
          },
          Runtime: 'nodejs6.10',
          Description: desc.description,
          FunctionName: deploymentId,
          Handler: '__now_handler.handler',
          Role: role.Role.Arn,
          Timeout: 15,
          MemorySize: 512
        })
      } catch (err) {
        if (
          err.retryable ||
          // created role is not ready
          err.code === 'InvalidParameterValueException'
        ) {
          debug('retrying creating function (%s)', err.message)
          throw err
        }

        bail(err)
      }
    },
    { minTimeout: 2500, maxTimeout: 5000 }
  )

  debug('initializing api gateway')
  const api = await createAPI(gateway, {
    name: deploymentId,
    description: desc.description
  })

  debug('retrieving root resource id')
  const resources = await getResources(gateway, {
    restApiId: api.id
  })
  const rootResourceId = resources.items[0].id

  debug('initializing gateway method for /')
  await putMethod(gateway, {
    restApiId: api.id,
    authorizationType: 'NONE',
    httpMethod: 'ANY',
    resourceId: rootResourceId
  })

  debug('initializing gateway integration for /')
  await putIntegration(gateway, {
    restApiId: api.id,
    resourceId: rootResourceId,
    httpMethod: 'ANY',
    type: 'AWS_PROXY',
    integrationHttpMethod: 'POST',
    uri: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${λ.FunctionArn}/invocations`
  })

  debug('initializing gateway resource')
  const resource = await createResource(gateway, {
    restApiId: api.id,
    parentId: rootResourceId,
    pathPart: '{proxy+}'
  })

  debug('initializing gateway method for {proxy+}')
  await putMethod(gateway, {
    restApiId: api.id,
    authorizationType: 'NONE',
    httpMethod: 'ANY',
    resourceId: resource.id
  })

  debug('initializing gateway integration for {proxy+}')
  await putIntegration(gateway, {
    restApiId: api.id,
    resourceId: resource.id,
    httpMethod: 'ANY',
    type: 'AWS_PROXY',
    integrationHttpMethod: 'POST',
    uri: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${λ.FunctionArn}/invocations`
  })

  debug('creating deployment')
  await createDeployment(gateway, {
    restApiId: api.id,
    stageName: 'now'
  })

  const [, accountId] = role.Role.Arn.match(/^arn:aws:iam::(\d+):/)

  await addPermission(lambda, {
    FunctionName: deploymentId,
    StatementId: deploymentId,
    Action: 'lambda:InvokeFunction',
    Principal: 'apigateway.amazonaws.com',
    SourceArn: `arn:aws:execute-api:${region}:${accountId}:${api.id}/now/ANY/*`
  })

  stopResourcesSpinner()
  console.log(
    ok(
      `API resources created (id: ${param(deploymentId)}) ${gray(
        `[${ms(Date.now() - resourcesStart)}]`
      )}`
    )
  )

  const url = `https://${api.id}.execute-api.${region}.amazonaws.com/now`
  const copied = copyToClipboard(url, config.copyToClipboard)

  console.log(
    success(
      `${link(url)} ${copied ? gray('(in clipboard)') : ''} ${gray(
        `[${ms(Date.now() - start)}]`
      )}`
    )
  )

  return 0
}
Esempio n. 3
0
const deploy = async (ctx: {
  config: any,
  authConfig: any,
  argv: Array<string>
}) => {
  const { argv: argv_ } = ctx
  const argv = mri(argv_, {
    boolean: ['help'],
    alias: {
      help: 'h'
    }
  })

  const token = await getToken(ctx)

  // `now [provider] [deploy] [target]`
  const [cmdOrTarget = null, target_ = null] = argv._.slice(2).slice(-2)

  let target

  if (cmdOrTarget === 'gcp' || cmdOrTarget === 'deploy') {
    target = target_ === null ? process.cwd() : target_
  } else {
    if (target_) {
      console.error(error('Unexpected number of arguments for deploy command'))
      return 1
    } else {
      target = cmdOrTarget === null ? process.cwd() : cmdOrTarget
    }
  }

  const start = Date.now()
  const resolved = await resolve(target)

  if (resolved === null) {
    console.error(error(`Could not resolve deployment target ${param(target)}`))
    return 1
  }

  let desc = null

  try {
    desc = await describeProject(resolved)
  } catch (err) {
    if (err.code === 'AMBIGOUS_CONFIG') {
      console.error(
        error(`There is more than one source of \`now\` config: ${err.files}`)
      )
      return 1
    } else {
      throw err
    }
  }

  // Example now.json for gcpConfig
  // {
  //   functionName: String,
  //   timeout: String,
  //   memory: Number,
  //   region: String
  // }
  const { nowJSON: { gcp: gcpConfig } } = desc

  const overrides = {
    'function.js': getFunctionHandler(desc)
  }

  const region = gcpConfig.region || 'us-central1'

  console.log(
    info(
      `Deploying ${param(humanPath(resolved))} ${gray('(gcp)')} ${gray(
        `(${region})`
      )}`
    )
  )

  const buildStart = Date.now()
  const stopBuildSpinner = wait('Building and bundling your app…')
  const zipFile = await build(resolved, desc, { overrides })
  stopBuildSpinner()

  if (zipFile.length > 100 * 1024 * 1024) {
    console.error(error('The build exceeds the 100mb GCP Functions limit'))
    return 1
  }

  console.log(
    ok(
      `Build generated a ${bold(bytes(zipFile.length))} zip ${gray(
        `[${ms(Date.now() - buildStart)}]`
      )}`
    )
  )

  const deploymentId = gcpConfig.functionName || 'now-' + desc.name + '-' + (await uid(10))
  const zipFileName = `${deploymentId}.zip`

  const { project } = ctx.authConfig.credentials.find(p => p.provider === 'gcp')

  const resourcesStart = Date.now()

  debug('checking gcp function check')
  const fnCheckExistsRes = () =>
    fetch(
      `https://cloudfunctions.googleapis.com/v1beta2/projects/${project.id}/locations/${region}/functions/${deploymentId}`,
      {
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${token}`,
        },
      }
    )
  const checkExistsRes = await fnCheckExistsRes()
  const fnExists = checkExistsRes.status !== 404

  const stopResourcesSpinner = wait(`${fnExists ? 'Updating' : 'Creating'} API resources`)

  if (!ctx.config.gcp) ctx.config.gcp = {}
  if (!ctx.config.gcp.bucketName) {
    ctx.config.gcp.bucketName = generateBucketName()
    writeToConfigFile(ctx.config)
  }

  const { bucketName } = ctx.config.gcp

  debug('creating gcp storage bucket')
  const bucketRes = await fetch(
    `https://www.googleapis.com/storage/v1/b?project=${project.id}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`
      },
      body: JSON.stringify({
        name: bucketName
      })
    }
  )

  if (
    bucketRes.status !== 200 &&
    bucketRes.status !== 409 /* already exists */
  ) {
    console.error(
      error(
        `Error while creating GCP Storage bucket: ${await bucketRes.text()}`
      )
    )
    return 1
  }

  debug('creating gcp storage file')
  const fileRes = await fetch(
    `https://www.googleapis.com/upload/storage/v1/b/${encodeURIComponent(
      bucketName
    )}/o?uploadType=media&name=${encodeURIComponent(
      zipFileName
    )}&project=${encodeURIComponent(project.id)}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/zip',
        'Content-Length': zipFile.length,
        Authorization: `Bearer ${token}`
      },
      body: zipFile
    }
  )

  try {
    await assertSuccessfulResponse(fileRes)
  } catch (err) {
    console.error(error(err.message))
    return 1
  }

  debug('creating gcp function create')
  const fnCreateRes = await fetch(
    `https://cloudfunctions.googleapis.com/v1beta2/projects/${project.id}/locations/${region}/functions${fnExists ? `/${deploymentId}` : ''}`,
    {
      method: fnExists ? 'PUT' : 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`
      },
      body: JSON.stringify({
        name: `projects/${project.id}/locations/${region}/functions/${deploymentId}`,
        timeout: gcpConfig.timeout || '15s',
        availableMemoryMb: gcpConfig.memory || 512,
        sourceArchiveUrl: `gs://${encodeURIComponent(
          bucketName
        )}/${zipFileName}`,
        entryPoint: 'handler',
        httpsTrigger: {
          url: null
        }
      })
    }
  )

  if (403 === fnCreateRes.status) {
    const url = `https://console.cloud.google.com/apis/api/cloudfunctions.googleapis.com/overview?project=${project.id}`
    console.error(
      error(
        'GCP Permission Denied error. Make sure the "Google Cloud Functions API" ' +
          `is enabled in the API Manager\n  ${bold('API Manager URL')}: ${link(
            url
          )}`
      )
    )
    return 1
  }

  try {
    await assertSuccessfulResponse(fnCreateRes)
  } catch (err) {
    console.error(error(err.message))
    return 1
  }

  let retriesLeft = 10
  let status
  let url = ''

  do {
    if (status === 'FAILED') {
      console.error(
        error('API resources failed to deploy.')
      )
      return 1
    } else if (!--retriesLeft) {
      console.error(
        error('Could not determine status of the deployment: ' + String(url))
      )
      return 1
    } else {
      await sleep(5000)
    }

    const checkExistsRes = await fnCheckExistsRes()
    try {
      await assertSuccessfulResponse(checkExistsRes)
    } catch (err) {
      console.error(error(err.message))
      return 1
    }

    ;({ status, httpsTrigger: { url } } = await checkExistsRes.json())
  } while (status !== 'READY')

  stopResourcesSpinner()
  console.log(
    ok(
      `API resources ${fnExists ? 'updated' : 'created'} (id: ${param(deploymentId)}) ${gray(
        `[${ms(Date.now() - resourcesStart)}]`
      )}`
    )
  )

  const copied = copyToClipboard(url, ctx.config.copyToClipboard)

  console.log(
    success(
      `${link(url)} ${copied ? gray('(in clipboard)') : ''} ${gray(
        `[${ms(Date.now() - start)}]`
      )}`
    )
  )

  return 0
}