diff --git a/packages/plugin-serverless/package.json b/packages/plugin-serverless/package.json index 33505cd7..ebc02891 100644 --- a/packages/plugin-serverless/package.json +++ b/packages/plugin-serverless/package.json @@ -69,6 +69,9 @@ }, "serverless:deploy": { "description": "deploys your local serverless project" + }, + "serverless:env": { + "description": "retrieve and modify the environment variables for your deployment" } } }, diff --git a/packages/plugin-serverless/src/TwilioRunCommand.js b/packages/plugin-serverless/src/TwilioRunCommand.js new file mode 100644 index 00000000..058f324d --- /dev/null +++ b/packages/plugin-serverless/src/TwilioRunCommand.js @@ -0,0 +1,49 @@ +const { + convertYargsOptionsToOclifFlags, + normalizeFlags, + createExternalCliOptions, + getRegionAndEdge, +} = require('./utils'); + +const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; + +function createTwilioRunCommand(name, path, inheritedFlags = []) { + const { handler, cliInfo, describe } = require(path); + const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options); + + const commandClass = class extends TwilioClientCommand { + async run() { + await super.run(); + + const flags = normalizeFlags(this.flags, aliasMap, process.argv); + + const externalOptions = createExternalCliOptions( + flags, + this.twilioClient + ); + + const { edge, region } = getRegionAndEdge(flags, this); + flags.region = region; + flags.edge = edge; + + const opts = Object.assign({}, flags, this.args); + + return handler(opts, externalOptions); + } + }; + + const inheritedFlagObject = inheritedFlags.reduce((current, flag) => { + return { + ...current, + [flag]: TwilioClientCommand.flags[flag], + }; + }, {}); + + Object.defineProperty(commandClass, 'name', { value: name }); + commandClass.description = describe; + commandClass.flags = Object.assign(flags, inheritedFlagObject); + + return commandClass; +} + +module.exports = { createTwilioRunCommand }; diff --git a/packages/plugin-serverless/src/commands/serverless/env/get.js b/packages/plugin-serverless/src/commands/serverless/env/get.js new file mode 100644 index 00000000..16f86277 --- /dev/null +++ b/packages/plugin-serverless/src/commands/serverless/env/get.js @@ -0,0 +1,7 @@ +const { createTwilioRunCommand } = require('../../../TwilioRunCommand'); + +module.exports = createTwilioRunCommand( + 'EnvGet', + 'twilio-run/dist/commands/env/env-get', + ['profile'] +); diff --git a/packages/plugin-serverless/src/commands/serverless/env/import.js b/packages/plugin-serverless/src/commands/serverless/env/import.js new file mode 100644 index 00000000..f194e99a --- /dev/null +++ b/packages/plugin-serverless/src/commands/serverless/env/import.js @@ -0,0 +1,7 @@ +const { createTwilioRunCommand } = require('../../../TwilioRunCommand'); + +module.exports = createTwilioRunCommand( + 'EnvImport', + 'twilio-run/dist/commands/env/env-import', + ['profile'] +); diff --git a/packages/plugin-serverless/src/commands/serverless/env/list.js b/packages/plugin-serverless/src/commands/serverless/env/list.js new file mode 100644 index 00000000..444a48c4 --- /dev/null +++ b/packages/plugin-serverless/src/commands/serverless/env/list.js @@ -0,0 +1,7 @@ +const { createTwilioRunCommand } = require('../../../TwilioRunCommand'); + +module.exports = createTwilioRunCommand( + 'EnvList', + 'twilio-run/dist/commands/env/env-list', + ['profile'] +); diff --git a/packages/plugin-serverless/src/commands/serverless/env/set.js b/packages/plugin-serverless/src/commands/serverless/env/set.js new file mode 100644 index 00000000..25c178f4 --- /dev/null +++ b/packages/plugin-serverless/src/commands/serverless/env/set.js @@ -0,0 +1,7 @@ +const { createTwilioRunCommand } = require('../../../TwilioRunCommand'); + +module.exports = createTwilioRunCommand( + 'EnvSet', + 'twilio-run/dist/commands/env/env-set', + ['profile'] +); diff --git a/packages/plugin-serverless/src/commands/serverless/env/unset.js b/packages/plugin-serverless/src/commands/serverless/env/unset.js new file mode 100644 index 00000000..730ce8cd --- /dev/null +++ b/packages/plugin-serverless/src/commands/serverless/env/unset.js @@ -0,0 +1,7 @@ +const { createTwilioRunCommand } = require('../../../TwilioRunCommand'); + +module.exports = createTwilioRunCommand( + 'EnvUnset', + 'twilio-run/dist/commands/env/env-unset', + ['profile'] +); diff --git a/packages/plugin-serverless/tests/TwilioRunCommand.test.js b/packages/plugin-serverless/tests/TwilioRunCommand.test.js new file mode 100644 index 00000000..a9628610 --- /dev/null +++ b/packages/plugin-serverless/tests/TwilioRunCommand.test.js @@ -0,0 +1,85 @@ +const { createTwilioRunCommand } = require('../src/TwilioRunCommand'); + +jest.mock( + 'twilio-run/test-command', + () => { + return { + handler: jest.fn(), + describe: 'Some test description', + cliInfo: { + options: { + region: { + type: 'string', + hidden: true, + describe: 'Twilio API Region', + }, + edge: { + type: 'string', + hidden: true, + describe: 'Twilio API Region', + }, + username: { + type: 'string', + alias: 'u', + describe: + 'A specific API key or account SID to be used for deployment. Uses fields in .env otherwise', + }, + password: { + type: 'string', + describe: + 'A specific API secret or auth token for deployment. Uses fields from .env otherwise', + }, + 'load-system-env': { + default: false, + type: 'boolean', + describe: + 'Uses system environment variables as fallback for variables specified in your .env file. Needs to be used with --env explicitly specified.', + }, + }, + }, + }; + }, + { virtual: true } +); + +const command = require('twilio-run/test-command'); +const { convertYargsOptionsToOclifFlags } = require('../src/utils'); +const TwilioClientCommand = require('@twilio/cli-core/src/base-commands/twilio-client-command'); + +describe('createTwilioRunCommand', () => { + test('should create a new class', () => { + const ResultCommand = createTwilioRunCommand( + 'TestCommand', + 'twilio-run/test-command' + ); + expect(ResultCommand.name).toBe('TestCommand'); + expect(ResultCommand.description).toBe(command.describe); + expect(ResultCommand.flags.toString()).toEqual( + convertYargsOptionsToOclifFlags(command.cliInfo.options).flags.toString() + ); + }); + + test('should add base properties as defined', () => { + const ResultCommand = createTwilioRunCommand( + 'TestCommand', + 'twilio-run/test-command', + ['profile'] + ); + expect(ResultCommand.name).toBe('TestCommand'); + expect(ResultCommand.description).toBe(command.describe); + expect(ResultCommand.flags.profile.toString()).toEqual( + TwilioClientCommand.flags.profile.toString() + ); + }); + + // takes too long in some runs. We should find a faster way. + // test('should call the handler', async () => { + // const ResultCommand = createTwilioRunCommand( + // 'TestCommand', + // 'twilio-run/test-command' + // ); + + // await ResultCommand.run(['--region', 'dev']); + // expect(command.handler).toHaveBeenCalled(); + // }); +}); diff --git a/packages/serverless-api/src/api/utils/type-checks.ts b/packages/serverless-api/src/api/utils/type-checks.ts new file mode 100644 index 00000000..02ba38ef --- /dev/null +++ b/packages/serverless-api/src/api/utils/type-checks.ts @@ -0,0 +1,15 @@ +import { Sid } from '../../types'; + +const SidRegEx = /^[A-Z]{2}[a-f0-9]{32}$/; + +export function isSid(value: unknown): value is Sid { + if (typeof value !== 'string') { + return false; + } + + if (value.length !== 34) { + return false; + } + + return SidRegEx.test(value); +} diff --git a/packages/serverless-api/src/api/variables.ts b/packages/serverless-api/src/api/variables.ts index 50e34397..e5ea59da 100644 --- a/packages/serverless-api/src/api/variables.ts +++ b/packages/serverless-api/src/api/variables.ts @@ -1,15 +1,17 @@ /** @module @twilio-labs/serverless-api/dist/api */ import debug from 'debug'; +import { TwilioServerlessApiClient } from '../client'; import { EnvironmentVariables, + Sid, Variable, VariableList, VariableResource, } from '../types'; -import { TwilioServerlessApiClient } from '../client'; -import { getPaginatedResource } from './utils/pagination'; import { ClientApiError } from '../utils/error'; +import { getPaginatedResource } from './utils/pagination'; +import { isSid } from './utils/type-checks'; const log = debug('twilio-serverless-api:variables'); @@ -137,13 +139,15 @@ function convertToVariableArray(env: EnvironmentVariables): Variable[] { * @param {string} environmentSid the environment the varibales should be set for * @param {string} serviceSid the service the environment belongs to * @param {TwilioServerlessApiClient} client API client + * @param {boolean} [removeRedundantOnes=false] whether to remove variables that are not passed but are currently set * @returns {Promise} */ export async function setEnvironmentVariables( envVariables: EnvironmentVariables, environmentSid: string, serviceSid: string, - client: TwilioServerlessApiClient + client: TwilioServerlessApiClient, + removeRedundantOnes: boolean = false ): Promise { const existingVariables = await listVariablesForEnvironment( environmentSid, @@ -181,4 +185,90 @@ export async function setEnvironmentVariables( }); await Promise.all(variableResources); + + if (removeRedundantOnes) { + const removeVariablePromises = existingVariables.map(async (variable) => { + if (typeof envVariables[variable.key] === 'undefined') { + return deleteEnvironmentVariable( + variable.sid, + environmentSid, + serviceSid, + client + ); + } + }); + await Promise.all(removeVariablePromises); + } +} + +/** + * Deletes a given variable from a given environment + * + * @export + * @param {string} variableSid the SID of the variable to delete + * @param {string} environmentSid the environment the variable belongs to + * @param {string} serviceSid the service the environment belongs to + * @param {TwilioServerlessApiClient} client API client instance + * @returns {Promise} + */ +export async function deleteEnvironmentVariable( + variableSid: string, + environmentSid: string, + serviceSid: string, + client: TwilioServerlessApiClient +): Promise { + try { + const resp = await client.request( + 'delete', + `Services/${serviceSid}/Environments/${environmentSid}/Variables/${variableSid}` + ); + return true; + } catch (err) { + log('%O', new ClientApiError(err)); + throw err; + } +} + +/** + * Deletes all variables matching the passed keys from an environment + * + * @export + * @param {string[]} keys the keys of the variables to delete + * @param {string} environmentSid the environment the variables belong to + * @param {string} serviceSid the service the environment belongs to + * @param {TwilioServerlessApiClient} client API client instance + * @returns {Promise} + */ +export async function removeEnvironmentVariables( + keys: string[], + environmentSid: string, + serviceSid: string, + client: TwilioServerlessApiClient +): Promise { + const existingVariables = await listVariablesForEnvironment( + environmentSid, + serviceSid, + client + ); + + const variableSidMap = new Map(); + existingVariables.forEach((variableResource) => { + variableSidMap.set(variableResource.key, variableResource.sid); + }); + + const requests: Promise[] = keys.map((key) => { + const variableSid = variableSidMap.get(key); + if (isSid(variableSid)) { + return deleteEnvironmentVariable( + variableSid, + environmentSid, + serviceSid, + client + ); + } + return Promise.resolve(true); + }); + + await Promise.all(requests); + return true; } diff --git a/packages/serverless-api/src/client.ts b/packages/serverless-api/src/client.ts index 2cffc3f5..6a32d383 100644 --- a/packages/serverless-api/src/client.ts +++ b/packages/serverless-api/src/client.ts @@ -46,6 +46,7 @@ import { getApiUrl } from './api/utils/api-client'; import { CONCURRENCY, RETRY_LIMIT } from './api/utils/http_config'; import { listVariablesForEnvironment, + removeEnvironmentVariables, setEnvironmentVariables, } from './api/variables'; import got from './got'; @@ -63,8 +64,18 @@ import { ListResult, LogApiResource, LogsConfig, + Sid, } from './types'; import { DeployStatus } from './types/consts'; +import { + GetEnvironmentVariablesConfig, + GetEnvironmentVariablesResult, + KeyValue, + RemoveEnvironmentVariablesConfig, + RemoveEnvironmentVariablesResult, + SetEnvironmentVariablesConfig, + SetEnvironmentVariablesResult, +} from './types/env'; import { ClientApiError, convertApiErrorsAndThrow } from './utils/error'; import { getListOfFunctionsAndAssets, SearchConfig } from './utils/fs'; @@ -150,6 +161,157 @@ export class TwilioServerlessApiClient extends events.EventEmitter { 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} + * @memberof TwilioServerlessApiClient + */ + async setEnvironmentVariables( + config: SetEnvironmentVariablesConfig + ): Promise { + let serviceSid: Sid | undefined = config.serviceSid; + if ( + typeof serviceSid === 'undefined' && + typeof config.serviceName !== 'undefined' + ) { + serviceSid = await findServiceSid(config.serviceName, this); + } + + if (typeof serviceSid === 'undefined') { + throw new Error('Missing service SID argument'); + } + + let environmentSid; + + if (!isEnvironmentSid(config.environment)) { + const environmentResource = await getEnvironmentFromSuffix( + config.environment, + serviceSid, + this + ); + environmentSid = environmentResource.sid; + } else { + environmentSid = config.environment; + } + + const removeRedundantVariables = !config.append; + await 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} + * @memberof TwilioServerlessApiClient + */ + async getEnvironmentVariables( + config: GetEnvironmentVariablesConfig + ): Promise { + let serviceSid: Sid | undefined = config.serviceSid; + if ( + typeof serviceSid === 'undefined' && + typeof config.serviceName !== 'undefined' + ) { + serviceSid = await findServiceSid(config.serviceName, this); + } + + if (typeof serviceSid === 'undefined') { + throw new Error('Missing service SID argument'); + } + + let environmentSid; + + if (!isEnvironmentSid(config.environment)) { + const environmentResource = await getEnvironmentFromSuffix( + config.environment, + serviceSid, + this + ); + environmentSid = environmentResource.sid; + } else { + environmentSid = config.environment; + } + + const result = await listVariablesForEnvironment( + environmentSid, + serviceSid, + this + ); + + let variables: KeyValue[] = 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} + * @memberof TwilioServerlessApiClient + */ + async removeEnvironmentVariables( + config: RemoveEnvironmentVariablesConfig + ): Promise { + let serviceSid: Sid | undefined = config.serviceSid; + if ( + typeof serviceSid === 'undefined' && + typeof config.serviceName !== 'undefined' + ) { + serviceSid = await findServiceSid(config.serviceName, this); + } + + if (typeof serviceSid === 'undefined') { + throw new Error('Missing service SID argument'); + } + + let environmentSid; + + if (!isEnvironmentSid(config.environment)) { + const environmentResource = await getEnvironmentFromSuffix( + config.environment, + serviceSid, + this + ); + environmentSid = environmentResource.sid; + } else { + environmentSid = config.environment; + } + + await 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` diff --git a/packages/serverless-api/src/types/env.ts b/packages/serverless-api/src/types/env.ts new file mode 100644 index 00000000..b298b8be --- /dev/null +++ b/packages/serverless-api/src/types/env.ts @@ -0,0 +1,47 @@ +import { ClientConfig } from './client'; +import { EnvironmentVariables } from './generic'; +import { Sid } from './serverless-api'; + +export type KeyValue = { + key: string; + value?: string; +}; + +export type GetEnvironmentVariablesConfig = ClientConfig & { + serviceSid?: string; + serviceName?: string; + environment: string | Sid; + keys: string[]; + getValues: boolean; +}; + +export type SetEnvironmentVariablesConfig = ClientConfig & { + serviceSid?: string; + serviceName?: string; + environment: string | Sid; + env: EnvironmentVariables; + append: boolean; +}; + +export type RemoveEnvironmentVariablesConfig = ClientConfig & { + serviceSid?: string; + serviceName?: string; + environment: string | Sid; + keys: string[]; +}; + +export type GetEnvironmentVariablesResult = { + serviceSid: Sid; + environmentSid: Sid; + variables: KeyValue[]; +}; + +export type SetEnvironmentVariablesResult = { + serviceSid: Sid; + environmentSid: Sid; +}; + +export type RemoveEnvironmentVariablesResult = { + serviceSid: Sid; + environmentSid: Sid; +}; diff --git a/packages/serverless-api/src/types/index.ts b/packages/serverless-api/src/types/index.ts index 2096812b..d31d3cbb 100644 --- a/packages/serverless-api/src/types/index.ts +++ b/packages/serverless-api/src/types/index.ts @@ -3,7 +3,8 @@ export * from './activate'; export * from './client'; export * from './deploy'; +export * from './env'; export * from './generic'; export * from './list'; -export * from './serverless-api'; export * from './logs'; +export * from './serverless-api'; diff --git a/packages/twilio-run/src/cli.ts b/packages/twilio-run/src/cli.ts index 7a3e1a71..666713ea 100644 --- a/packages/twilio-run/src/cli.ts +++ b/packages/twilio-run/src/cli.ts @@ -1,5 +1,6 @@ import yargs from 'yargs'; import * as DeployCommand from './commands/deploy'; +import EnvCommands from './commands/env'; import * as ListCommand from './commands/list'; import * as ListTemplatesCommand from './commands/list-templates'; import * as LogsCommand from './commands/logs'; @@ -16,5 +17,14 @@ export async function run(rawArgs: string[]) { .command(ListCommand) .command(ActivateCommand) .command(LogsCommand) + .command( + 'env', + 'Retrieve and modify the environment variables for your deployment', + (yargs) => { + yargs.command(EnvCommands.GetCommand); + yargs.command(EnvCommands.ListCommand); + yargs.command(EnvCommands.UnsetCommand); + } + ) .parse(rawArgs.slice(2)); } diff --git a/packages/twilio-run/src/commands/env/env-get.ts b/packages/twilio-run/src/commands/env/env-get.ts new file mode 100644 index 00000000..a12db53c --- /dev/null +++ b/packages/twilio-run/src/commands/env/env-get.ts @@ -0,0 +1,107 @@ +import { TwilioServerlessApiClient } from '@twilio-labs/serverless-api'; +import { Argv } from 'yargs'; +import { checkConfigForCredentials } from '../../checks/check-credentials'; +import checkForValidServiceSid from '../../checks/check-service-sid'; +import checkLegacyConfig from '../../checks/legacy-config'; +import { + EnvGetConfig, + EnvGetFlags, + getConfigFromFlags, +} from '../../config/env/env-get'; +import { + BASE_API_FLAG_NAMES, + BASE_CLI_FLAG_NAMES, + getRelevantFlags, +} from '../../flags'; +import { + getDebugFunction, + logApiError, + logger, + setLogLevelByName, +} from '../../utils/logger'; +import { writeOutput } from '../../utils/output'; +import { ExternalCliOptions } from '../shared'; +import { CliInfo } from '../types'; +import { getFullCommand } from '../utils'; + +const debug = getDebugFunction('twilio-run:env:get'); + +function handleError(err: Error) { + debug('%O', err); + if (err.name === 'TwilioApiError') { + logApiError(logger, err); + } else { + logger.error(err.message); + } + process.exit(1); +} + +export async function handler( + flags: EnvGetFlags, + externalCliOptions?: ExternalCliOptions +) { + setLogLevelByName(flags.logLevel); + + await checkLegacyConfig(flags.cwd, false); + + let config: EnvGetConfig; + try { + config = await getConfigFromFlags(flags, externalCliOptions); + } catch (err) { + debug(err); + logger.error(err.message); + process.exit(1); + return; + } + + if (!config) { + logger.error('Internal Error'); + process.exit(1); + } + + checkConfigForCredentials(config); + const command = getFullCommand(flags); + checkForValidServiceSid(command, config.serviceSid); + + try { + const client = new TwilioServerlessApiClient(config); + const result = await client.getEnvironmentVariables(config); + + const resultVariable = result.variables[0]; + if (!resultVariable) { + throw new Error( + `Could not find environment variable with name ${flags.key} for service "${result.serviceSid}" and environment "${result.environmentSid}".` + ); + } else { + writeOutput(resultVariable.value); + } + } catch (err) { + handleError(err); + } +} + +export const cliInfo: CliInfo = { + options: { + ...getRelevantFlags([ + ...BASE_CLI_FLAG_NAMES, + ...BASE_API_FLAG_NAMES, + 'service-sid', + 'environment', + 'key', + 'production', + ]), + }, +}; + +function optionBuilder(yargs: Argv): Argv { + yargs = Object.keys(cliInfo.options).reduce((yargs, name) => { + return yargs.option(name, cliInfo.options[name]); + }, yargs); + + return yargs; +} + +export const command = ['get']; +export const describe = + 'Retrieves the value of a specific environment variable'; +export const builder = optionBuilder; diff --git a/packages/twilio-run/src/commands/env/env-import.ts b/packages/twilio-run/src/commands/env/env-import.ts new file mode 100644 index 00000000..b6b01dc5 --- /dev/null +++ b/packages/twilio-run/src/commands/env/env-import.ts @@ -0,0 +1,98 @@ +import { TwilioServerlessApiClient } from '@twilio-labs/serverless-api'; +import { Argv } from 'yargs'; +import { checkConfigForCredentials } from '../../checks/check-credentials'; +import checkForValidServiceSid from '../../checks/check-service-sid'; +import checkLegacyConfig from '../../checks/legacy-config'; +import { + EnvImportConfig, + EnvImportFlags, + getConfigFromFlags, +} from '../../config/env/env-import'; +import { + BASE_API_FLAG_NAMES, + BASE_CLI_FLAG_NAMES, + getRelevantFlags, +} from '../../flags'; +import { + getDebugFunction, + logApiError, + logger, + setLogLevelByName, +} from '../../utils/logger'; +import { ExternalCliOptions } from '../shared'; +import { CliInfo } from '../types'; +import { getFullCommand } from '../utils'; + +const debug = getDebugFunction('twilio-run:env:unset'); + +function handleError(err: Error) { + debug('%O', err); + if (err.name === 'TwilioApiError') { + logApiError(logger, err); + } else { + logger.error(err.message); + } + process.exit(1); +} + +export async function handler( + flags: EnvImportFlags, + externalCliOptions?: ExternalCliOptions +) { + setLogLevelByName(flags.logLevel); + + await checkLegacyConfig(flags.cwd, false); + + let config: EnvImportConfig; + try { + config = await getConfigFromFlags(flags, externalCliOptions); + } catch (err) { + debug(err); + logger.error(err.message); + process.exit(1); + return; + } + + if (!config) { + logger.error('Internal Error'); + process.exit(1); + } + + checkConfigForCredentials(config); + const command = getFullCommand(flags); + checkForValidServiceSid(command, config.serviceSid); + + try { + const client = new TwilioServerlessApiClient(config); + await client.setEnvironmentVariables(config); + logger.info(`Environment variables updated`); + } catch (err) { + handleError(err); + } +} + +export const cliInfo: CliInfo = { + options: { + ...getRelevantFlags([ + ...BASE_CLI_FLAG_NAMES, + ...BASE_API_FLAG_NAMES, + 'service-sid', + 'environment', + 'env', + 'production', + ]), + }, +}; + +function optionBuilder(yargs: Argv): Argv { + yargs = Object.keys(cliInfo.options).reduce((yargs, name) => { + return yargs.option(name, cliInfo.options[name]); + }, yargs); + + return yargs; +} + +export const command = ['import']; +export const describe = + 'Takes a .env file and uploads all environment variables to a given environment'; +export const builder = optionBuilder; diff --git a/packages/twilio-run/src/commands/env/env-list.ts b/packages/twilio-run/src/commands/env/env-list.ts new file mode 100644 index 00000000..9ec0280e --- /dev/null +++ b/packages/twilio-run/src/commands/env/env-list.ts @@ -0,0 +1,101 @@ +import { TwilioServerlessApiClient } from '@twilio-labs/serverless-api'; +import { Argv } from 'yargs'; +import { checkConfigForCredentials } from '../../checks/check-credentials'; +import checkForValidServiceSid from '../../checks/check-service-sid'; +import checkLegacyConfig from '../../checks/legacy-config'; +import { + EnvListConfig, + EnvListFlags, + getConfigFromFlags, +} from '../../config/env/env-list'; +import { + BASE_API_FLAG_NAMES, + BASE_CLI_FLAG_NAMES, + getRelevantFlags, +} from '../../flags'; +import { outputVariables } from '../../printers/env/env-list'; +import { + getDebugFunction, + logApiError, + logger, + setLogLevelByName, +} from '../../utils/logger'; +import { ExternalCliOptions } from '../shared'; +import { CliInfo } from '../types'; +import { getFullCommand } from '../utils'; + +const debug = getDebugFunction('twilio-run:env:get'); + +function handleError(err: Error) { + debug('%O', err); + if (err.name === 'TwilioApiError') { + logApiError(logger, err); + } else { + logger.error(err.message); + } + process.exit(1); +} + +export async function handler( + flags: EnvListFlags, + externalCliOptions?: ExternalCliOptions +) { + setLogLevelByName(flags.logLevel); + + await checkLegacyConfig(flags.cwd, false); + + let config: EnvListConfig; + try { + config = await getConfigFromFlags(flags, externalCliOptions); + } catch (err) { + debug(err); + logger.error(err.message); + process.exit(1); + return; + } + + if (!config) { + logger.error('Internal Error'); + process.exit(1); + } + + checkConfigForCredentials(config); + const command = getFullCommand(flags); + checkForValidServiceSid(command, config.serviceSid); + + try { + const client = new TwilioServerlessApiClient(config); + const result = await client.getEnvironmentVariables(config); + + outputVariables(result, flags.outputFormat); + } catch (err) { + handleError(err); + } +} + +export const cliInfo: CliInfo = { + options: { + ...getRelevantFlags([ + ...BASE_CLI_FLAG_NAMES, + ...BASE_API_FLAG_NAMES, + 'service-sid', + 'environment', + 'show-values', + 'production', + 'output-format', + ]), + }, +}; + +function optionBuilder(yargs: Argv): Argv { + yargs = Object.keys(cliInfo.options).reduce((yargs, name) => { + return yargs.option(name, cliInfo.options[name]); + }, yargs); + + return yargs; +} + +export const command = ['list']; +export const describe = + 'Lists all environment variables for a given environment'; +export const builder = optionBuilder; diff --git a/packages/twilio-run/src/commands/env/env-set.ts b/packages/twilio-run/src/commands/env/env-set.ts new file mode 100644 index 00000000..4af95062 --- /dev/null +++ b/packages/twilio-run/src/commands/env/env-set.ts @@ -0,0 +1,99 @@ +import { TwilioServerlessApiClient } from '@twilio-labs/serverless-api'; +import { Argv } from 'yargs'; +import { checkConfigForCredentials } from '../../checks/check-credentials'; +import checkForValidServiceSid from '../../checks/check-service-sid'; +import checkLegacyConfig from '../../checks/legacy-config'; +import { + EnvSetConfig, + EnvSetFlags, + getConfigFromFlags, +} from '../../config/env/env-set'; +import { + BASE_API_FLAG_NAMES, + BASE_CLI_FLAG_NAMES, + getRelevantFlags, +} from '../../flags'; +import { + getDebugFunction, + logApiError, + logger, + setLogLevelByName, +} from '../../utils/logger'; +import { ExternalCliOptions } from '../shared'; +import { CliInfo } from '../types'; +import { getFullCommand } from '../utils'; + +const debug = getDebugFunction('twilio-run:env:unset'); + +function handleError(err: Error) { + debug('%O', err); + if (err.name === 'TwilioApiError') { + logApiError(logger, err); + } else { + logger.error(err.message); + } + process.exit(1); +} + +export async function handler( + flags: EnvSetFlags, + externalCliOptions?: ExternalCliOptions +) { + setLogLevelByName(flags.logLevel); + + await checkLegacyConfig(flags.cwd, false); + + let config: EnvSetConfig; + try { + config = await getConfigFromFlags(flags, externalCliOptions); + } catch (err) { + debug(err); + logger.error(err.message); + process.exit(1); + return; + } + + if (!config) { + logger.error('Internal Error'); + process.exit(1); + } + + checkConfigForCredentials(config); + const command = getFullCommand(flags); + checkForValidServiceSid(command, config.serviceSid); + + try { + const client = new TwilioServerlessApiClient(config); + await client.setEnvironmentVariables(config); + logger.info(`${flags.key} has been set`); + } catch (err) { + handleError(err); + } +} + +export const cliInfo: CliInfo = { + options: { + ...getRelevantFlags([ + ...BASE_CLI_FLAG_NAMES, + ...BASE_API_FLAG_NAMES, + 'service-sid', + 'environment', + 'key', + 'value', + 'production', + ]), + }, +}; + +function optionBuilder(yargs: Argv): Argv { + yargs = Object.keys(cliInfo.options).reduce((yargs, name) => { + return yargs.option(name, cliInfo.options[name]); + }, yargs); + + return yargs; +} + +export const command = ['set']; +export const describe = + 'Sets an environment variable with a given key and value'; +export const builder = optionBuilder; diff --git a/packages/twilio-run/src/commands/env/env-unset.ts b/packages/twilio-run/src/commands/env/env-unset.ts new file mode 100644 index 00000000..c4d40601 --- /dev/null +++ b/packages/twilio-run/src/commands/env/env-unset.ts @@ -0,0 +1,94 @@ +import { TwilioServerlessApiClient } from '@twilio-labs/serverless-api'; +import { Argv } from 'yargs'; +import { checkConfigForCredentials } from '../../checks/check-credentials'; +import checkForValidServiceSid from '../../checks/check-service-sid'; +import checkLegacyConfig from '../../checks/legacy-config'; +import { getConfigFromFlags } from '../../config/env/env-get'; +import { EnvUnsetConfig, EnvUnsetFlags } from '../../config/env/env-unset'; +import { + BASE_API_FLAG_NAMES, + BASE_CLI_FLAG_NAMES, + getRelevantFlags, +} from '../../flags'; +import { + getDebugFunction, + logApiError, + logger, + setLogLevelByName, +} from '../../utils/logger'; +import { ExternalCliOptions } from '../shared'; +import { CliInfo } from '../types'; +import { getFullCommand } from '../utils'; + +const debug = getDebugFunction('twilio-run:env:unset'); + +function handleError(err: Error) { + debug('%O', err); + if (err.name === 'TwilioApiError') { + logApiError(logger, err); + } else { + logger.error(err.message); + } + process.exit(1); +} + +export async function handler( + flags: EnvUnsetFlags, + externalCliOptions?: ExternalCliOptions +) { + setLogLevelByName(flags.logLevel); + + await checkLegacyConfig(flags.cwd, false); + + let config: EnvUnsetConfig; + try { + config = await getConfigFromFlags(flags, externalCliOptions); + } catch (err) { + debug(err); + logger.error(err.message); + process.exit(1); + return; + } + + if (!config) { + logger.error('Internal Error'); + process.exit(1); + } + + checkConfigForCredentials(config); + const command = getFullCommand(flags); + checkForValidServiceSid(command, config.serviceSid); + + try { + const client = new TwilioServerlessApiClient(config); + await client.removeEnvironmentVariables(config); + logger.info(`${flags.key} has been deleted`); + } catch (err) { + handleError(err); + } +} + +export const cliInfo: CliInfo = { + options: { + ...getRelevantFlags([ + ...BASE_CLI_FLAG_NAMES, + ...BASE_API_FLAG_NAMES, + 'service-sid', + 'environment', + 'key', + 'production', + ]), + }, +}; + +function optionBuilder(yargs: Argv): Argv { + yargs = Object.keys(cliInfo.options).reduce((yargs, name) => { + return yargs.option(name, cliInfo.options[name]); + }, yargs); + + return yargs; +} + +export const command = ['unset']; +export const describe = 'Removes an environment variable for a given key'; +export const builder = optionBuilder; diff --git a/packages/twilio-run/src/commands/env/index.ts b/packages/twilio-run/src/commands/env/index.ts new file mode 100644 index 00000000..ce4315e0 --- /dev/null +++ b/packages/twilio-run/src/commands/env/index.ts @@ -0,0 +1,13 @@ +import * as GetCommand from './env-get'; +import * as ImportCommand from './env-import'; +import * as ListCommand from './env-list'; +import * as SetCommand from './env-set'; +import * as UnsetCommand from './env-unset'; + +export default { + GetCommand, + SetCommand, + UnsetCommand, + ImportCommand, + ListCommand, +}; diff --git a/packages/twilio-run/src/config/env/env-get.ts b/packages/twilio-run/src/config/env/env-get.ts new file mode 100644 index 00000000..57647585 --- /dev/null +++ b/packages/twilio-run/src/config/env/env-get.ts @@ -0,0 +1,99 @@ +import { GetEnvironmentVariablesConfig as ApiEnvironmentConfig } from '@twilio-labs/serverless-api'; +import path from 'path'; +import { Arguments } from 'yargs'; +import { cliInfo } from '../../commands/list'; +import { ExternalCliOptions } from '../../commands/shared'; +import { + AllAvailableFlagTypes, + SharedFlagsWithCredentialNames, +} from '../../flags'; +import { getFunctionServiceSid } from '../../serverless-api/utils'; +import { readSpecializedConfig } from './../global'; +import { + getCredentialsFromFlags, + getServiceNameFromFlags, + readLocalEnvFile, +} from './../utils'; +import { mergeFlagsAndConfig } from './../utils/mergeFlagsAndConfig'; + +export type EnvGetConfig = ApiEnvironmentConfig & { + username: string; + password: string; + cwd: string; +}; + +export type ConfigurableEnvGetCliFlags = Pick< + AllAvailableFlagTypes, + SharedFlagsWithCredentialNames | 'serviceSid' | 'environment' | 'production' +>; +export type EnvGetFlags = Arguments< + ConfigurableEnvGetCliFlags & { + key: string; + } +>; + +export async function getConfigFromFlags( + flags: EnvGetFlags, + externalCliOptions?: ExternalCliOptions +): Promise { + let cwd = flags.cwd ? path.resolve(flags.cwd) : process.cwd(); + flags.cwd = cwd; + + if (flags.production) { + flags.environment = ''; + } + + const configFlags = readSpecializedConfig(cwd, flags.config, 'env', { + username: + flags.username || + (externalCliOptions && externalCliOptions.accountSid) || + undefined, + environmentSuffix: flags.environment, + }); + + flags = mergeFlagsAndConfig(configFlags, flags, cliInfo); + cwd = flags.cwd || cwd; + + const { localEnv: envFileVars, envPath } = await readLocalEnvFile(flags); + const { username, password } = await getCredentialsFromFlags( + flags, + envFileVars, + externalCliOptions + ); + + const serviceSid = + flags.serviceSid || + (await getFunctionServiceSid( + cwd, + flags.config, + 'env', + flags.username?.startsWith('AC') + ? flags.username + : username.startsWith('AC') + ? username + : externalCliOptions?.accountSid + )); + + let serviceName = await getServiceNameFromFlags(flags); + + if (!flags.key) { + throw new Error( + 'Missing --key argument. Please provide a key for your environment variable.' + ); + } + + const keys = [flags.key]; + + return { + cwd, + username, + password, + serviceSid, + serviceName, + environment: flags.environment, + region: flags.region, + edge: flags.edge, + keys, + getValues: true, + }; +} diff --git a/packages/twilio-run/src/config/env/env-import.ts b/packages/twilio-run/src/config/env/env-import.ts new file mode 100644 index 00000000..881c2d13 --- /dev/null +++ b/packages/twilio-run/src/config/env/env-import.ts @@ -0,0 +1,106 @@ +import { SetEnvironmentVariablesConfig as ApiEnvironmentConfig } from '@twilio-labs/serverless-api'; +import path from 'path'; +import { Arguments } from 'yargs'; +import { cliInfo } from '../../commands/list'; +import { ExternalCliOptions } from '../../commands/shared'; +import { + AllAvailableFlagTypes, + SharedFlagsWithCredentialNames, +} from '../../flags'; +import { getFunctionServiceSid } from '../../serverless-api/utils'; +import { readSpecializedConfig } from '../global'; +import { + filterEnvVariablesForDeploy, + getCredentialsFromFlags, + getServiceNameFromFlags, + readLocalEnvFile, +} from '../utils'; +import { mergeFlagsAndConfig } from '../utils/mergeFlagsAndConfig'; + +export type EnvImportConfig = ApiEnvironmentConfig & { + username: string; + password: string; + cwd: string; +}; + +export type ConfigurableEnvGetCliFlags = Pick< + AllAvailableFlagTypes, + SharedFlagsWithCredentialNames | 'serviceSid' | 'environment' | 'production' +>; +export type EnvImportFlags = Arguments< + ConfigurableEnvGetCliFlags & { + env: string; + } +>; + +export async function getConfigFromFlags( + flags: EnvImportFlags, + externalCliOptions?: ExternalCliOptions +): Promise { + let cwd = flags.cwd ? path.resolve(flags.cwd) : process.cwd(); + flags.cwd = cwd; + + if (flags.production) { + flags.environment = ''; + } + + const configFlags = readSpecializedConfig(cwd, flags.config, 'env', { + username: + flags.username || + (externalCliOptions && externalCliOptions.accountSid) || + undefined, + environmentSuffix: flags.environment, + }); + + flags = mergeFlagsAndConfig(configFlags, flags, cliInfo); + cwd = flags.cwd || cwd; + + const { localEnv: envFileVars, envPath } = await readLocalEnvFile(flags); + const { username, password } = await getCredentialsFromFlags( + flags, + envFileVars, + externalCliOptions + ); + + const serviceSid = + flags.serviceSid || + (await getFunctionServiceSid( + cwd, + flags.config, + 'env', + flags.username?.startsWith('AC') + ? flags.username + : username.startsWith('AC') + ? username + : externalCliOptions?.accountSid + )); + + let serviceName = await getServiceNameFromFlags(flags); + + if (!flags.key) { + throw new Error( + 'Missing --key argument. Please provide a key for your environment variable.' + ); + } + + if (!flags.value) { + throw new Error( + 'Missing --value argument. Please provide a key for your environment variable.' + ); + } + + const env = filterEnvVariablesForDeploy(envFileVars); + + return { + cwd, + username, + password, + serviceSid, + serviceName, + environment: flags.environment, + region: flags.region, + edge: flags.edge, + env, + append: false, + }; +} diff --git a/packages/twilio-run/src/config/env/env-list.ts b/packages/twilio-run/src/config/env/env-list.ts new file mode 100644 index 00000000..630f9fa5 --- /dev/null +++ b/packages/twilio-run/src/config/env/env-list.ts @@ -0,0 +1,97 @@ +import { GetEnvironmentVariablesConfig as ApiEnvironmentConfig } from '@twilio-labs/serverless-api'; +import path from 'path'; +import { Arguments } from 'yargs'; +import { cliInfo } from '../../commands/list'; +import { ExternalCliOptions } from '../../commands/shared'; +import { + AllAvailableFlagTypes, + SharedFlagsWithCredentialNames, +} from '../../flags'; +import { getFunctionServiceSid } from '../../serverless-api/utils'; +import { readSpecializedConfig } from '../global'; +import { + getCredentialsFromFlags, + getServiceNameFromFlags, + readLocalEnvFile, +} from '../utils'; +import { mergeFlagsAndConfig } from '../utils/mergeFlagsAndConfig'; + +export type EnvListConfig = ApiEnvironmentConfig & { + username: string; + password: string; + cwd: string; +}; + +export type ConfigurableEnvGetCliFlags = Pick< + AllAvailableFlagTypes, + | SharedFlagsWithCredentialNames + | 'serviceSid' + | 'environment' + | 'production' + | 'outputFormat' +>; +export type EnvListFlags = Arguments< + ConfigurableEnvGetCliFlags & { + showValues: boolean; + } +>; + +export async function getConfigFromFlags( + flags: EnvListFlags, + externalCliOptions?: ExternalCliOptions +): Promise { + let cwd = flags.cwd ? path.resolve(flags.cwd) : process.cwd(); + flags.cwd = cwd; + + if (flags.production) { + flags.environment = ''; + } + + const configFlags = readSpecializedConfig(cwd, flags.config, 'env', { + username: + flags.username || + (externalCliOptions && externalCliOptions.accountSid) || + undefined, + environmentSuffix: flags.environment, + }); + + flags = mergeFlagsAndConfig(configFlags, flags, cliInfo); + cwd = flags.cwd || cwd; + + const { localEnv: envFileVars, envPath } = await readLocalEnvFile(flags); + const { username, password } = await getCredentialsFromFlags( + flags, + envFileVars, + externalCliOptions + ); + + const serviceSid = + flags.serviceSid || + (await getFunctionServiceSid( + cwd, + flags.config, + 'env', + flags.username?.startsWith('AC') + ? flags.username + : username.startsWith('AC') + ? username + : externalCliOptions?.accountSid + )); + + let serviceName = await getServiceNameFromFlags(flags); + + const keys: string[] = []; + + return { + cwd, + username, + password, + serviceSid, + serviceName, + environment: flags.environment, + region: flags.region, + edge: flags.edge, + keys, + getValues: flags.showValues, + }; +} diff --git a/packages/twilio-run/src/config/env/env-set.ts b/packages/twilio-run/src/config/env/env-set.ts new file mode 100644 index 00000000..15f4a1dc --- /dev/null +++ b/packages/twilio-run/src/config/env/env-set.ts @@ -0,0 +1,108 @@ +import { SetEnvironmentVariablesConfig as ApiEnvironmentConfig } from '@twilio-labs/serverless-api'; +import path from 'path'; +import { Arguments } from 'yargs'; +import { cliInfo } from '../../commands/list'; +import { ExternalCliOptions } from '../../commands/shared'; +import { + AllAvailableFlagTypes, + SharedFlagsWithCredentialNames, +} from '../../flags'; +import { getFunctionServiceSid } from '../../serverless-api/utils'; +import { readSpecializedConfig } from '../global'; +import { + getCredentialsFromFlags, + getServiceNameFromFlags, + readLocalEnvFile, +} from '../utils'; +import { mergeFlagsAndConfig } from '../utils/mergeFlagsAndConfig'; + +export type EnvSetConfig = ApiEnvironmentConfig & { + username: string; + password: string; + cwd: string; +}; + +export type ConfigurableEnvGetCliFlags = Pick< + AllAvailableFlagTypes, + SharedFlagsWithCredentialNames | 'serviceSid' | 'environment' | 'production' +>; +export type EnvSetFlags = Arguments< + ConfigurableEnvGetCliFlags & { + key: string; + value: string; + } +>; + +export async function getConfigFromFlags( + flags: EnvSetFlags, + externalCliOptions?: ExternalCliOptions +): Promise { + let cwd = flags.cwd ? path.resolve(flags.cwd) : process.cwd(); + flags.cwd = cwd; + + if (flags.production) { + flags.environment = ''; + } + + const configFlags = readSpecializedConfig(cwd, flags.config, 'env', { + username: + flags.username || + (externalCliOptions && externalCliOptions.accountSid) || + undefined, + environmentSuffix: flags.environment, + }); + + flags = mergeFlagsAndConfig(configFlags, flags, cliInfo); + cwd = flags.cwd || cwd; + + const { localEnv: envFileVars, envPath } = await readLocalEnvFile(flags); + const { username, password } = await getCredentialsFromFlags( + flags, + envFileVars, + externalCliOptions + ); + + const serviceSid = + flags.serviceSid || + (await getFunctionServiceSid( + cwd, + flags.config, + 'env', + flags.username?.startsWith('AC') + ? flags.username + : username.startsWith('AC') + ? username + : externalCliOptions?.accountSid + )); + + let serviceName = await getServiceNameFromFlags(flags); + + if (!flags.key) { + throw new Error( + 'Missing --key argument. Please provide a key for your environment variable.' + ); + } + + if (!flags.value) { + throw new Error( + 'Missing --value argument. Please provide a key for your environment variable.' + ); + } + + const env = { + [flags.key]: flags.value, + }; + + return { + cwd, + username, + password, + serviceSid, + serviceName, + environment: flags.environment, + region: flags.region, + edge: flags.edge, + env, + append: true, + }; +} diff --git a/packages/twilio-run/src/config/env/env-unset.ts b/packages/twilio-run/src/config/env/env-unset.ts new file mode 100644 index 00000000..7f4df885 --- /dev/null +++ b/packages/twilio-run/src/config/env/env-unset.ts @@ -0,0 +1,98 @@ +import { RemoveEnvironmentVariablesConfig as ApiEnvironmentConfig } from '@twilio-labs/serverless-api'; +import path from 'path'; +import { Arguments } from 'yargs'; +import { cliInfo } from '../../commands/list'; +import { ExternalCliOptions } from '../../commands/shared'; +import { + AllAvailableFlagTypes, + SharedFlagsWithCredentialNames, +} from '../../flags'; +import { getFunctionServiceSid } from '../../serverless-api/utils'; +import { readSpecializedConfig } from '../global'; +import { + getCredentialsFromFlags, + getServiceNameFromFlags, + readLocalEnvFile, +} from '../utils'; +import { mergeFlagsAndConfig } from '../utils/mergeFlagsAndConfig'; + +export type EnvUnsetConfig = ApiEnvironmentConfig & { + username: string; + password: string; + cwd: string; +}; + +export type ConfigurableEnvGetCliFlags = Pick< + AllAvailableFlagTypes, + SharedFlagsWithCredentialNames | 'serviceSid' | 'environment' | 'production' +>; +export type EnvUnsetFlags = Arguments< + ConfigurableEnvGetCliFlags & { + key: string; + } +>; + +export async function getConfigFromFlags( + flags: EnvUnsetFlags, + externalCliOptions?: ExternalCliOptions +): Promise { + let cwd = flags.cwd ? path.resolve(flags.cwd) : process.cwd(); + flags.cwd = cwd; + + if (flags.production) { + flags.environment = ''; + } + + const configFlags = readSpecializedConfig(cwd, flags.config, 'env', { + username: + flags.username || + (externalCliOptions && externalCliOptions.accountSid) || + undefined, + environmentSuffix: flags.environment, + }); + + flags = mergeFlagsAndConfig(configFlags, flags, cliInfo); + cwd = flags.cwd || cwd; + + const { localEnv: envFileVars, envPath } = await readLocalEnvFile(flags); + const { username, password } = await getCredentialsFromFlags( + flags, + envFileVars, + externalCliOptions + ); + + const serviceSid = + flags.serviceSid || + (await getFunctionServiceSid( + cwd, + flags.config, + 'env', + flags.username?.startsWith('AC') + ? flags.username + : username.startsWith('AC') + ? username + : externalCliOptions?.accountSid + )); + + let serviceName = await getServiceNameFromFlags(flags); + + if (!flags.key) { + throw new Error( + 'Missing --key argument. Please provide a key for your environment variable.' + ); + } + + const keys = [flags.key]; + + return { + cwd, + username, + password, + serviceSid, + serviceName, + environment: flags.environment, + region: flags.region, + edge: flags.edge, + keys, + }; +} diff --git a/packages/twilio-run/src/config/global.ts b/packages/twilio-run/src/config/global.ts index 9c1ef6dc..43961d9b 100644 --- a/packages/twilio-run/src/config/global.ts +++ b/packages/twilio-run/src/config/global.ts @@ -9,7 +9,14 @@ export type SpecializedConfigOptions = { environmentSuffix: string; }; -export const EXCLUDED_FLAGS = ['username', 'password', 'config']; +export const EXCLUDED_FLAGS = [ + 'username', + 'password', + 'config', + 'key', + 'value', + 'show-values', +]; export function readSpecializedConfig( baseDir: string, diff --git a/packages/twilio-run/src/flags.ts b/packages/twilio-run/src/flags.ts index eff06433..d4de65b8 100644 --- a/packages/twilio-run/src/flags.ts +++ b/packages/twilio-run/src/flags.ts @@ -231,6 +231,21 @@ export const ALL_FLAGS = { describe: 'The version of Node.js to deploy the build to. (node10 or node12)', } as Options, + key: { + type: 'string', + describe: 'Name of the environment variable', + demandOption: true, + } as Options, + value: { + type: 'string', + describe: 'Name of the environment variable', + demandOption: true, + } as Options, + 'show-values': { + type: 'boolean', + describe: 'Show the values of your environment variables', + default: false, + } as Options, }; export type AvailableFlags = typeof ALL_FLAGS; @@ -297,4 +312,7 @@ export type AllAvailableFlagTypes = SharedFlagsWithCredentials & { legacyMode: boolean; forkProcess: boolean; runtime?: string; + key: string; + value?: string; + showValues: boolean; }; diff --git a/packages/twilio-run/src/printers/env/env-list.ts b/packages/twilio-run/src/printers/env/env-list.ts new file mode 100644 index 00000000..2343ee6b --- /dev/null +++ b/packages/twilio-run/src/printers/env/env-list.ts @@ -0,0 +1,24 @@ +import { GetEnvironmentVariablesResult } from '@twilio-labs/serverless-api'; +import chalk from 'chalk'; +import { writeOutput } from '../../utils/output'; + +export function outputVariables( + result: GetEnvironmentVariablesResult, + format?: 'json' +) { + if (format === 'json') { + writeOutput(JSON.stringify(result, null, '\t')); + } else { + const output = result.variables + .map((entry: { [key: string]: string | undefined }) => { + const key = chalk`{bold ${entry.key}}`; + const value = + typeof entry.value !== 'undefined' + ? entry.value + : chalk`{dim Use --show-values to display value}`; + return `${key} ${value}`; + }) + .join('\n'); + writeOutput(output); + } +} diff --git a/packages/twilio-run/src/serverless-api/utils.ts b/packages/twilio-run/src/serverless-api/utils.ts index d05db822..7a43773e 100644 --- a/packages/twilio-run/src/serverless-api/utils.ts +++ b/packages/twilio-run/src/serverless-api/utils.ts @@ -21,7 +21,7 @@ export type ApiErrorResponse = { export async function getFunctionServiceSid( cwd: string, configName: string, - commandConfig: 'deploy' | 'list' | 'logs' | 'promote', + commandConfig: 'deploy' | 'list' | 'logs' | 'promote' | 'env', username?: string ): Promise { const twilioConfig = readSpecializedConfig(cwd, configName, commandConfig, { diff --git a/packages/twilio-run/src/types/config.ts b/packages/twilio-run/src/types/config.ts index 464802a4..80dd3bfa 100644 --- a/packages/twilio-run/src/types/config.ts +++ b/packages/twilio-run/src/types/config.ts @@ -1,6 +1,7 @@ import { Merge } from 'type-fest'; import { ConfigurableNewCliFlags } from '../commands/new'; import { ConfigurableDeployCliFlags } from '../config/deploy'; +import { ConfigurableEnvGetCliFlags } from '../config/env/env-get'; import { ConfigurableListCliFlags } from '../config/list'; import { ConfigurableLogsCliFlags } from '../config/logs'; import { ConfigurablePromoteCliFlags } from '../config/promote'; @@ -24,6 +25,7 @@ export type CommandConfigurations = { promote?: Partial; logs?: Partial; new?: Partial; + env?: Partial; }; export type ConfigurationFile = Merge<