const main = async ctx => { argv = mri(ctx.argv.slice(2), { boolean: ['help', 'debug', 'all'], alias: { help: 'h', debug: 'd' } }) argv._ = argv._.slice(1) app = argv._[0] debug = argv.debug apiUrl = ctx.apiUrl if (argv.help || app === 'help') { help() await exit(0) } const {authConfig: { credentials }, config: { sh, includeScheme }} = ctx const {token} = credentials.find(item => item.provider === 'sh') try { await list({ token, sh, includeScheme }) } catch (err) { console.error(error(`Unknown error: ${err}\n${err.stack}`)) getProcess().exit(1) } }
const main = async ctx => { argv = mri(ctx.argv.slice(2), { boolean: ['help', 'debug', 'hard', 'yes', 'safe'], alias: { help: 'h', debug: 'd', yes: 'y', safe: 's' } }) argv._ = argv._.slice(1) debug = argv.debug apiUrl = ctx.apiUrl hard = argv.hard || false skipConfirmation = argv.yes || false ids = argv._ if (argv.help || ids.length === 0 || ids[0] === 'help') { help() await exit(0) } const {authConfig: { credentials }, config: { sh }} = ctx const {token} = credentials.find(item => item.provider === 'sh') try { await remove({ token, sh }) } catch (err) { console.error(error(`Unknown error: ${err}\n${err.stack}`)) getProcess().exit(1) } }
const main = async ctx => { argv = mri(ctx.argv.slice(2), { boolean: ['help', 'debug', 'base64'], alias: { help: 'h', debug: 'd', base64: 'b' } }) argv._ = argv._.slice(1) debug = argv.debug apiUrl = ctx.apiUrl subcommand = argv._[0] if (argv.help || !subcommand) { help() await exit(0) } const {authConfig: { credentials }, config: { sh }} = ctx const {token} = credentials.find(item => item.provider === 'sh') try { await run({ token, sh }) } catch (err) { handleError(err) exit(1) } }
const main = async ctx => { argv = mri(ctx.argv.slice(2), { boolean: ['help', 'debug'], alias: { help: 'h', debug: 'd' } }) argv._ = argv._.slice(1) debug = argv.debug apiUrl = ctx.apiUrl if (argv.help || argv._[0] === 'help') { help() exit(0) } const {authConfig: { credentials }, config: { sh }} = ctx const {token} = credentials.find(item => item.provider === 'sh') try { await run({ token, sh }) } catch (err) { if (err.userError) { console.error(error(err.message)) } else { console.error(error(`Unknown error: ${err.stack}`)) } exit(1) } }
async function main(ctx) { argv = mri(ctx.argv.slice(2), mriOpts) // very ugly hack – this (now-cli's code) expects that `argv._[0]` is the path // we should fix this ASAP if (argv._[0] === 'sh') { argv._.shift() } if (argv._[0] === 'deploy') { argv._.shift() } if (argv._[0]) { // If path is relative: resolve // if path is absolute: clear up strange `/` etc path = resolve(getProcess().cwd(), argv._[0]) } else { path = getProcess().cwd() } // Options forceNew = argv.force deploymentName = argv.name sessionAffinity = argv['session-affinity'] debug = argv.debug clipboard = !argv['no-clipboard'] forwardNpm = argv['forward-npm'] followSymlinks = !argv.links wantsPublic = argv.public apiUrl = ctx.apiUrl isTTY = getProcess().stdout.isTTY quiet = !isTTY if (argv.h || argv.help) { help() await exit(0) } const { authConfig: { credentials }, config: { sh } } = ctx const { token } = credentials.find(item => item.provider === 'sh') const config = sh alwaysForwardNpm = config.forwardNpm try { return sync({ token, config }) } catch (err) { await stopDeployment(err) } }
const main = async ctx => { argv = mri(ctx.argv.slice(2), { boolean: ['help', 'debug', 'all'], alias: { help: 'h', debug: 'd' } }) argv._ = argv._.slice(1) if (argv.help || argv._[0] === 'help') { help() process.exit(0) } await whoami(ctx.config.sh) }
const main = async ctx => { argv = mri(ctx.argv.slice(2), { string: ['coupon'], boolean: ['help', 'debug', 'external', 'force'], alias: { help: 'h', coupon: 'c', debug: 'd', external: 'e', force: 'f' } }) argv._ = argv._.slice(1) debug = argv.debug apiUrl = ctx.apiUrl subcommand = argv._[0] if (argv.help || !subcommand) { help() await exit(0) } const {authConfig: { credentials }, config: { sh }} = ctx const {token} = credentials.find(item => item.provider === 'sh') try { await run({ token, sh }) } catch (err) { if (err.userError) { console.error(error(err.message)) } else { console.error(error(`Unknown error: ${err}\n${err.stack}`)) } exit(1) } }
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 login = async ctx => { const argv = mri(ctx.argv.slice(2), { boolean: ['help'], alias: { help: 'h' } }) if (argv.help) { help() await exit(0) } argv._ = argv._.slice(1) const apiUrl = ctx.apiUrl let email let emailIsValid = false let stopSpinner const possibleAddress = argv._[0] // if the last arg is not the command itself, then maybe it's an email if (possibleAddress) { if (!validateEmail(possibleAddress)) { // if it's not a valid email, let's just error console.log(error(`Invalid email: ${param(possibleAddress)}.`)) return 1 } // valid email, no need to prompt the user email = possibleAddress } else { do { try { email = await readEmail() } catch (err) { let erase = '' if (err.message.includes('Aborted')) { // no need to keep the prompt if the user `ctrl+c`ed erase = eraseLines(2) } console.log(erase + err.message) return 1 } emailIsValid = validateEmail(email) if (!emailIsValid) { // let's erase the `> Enter email [...]` // we can't use `console.log()` because it appends a `\n` // we need this check because `email-prompt` doesn't print // anything if there's no TTY process.stdout.write(eraseLines(2)) } } while (!emailIsValid) } let verificationToken let securityCode stopSpinner = wait('Sending you an email') try { const data = await getVerificationData({ apiUrl, email }) verificationToken = data.token securityCode = data.securityCode } catch (err) { stopSpinner() console.log(err.message) return 1 } stopSpinner() // Clear up `Sending email` success message process.stdout.write(eraseLines(possibleAddress ? 1 : 2)) console.log(info( `We sent an email to ${highlight(email)}. Please follow the steps provided`, ` inside it and make sure the security code matches ${highlight(securityCode)}.` )) stopSpinner = wait('Waiting for your confirmation') let token while (!token) { try { await sleep(ms('1s')) token = await verify({ apiUrl, email, verificationToken }) } catch (err) { if (/invalid json response body/.test(err.message)) { // /now/registraton is currently returning plain text in that case // we just wait for the user to click on the link } else { stopSpinner() console.log(err.message) return 1 } } } stopSpinner() console.log(ok('Email confirmed')) stopSpinner = wait('Feching your personal details') let user try { user = await getUser({ apiUrl, token }) } catch (err) { stopSpinner() console.log(err) return 1 } const index = ctx.authConfig.credentials.findIndex(c => c.provider === 'sh') const obj = { provider: 'sh', token } if (index === -1) { // wasn't logged in before ctx.authConfig.credentials.push(obj) } else { // let's just replace the existing object ctx.authConfig.credentials[index] = obj } // NOTE: this will override any existing config for `sh` ctx.config.sh = { user } writeToAuthConfigFile(ctx.authConfig) writeToConfigFile(ctx.config) stopSpinner() console.log(ok('Fetched your personal details')) console.log( ready( `Authentication token and personal details saved in ${param( hp(getNowDir()) )}` ) ) return ctx }
#!/usr/bin/env node 'use strict' const mri = require('mri') const pkg = require('./package.json') const argv = mri(process.argv.slice(2), { boolean: ['help', 'h', 'version', 'v', 'quiet', 'q'] }) if (argv.help || argv.h) { process.stdout.write(` Usage: record-vbb-delays [command] [options] Options: --db -d Path to LevelDB. Default: vbb-delays.ldb --stations -s Stations to monitor. Default: all --stations-file JSON file with stations to monitor. --interval -i In seconds. Default: 30 --quiet -q Don't show progress reports. Default: false Examples: record-vbb-delays --db my-custom.leveldb -s 900000100003,900000100001 record-vbb-delays --stations-file stations-to-monitor.json -q record-vbb-delays export-sql --db my-custom.leveldb record-vbb-delays export-ndjson --db my-custom.leveldb \n`) process.exit(0) } if (argv.version || argv.v) { process.stdout.write(`record-vbb-delays v${pkg.version}\n`)
const success = require('../../../util/output/success') const uid = require('../../../util/output/uid') const eraseLines = require('../../../util/output/erase-lines') const stamp = require('../../../util/output/stamp') const error = require('../../../util/output/error') const treatBuyError = require('../util/domains/treat-buy-error') const scaleInfo = require('./scale-info') const { DOMAIN_VERIFICATION_ERROR } = require('./errors') const isZeitWorld = require('./is-zeit-world') const resolve4 = require('./dns') const toHost = require('./to-host') const exit = require('../../../util/exit') const Now = require('./') const argv = mri(process.argv.slice(2), { boolean: ['no-clipboard'], alias: { 'no-clipboard': 'C' } }) const isTTY = process.stdout.isTTY const clipboard = !argv['no-clipboard'] const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/ module.exports = class Alias extends Now { async ls(deployment) { if (deployment) { const target = await this.findDeployment(deployment) if (!target) { const err = new Error( `Aliases not found by "${deployment}". Run ${chalk.dim( '`now alias ls`'
const main = async (argv_) => { await checkForUpdates() const argv = mri(argv_, { boolean: [ 'help', 'version' ], string: [ 'token', 'team', 'api' ], alias: { help: 'h', version: 'v', token: 't', team: 'T' } }) // the second argument to the command can be a path // (as in: `now path/`) or a subcommand / provider // (as in: `now ls` or `now aws help`) let targetOrSubcommand: ?string = argv._[2] // we want to handle version or help directly only if (!targetOrSubcommand) { if (argv.version) { console.log(require('../package').version) return 0 } } let nowDirExists try { nowDirExists = existsSync(NOW_DIR) } catch (err) { console.error( error( 'An unexpected error occurred while trying to find the ' + 'now global directory: ' + err.message ) ) return } if (!nowDirExists) { try { await mkdirp(NOW_DIR) } catch (err) { console.error(error( 'An unexpected error occurred while trying to create the ' + `now global directory "${hp(NOW_DIR)}" ` + err.message )) } } let migrated = false let configExists try { configExists = existsSync(NOW_CONFIG_PATH) } catch (err) { console.error( error( 'An unexpected error occurred while trying to find the ' + `now config file "${hp(NOW_CONFIG_PATH)}" ` + err.message ) ) return } let config if (configExists) { try { config = configFiles.readConfigFile() } catch (err) { console.error( error( 'An unexpected error occurred while trying to read the ' + `now config in "${hp(NOW_CONFIG_PATH)}" ` + err.message ) ) return } try { config = JSON.parse(config) } catch (err) { console.error( error( `An error occurred while trying to parse "${hp(NOW_CONFIG_PATH)}": ` + err.message ) ) return } } else { const results = await getDefaultCfg() config = results.config migrated = results.migrated try { configFiles.writeToConfigFile(config) } catch (err) { console.error( error( 'An unexpected error occurred while trying to write the ' + `default now config to "${hp(NOW_CONFIG_PATH)}" ` + err.message ) ) return } } let authConfigExists try { authConfigExists = existsSync(NOW_AUTH_CONFIG_PATH) } catch (err) { console.error( error( 'An unexpected error occurred while trying to find the ' + `now auth file "${hp(NOW_AUTH_CONFIG_PATH)}" ` + err.message ) ) return } let authConfig = null if (authConfigExists) { try { authConfig = configFiles.readAuthConfigFile() } catch (err) { console.error( error( 'An unexpected error occurred while trying to read the ' + `now auth config in "${hp(NOW_AUTH_CONFIG_PATH)}" ` + err.message ) ) return } try { authConfig = JSON.parse(authConfig) if (!Array.isArray(authConfig.credentials)) { console.error( error( `The content of "${hp(NOW_AUTH_CONFIG_PATH)}" is invalid. ` + 'No `credentials` list found inside' ) ) return } for (const [i, { provider }] of authConfig.credentials.entries()) { if (null == provider) { console.error( error( `Invalid credential found in "${hp(NOW_AUTH_CONFIG_PATH)}". ` + `Missing \`provider\` key in entry with index ${i}` ) ) return } if (!(provider in providers)) { console.error( error( `Invalid credential found in "${hp(NOW_AUTH_CONFIG_PATH)}". ` + `Unknown provider "${provider}"` ) ) return } } } catch (err) { console.error( error( `An error occurred while trying to parse "${hp( NOW_AUTH_CONFIG_PATH )}": ` + err.message ) ) return } } else { const results = await getDefaultAuthCfg() authConfig = results.config migrated = results.migrated try { configFiles.writeToAuthConfigFile(authConfig) } catch (err) { console.error( error( 'An unexpected error occurred while trying to write the ' + `default now config to "${hp(NOW_CONFIG_PATH)}" ` + err.message ) ) return } } // Let the user know we migrated the config if (migrated) { const directory = param(hp(NOW_DIR)) console.log(info(`Your credentials and configuration were migrated to ${directory}`)) } // the context object to supply to the providers or the commands const ctx: Object = { config, authConfig, argv: argv_ } if (targetOrSubcommand === 'config') { const _config = require('./config') const subcommand = _config.subcommands.has(argv._[3]) ? argv._[3] : 'help' debug(`executing config %s`, subcommand) try { return _config[subcommand](ctx) } catch (err) { console.error( error( `An unexpected error occurred in config ${subcommand}: ${err.stack}` ) ) return } } let suppliedProvider = null // if the target is something like `aws` if (targetOrSubcommand && targetOrSubcommand in providers) { debug('user supplied a known provider') const targetPath = join(process.cwd(), targetOrSubcommand) const targetPathExists = existsSync(targetPath) if (targetPathExists) { console.error( error( `The supplied argument ${param(targetOrSubcommand)} is ambiguous. ` + 'Both a directory and a provider are known' ) ) return } suppliedProvider = targetOrSubcommand targetOrSubcommand = argv._[3] } // $FlowFixMe let { defaultProvider = null }: { defaultProvider: ?string } = config if (null === suppliedProvider) { if (null === defaultProvider) { debug(`falling back to default now provider 'sh'`) defaultProvider = 'sh' } else { debug('using provider supplied by user', defaultProvider) if (!(defaultProvider in providers)) { console.error( error( `The \`defaultProvider\` "${defaultProvider}" supplied in ` + `"${NOW_CONFIG_PATH}" is not a valid provider` ) ) return } } } const providerName = suppliedProvider || defaultProvider const provider: Object = providers[providerName] let subcommand // we check if we are deploying something if (targetOrSubcommand) { const targetPath = join(process.cwd(), targetOrSubcommand) const targetPathExists = existsSync(targetPath) const subcommandExists = GLOBAL_COMMANDS.has(targetOrSubcommand) || provider.subcommands.has(targetOrSubcommand) if (targetPathExists && subcommandExists) { console.error( error( `The supplied argument ${param(targetOrSubcommand)} is ambiguous. ` + 'Both a directory and a subcommand are known' ) ) return } if (subcommandExists) { debug('user supplied known subcommand', targetOrSubcommand) subcommand = targetOrSubcommand } else { debug('user supplied a possible target for deployment') // our default command is deployment // at this point we're subcommand = 'deploy' } } else { debug('user supplied no target, defaulting to deploy') subcommand = 'deploy' } if (subcommand === 'help') { subcommand = argv._[3] || 'deploy' ctx.argv.push('-h') } const { sh } = ctx.config ctx.apiUrl = 'https://api.zeit.co' if (argv.api && typeof argv.api === 'string') { ctx.apiUrl = argv.api } else if (sh && sh.api) { ctx.apiUrl = sh.api } // $FlowFixMe const { isTTY } = process.stdout // If no credentials are set at all, prompt for // login to the .sh provider if ( !authConfig.credentials.length && !ctx.argv.includes('-h') && !ctx.argv.includes('--help') && !argv.token && subcommand !== 'login' ) { if (isTTY) { console.log(info(`No existing credentials found. Please log in:`)) subcommand = 'login' ctx.argv[2] = 'login' // Ensure that sub commands lead to login as well, if // no credentials are defined ctx.argv = ctx.argv.splice(0, 3) } else { console.error(error({ message: 'No existing credentials found. Please ' + `${param('now login')} to log in or pass ${param('--token')}`, slug: 'no-credentials-found' })) await exit(1) } } if (typeof argv.token === 'string' && subcommand === 'switch') { console.error(error({ message: `This command doesn't work with ${param('--token')}. Please use ${param('--team')}.`, slug: 'no-token-allowed' })) await exit(1) } if (typeof argv.token === 'string') { const {token} = argv if (token.length === 0) { console.error(error({ message: `You defined ${param('--token')}, but it's missing a value`, slug: 'missing-token-value' })) await exit(1) } const obj = { provider: 'sh', token } const credentialsIndex = ctx.authConfig.credentials.findIndex( cred => cred.provider === 'sh' ) if (credentialsIndex === -1) { ctx.authConfig.credentials.push(obj) } else { ctx.authConfig.credentials[credentialsIndex] = obj } if (isTTY) { console.log(info('Caching account information')) } const user = await getUser({ apiUrl: ctx.apiUrl, token }) ctx.config.sh = Object.assign(ctx.config.sh || {}, { user }) } if (typeof argv.team === 'string' && subcommand !== 'login') { const { team } = argv const { sh } = ctx.config if (team.length === 0) { console.error(error({ message: `You defined ${param('--team')}, but it's missing a value`, slug: 'missing-team-value' })) await exit(1) } const cachedUser = sh && sh.user && sh.user.username === team if (cachedUser) { delete ctx.config.sh.currentTeam } const cachedTeam = sh && sh.currentTeam && sh.currentTeam.slug === team // Only download team data if not cached if (!cachedTeam && !cachedUser) { if (isTTY) { console.log(info('Caching team information')) } const { token } = ctx.authConfig.credentials.find(item => item.provider === 'sh') const headers = { Authorization: `Bearer ${token}` } const url = `https://api.zeit.co/teams/?slug=${team}` let body try { const res = await fetch(url, { headers }) if (res.status === 403) { console.error(error({ message: `You don't have access to the specified team`, slug: 'team-not-accessible' })) await exit(1) } body = await res.json() } catch (err) { console.error(error('Not able to load teams')) await exit(1) } if (!body || body.error) { console.error(error({ message: 'The specified team doesn\'t exist', slug: 'team-not-existent' })) await exit(1) } // $FlowFixMe delete body.creator_id // $FlowFixMe delete body.created ctx.config.sh.currentTeam = body } } try { await provider[subcommand](ctx) } catch (err) { console.error( error( `An unexpected error occurred in provider ${subcommand}: ${err.stack}` ) ) } if (providerName === 'gcp') { process.exit() } }
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 }