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 }
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 }
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 }