UNPKG

@twilio-labs/serverless-api

Version:
556 lines (555 loc) 25.5 kB
"use strict"; /** @module @twilio-labs/serverless-api */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TwilioServerlessApiClient = exports.createGotClient = void 0; const debug_1 = __importDefault(require("debug")); const events_1 = __importDefault(require("events")); const hpagent_1 = require("hpagent"); const p_limit_1 = __importDefault(require("p-limit")); const util_1 = require("util"); const assets_1 = require("./api/assets"); const builds_1 = require("./api/builds"); const dependencies_1 = require("./api/dependencies"); const environments_1 = require("./api/environments"); const functions_1 = require("./api/functions"); const logs_1 = require("./api/logs"); const services_1 = require("./api/services"); const api_client_1 = require("./api/utils/api-client"); const http_config_1 = require("./api/utils/http_config"); const variables_1 = require("./api/variables"); const got_1 = __importDefault(require("./got")); const logs_2 = require("./streams/logs"); const consts_1 = require("./types/consts"); const error_1 = require("./utils/error"); const fs_1 = require("./utils/fs"); const user_agent_1 = __importDefault(require("./utils/user-agent")); const log = (0, debug_1.default)('twilio-serverless-api:client'); function createGotClient(config) { let username, password; if ('accountSid' in config) { username = config.accountSid; password = config.authToken; (0, util_1.deprecate)(() => { }, '`accountSid` and `authToken` client config is deprecated, please use `username` and `password` instead.')(); } else { username = config.username; password = config.password; } let client = got_1.default.extend({ prefixUrl: (0, api_client_1.getApiUrl)(config), responseType: 'json', username: username, password: password, headers: { 'User-Agent': (0, user_agent_1.default)(config.userAgentExtensions), }, }); if (process.env.HTTP_PROXY) { /* * If environment variable HTTP_PROXY is set, * add a proxy agent to the got client. */ client = client.extend({ agent: { https: new hpagent_1.HttpsProxyAgent({ proxy: process.env.HTTP_PROXY, }), }, }); } return client; } exports.createGotClient = createGotClient; class TwilioServerlessApiClient extends events_1.default.EventEmitter { constructor(config) { debug_1.default.enable(process.env.DEBUG || ''); super(); this.config = config; this.client = createGotClient(config); this.limit = (0, p_limit_1.default)(config.concurrency || http_config_1.CONCURRENCY); } /** * Returns the internally used GotClient instance used to make API requests * @returns {GotClient} A client instance with set-up credentials */ getClient() { debug_1.default.enable(process.env.DEBUG || ''); return this.client; } /** * Sets a set of environment variables for a given Twilio Serverless environment * If append is false it will remove all existing environment variables. * * @param {SetEnvironmentVariablesConfig} config * @returns {Promise<SetEnvironmentVariablesResult>} * @memberof TwilioServerlessApiClient */ async setEnvironmentVariables(config) { let serviceSid = config.serviceSid; if (typeof serviceSid === 'undefined' && typeof config.serviceName !== 'undefined') { serviceSid = await (0, services_1.findServiceSid)(config.serviceName, this); } if (typeof serviceSid === 'undefined') { throw new Error('Missing service SID argument'); } let environmentSid; if (!(0, environments_1.isEnvironmentSid)(config.environment)) { const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(config.environment, serviceSid, this); environmentSid = environmentResource.sid; } else { environmentSid = config.environment; } const removeRedundantVariables = !config.append; await (0, variables_1.setEnvironmentVariables)(config.env, environmentSid, serviceSid, this, removeRedundantVariables); return { serviceSid, environmentSid }; } /** * Retrieves a list of environment variables for a given Twilio Serverless environment. * If config.getValues is false (default) the values will be all set to undefined. * * @param {GetEnvironmentVariablesConfig} config * @returns {Promise<GetEnvironmentVariablesResult>} * @memberof TwilioServerlessApiClient */ async getEnvironmentVariables(config) { let serviceSid = config.serviceSid; if (typeof serviceSid === 'undefined' && typeof config.serviceName !== 'undefined') { serviceSid = await (0, services_1.findServiceSid)(config.serviceName, this); } if (typeof serviceSid === 'undefined') { throw new Error('Missing service SID argument'); } let environmentSid; if (!(0, environments_1.isEnvironmentSid)(config.environment)) { const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(config.environment, serviceSid, this); environmentSid = environmentResource.sid; } else { environmentSid = config.environment; } const result = await (0, variables_1.listVariablesForEnvironment)(environmentSid, serviceSid, this); let variables = result.map((resource) => { return { key: resource.key, value: config.getValues ? resource.value : undefined, }; }); if (config.keys.length > 0) { variables = variables.filter((entry) => { return config.keys.includes(entry.key); }); } return { serviceSid, environmentSid, variables }; } /** * Deletes a list of environment variables (by key) for a given Twilio Serverless environment. * * @param {RemoveEnvironmentVariablesConfig} config * @returns {Promise<RemoveEnvironmentVariablesResult>} * @memberof TwilioServerlessApiClient */ async removeEnvironmentVariables(config) { let serviceSid = config.serviceSid; if (typeof serviceSid === 'undefined' && typeof config.serviceName !== 'undefined') { serviceSid = await (0, services_1.findServiceSid)(config.serviceName, this); } if (typeof serviceSid === 'undefined') { throw new Error('Missing service SID argument'); } let environmentSid; if (!(0, environments_1.isEnvironmentSid)(config.environment)) { const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(config.environment, serviceSid, this); environmentSid = environmentResource.sid; } else { environmentSid = config.environment; } await (0, variables_1.removeEnvironmentVariables)(config.keys, environmentSid, serviceSid, this); return { serviceSid, environmentSid }; } /** * Returns an object containing lists of services, environments, variables * functions or assets, depending on which have beeen requested in `listConfig` * @param {ListConfig} listConfig Specifies info around which things should be listed * @returns Promise<ListResult> Object containing the different lists. */ async list(listConfig) { try { let { types, serviceSid, serviceName: serviceName, environment: environmentSid, } = listConfig; if (types === 'services' || (types.length === 1 && types[0] === 'services')) { const services = await (0, services_1.listServices)(this); return { services }; } if (typeof serviceSid === 'undefined' && typeof serviceName !== 'undefined') { serviceSid = await (0, services_1.findServiceSid)(serviceName, this); } if (typeof serviceSid === 'undefined') { throw new Error('Missing service SID argument'); } const result = {}; let currentBuildSidForEnv; let currentBuild; for (const type of types) { try { if (type === 'environments') { result.environments = await (0, environments_1.listEnvironments)(serviceSid, this); } if (type === 'builds') { result.builds = await (0, builds_1.listBuilds)(serviceSid, this); } if (typeof environmentSid === 'string') { if (!(0, environments_1.isEnvironmentSid)(environmentSid)) { const environment = await (0, environments_1.getEnvironmentFromSuffix)(environmentSid, serviceSid, this); environmentSid = environment.sid; currentBuildSidForEnv = environment.build_sid; } else if (!currentBuildSidForEnv) { const environment = await (0, environments_1.getEnvironment)(environmentSid, serviceSid, this); currentBuildSidForEnv = environment.build_sid; } if (type === 'functions' || type === 'assets') { if (!currentBuild) { currentBuild = await (0, builds_1.getBuild)(currentBuildSidForEnv, serviceSid, this); } if (type === 'functions') { result.functions = { environmentSid, entries: currentBuild.function_versions, }; } else if (type === 'assets') { result.assets = { environmentSid, entries: currentBuild.asset_versions, }; } } if (type === 'variables') { result.variables = { entries: await (0, variables_1.listVariablesForEnvironment)(environmentSid, serviceSid, this), environmentSid, }; } } } catch (err) { log(new error_1.ClientApiError(err)); } } return result; } catch (err) { (0, error_1.convertApiErrorsAndThrow)(err); } } async getLogsStream(logsConfig) { try { let { serviceSid, environment, filterByFunction } = logsConfig; if (!(0, environments_1.isEnvironmentSid)(environment)) { const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(environment, serviceSid, this); environment = environmentResource.sid; } if (filterByFunction && !(0, functions_1.isFunctionSid)(filterByFunction)) { const availableFunctions = await (0, functions_1.listFunctionResources)(serviceSid, this); const foundFunction = availableFunctions.find((fn) => fn.friendly_name === filterByFunction); if (!foundFunction) { throw new Error('Invalid Function Name or SID'); } filterByFunction = foundFunction.sid; } const logsStream = new logs_2.LogsStream(environment, serviceSid, this, logsConfig); return logsStream; } catch (err) { (0, error_1.convertApiErrorsAndThrow)(err); } } async getLogs(logsConfig) { try { let { serviceSid, environment, filterByFunction } = logsConfig; if (!(0, environments_1.isEnvironmentSid)(environment)) { const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(environment, serviceSid, this); environment = environmentResource.sid; } if (filterByFunction && !(0, functions_1.isFunctionSid)(filterByFunction)) { const availableFunctions = await (0, functions_1.listFunctionResources)(serviceSid, this); const foundFunction = availableFunctions.find((fn) => fn.friendly_name === filterByFunction); if (!foundFunction) { throw new Error('Invalid Function Name or SID'); } filterByFunction = foundFunction.sid; } return (0, logs_1.listOnePageLogResources)(environment, serviceSid, this, { pageSize: 50, functionSid: filterByFunction, }); } catch (err) { (0, error_1.convertApiErrorsAndThrow)(err); } } /** * "Activates" a build by taking a specified build SID or a "source environment" * and activating the same build in the specified `environment`. * * Can optionally create the new environment when called with `activateConfig.createEnvironment` * @param {ActivateConfig} activateConfig Config to specify which build to activate in which environment * @returns Promise<ActivateResult> Object containing meta information around deployment */ async activateBuild(activateConfig) { try { let { buildSid, targetEnvironment, serviceSid, sourceEnvironment, env } = activateConfig; if (!buildSid && !sourceEnvironment) { const error = new Error('You need to specify either a build SID or source environment to activate'); error.name = 'activate-missing-source'; throw error; } if (!(0, environments_1.isEnvironmentSid)(targetEnvironment)) { try { const environment = await (0, environments_1.getEnvironmentFromSuffix)(targetEnvironment, serviceSid, this); targetEnvironment = environment.sid; } catch (err) { if (activateConfig.force || activateConfig.createEnvironment) { const environment = await (0, environments_1.createEnvironmentFromSuffix)(targetEnvironment, serviceSid, this); targetEnvironment = environment.sid; } else { throw err; } } } if (!buildSid && sourceEnvironment) { let currentEnv; if (!(0, environments_1.isEnvironmentSid)(sourceEnvironment)) { currentEnv = await (0, environments_1.getEnvironmentFromSuffix)(sourceEnvironment, serviceSid, this); } else { currentEnv = await (0, environments_1.getEnvironment)(sourceEnvironment, serviceSid, this); } buildSid = currentEnv.build_sid; } if (!buildSid) { throw new Error('Could not determine build SID'); } this.emit('status-update', { status: consts_1.DeployStatus.SETTING_VARIABLES, message: 'Setting environment variables', }); await (0, variables_1.setEnvironmentVariables)(env, targetEnvironment, serviceSid, this); const { domain_name } = await (0, environments_1.getEnvironment)(targetEnvironment, serviceSid, this); await (0, builds_1.activateBuild)(buildSid, targetEnvironment, serviceSid, this); return { serviceSid, buildSid, environmentSid: targetEnvironment, domain: domain_name, }; } catch (err) { (0, error_1.convertApiErrorsAndThrow)(err); } } /** * Deploys a set of functions, assets, variables and dependencies specified * in `deployConfig`. Functions & assets can either be paths to the local * filesystem or `Buffer` instances allowing you to dynamically upload * even without a file system. * * Unless a `deployConfig. serviceSid` is specified, it will try to create one. If a service * with the name `deployConfig.serviceName` already exists, it will throw * an error. You can make it use the existing service by setting `overrideExistingService` to * true. * * Updates to the deployment will be emitted as events to `status-update`. Example: * * ```js * client.on('status-update', ({ status, message }) => { * console.log('[%s]: %s', status, message); * }) * ``` * @param {DeployProjectConfig} deployConfig Config containing all details for deployment * @returns Promise<DeployResult> Object containing meta information around deployment */ async deployProject(deployConfig) { try { const config = { ...this.config, ...deployConfig, }; const { functions, assets, runtime } = config; let serviceName = config.serviceName; let serviceSid = config.serviceSid; if (!serviceSid) { this.emit('status-update', { status: consts_1.DeployStatus.CREATING_SERVICE, message: 'Creating Service', }); try { serviceSid = await (0, services_1.createService)(config.serviceName, this, config.uiEditable); } catch (err) { const alternativeServiceSid = await (0, services_1.findServiceSid)(config.serviceName, this); if (!alternativeServiceSid) { throw err; } if (config.overrideExistingService || config.force) { serviceSid = alternativeServiceSid; } else { const error = new Error(`Service with name "${config.serviceName}" already exists with SID "${alternativeServiceSid}".`); error.name = 'conflicting-servicename'; Object.defineProperty(error, 'serviceSid', { value: alternativeServiceSid, }); Object.defineProperty(error, 'serviceName', { value: config.serviceName, }); throw error; } } } else { const serviceResource = await (0, services_1.getService)(serviceSid, this); serviceName = serviceResource.unique_name; } this.emit('status-update', { status: consts_1.DeployStatus.CONFIGURING_ENVIRONMENT, message: `Configuring ${config.functionsEnv.length === 0 ? 'bare' : `"${config.functionsEnv}"`} environment`, }); const environment = await (0, environments_1.createEnvironmentIfNotExists)(config.functionsEnv, serviceSid, this); const { sid: environmentSid, domain_name: domain } = environment; // // Functions // this.emit('status-update', { status: consts_1.DeployStatus.CREATING_FUNCTIONS, message: `Creating ${functions.length} Functions`, }); const functionResources = await (0, functions_1.getOrCreateFunctionResources)(functions, serviceSid, this); this.emit('status-update', { status: consts_1.DeployStatus.UPLOADING_FUNCTIONS, message: `Uploading ${functions.length} Functions`, }); const functionVersions = await Promise.all(functionResources.map((fn) => { return (0, functions_1.uploadFunction)(fn, serviceSid, this, this.config); })); // // Assets // this.emit('status-update', { status: consts_1.DeployStatus.CREATING_ASSETS, message: `Creating ${assets.length} Assets`, }); const assetResources = await (0, assets_1.getOrCreateAssetResources)(assets, serviceSid, this); this.emit('status-update', { status: consts_1.DeployStatus.UPLOADING_ASSETS, message: `Uploading ${assets.length} Assets`, }); const assetVersions = await Promise.all(assetResources.map((asset) => { return (0, assets_1.uploadAsset)(asset, serviceSid, this, this.config); })); this.emit('status-update', { status: consts_1.DeployStatus.BUILDING, message: 'Waiting for deployment.', }); const dependencies = (0, dependencies_1.getDependencies)(config.pkgJson); const build = await (0, builds_1.triggerBuild)({ functionVersions, dependencies, assetVersions, runtime }, serviceSid, this); await (0, builds_1.waitForSuccessfulBuild)(build.sid, serviceSid, this, this); this.emit('status-update', { status: consts_1.DeployStatus.SETTING_VARIABLES, message: 'Setting environment variables', }); await (0, variables_1.setEnvironmentVariables)(config.env, environmentSid, serviceSid, this); this.emit('status-update', { status: consts_1.DeployStatus.ACTIVATING_DEPLOYMENT, message: 'Activating deployment', }); await (0, builds_1.activateBuild)(build.sid, environmentSid, serviceSid, this); this.emit('status', { status: consts_1.DeployStatus.DONE, message: 'Project successfully deployed', }); return { serviceSid, environmentSid, buildSid: build.sid, domain, functionResources, assetResources, runtime: build.runtime, serviceName, }; } catch (err) { (0, error_1.convertApiErrorsAndThrow)(err); } } /** * Deploys a local project by reading existing functions and assets * from `deployConfig.cwd` and calling `this.deployProject` with it. * * Functions have to be placed in a `functions` or `src` directory to be found. * Assets have to be placed into an `assets` or `static` directory. * * Nested folder structures will result in nested routes. * @param {DeployLocalProjectConfig} deployConfig * @returns Promise<DeployResult> Object containing meta information around deployment */ async deployLocalProject(deployConfig) { try { this.emit('status-update', { status: consts_1.DeployStatus.READING_FILESYSTEM, message: 'Gathering Functions and Assets to deploy', }); log('Deploy config %P', deployConfig); const searchConfig = {}; if (deployConfig.functionsFolderName) { searchConfig.functionsFolderNames = [deployConfig.functionsFolderName]; } if (deployConfig.assetsFolderName) { searchConfig.assetsFolderNames = [deployConfig.assetsFolderName]; } let { functions, assets } = await (0, fs_1.getListOfFunctionsAndAssets)(deployConfig.cwd, searchConfig); if (deployConfig.noFunctions) { log('Disabling functions upload by emptying functions array'); functions = []; } if (deployConfig.noAssets) { log('Disabling assets upload by emptying assets array'); assets = []; } const config = { ...this.config, ...deployConfig, functions, assets, }; return this.deployProject(config); } catch (err) { (0, error_1.convertApiErrorsAndThrow)(err); } } // general implementation async request(method, path, options = {}) { options.retry = { limit: this.config.retryLimit || http_config_1.RETRY_LIMIT, methods: ['GET', 'POST', 'DELETE'], statusCodes: [429], errorCodes: [], }; return this.limit(() => this.client[method](path, options)); } } exports.TwilioServerlessApiClient = TwilioServerlessApiClient; exports.default = TwilioServerlessApiClient;