From 44fbbbb633e6dbdaae4e83ef24023f64413bd5d1 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Thu, 24 Jun 2021 17:24:11 -0700 Subject: [PATCH 1/3] feat: add new env commands This change introduces a new set of commands around retrieving and modifying environment variables from already deployed environments. re #249 --- packages/plugin-serverless/package.json | 3 + .../src/commands/serverless/env/get.js | 41 ++++++ .../src/commands/serverless/env/import.js | 41 ++++++ .../src/commands/serverless/env/list.js | 41 ++++++ .../src/commands/serverless/env/unset.js | 41 ++++++ .../src/api/utils/type-checks.ts | 15 ++ packages/serverless-api/src/api/variables.ts | 58 +++++++- packages/serverless-api/src/client.ts | 130 ++++++++++++++++++ packages/serverless-api/src/types/env.ts | 47 +++++++ packages/serverless-api/src/types/index.ts | 3 +- packages/twilio-run/src/cli.ts | 10 ++ .../twilio-run/src/commands/env/env-get.ts | 107 ++++++++++++++ .../twilio-run/src/commands/env/env-import.ts | 39 ++++++ .../twilio-run/src/commands/env/env-list.ts | 101 ++++++++++++++ .../twilio-run/src/commands/env/env-set.ts | 40 ++++++ .../twilio-run/src/commands/env/env-unset.ts | 94 +++++++++++++ packages/twilio-run/src/commands/env/index.ts | 13 ++ packages/twilio-run/src/config/env/env-get.ts | 95 +++++++++++++ .../twilio-run/src/config/env/env-list.ts | 93 +++++++++++++ .../twilio-run/src/config/env/env-unset.ts | 94 +++++++++++++ packages/twilio-run/src/config/global.ts | 9 +- packages/twilio-run/src/flags.ts | 18 +++ .../twilio-run/src/printers/env/env-list.ts | 19 +++ .../twilio-run/src/serverless-api/utils.ts | 2 +- packages/twilio-run/src/types/config.ts | 2 + 25 files changed, 1151 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-serverless/src/commands/serverless/env/get.js create mode 100644 packages/plugin-serverless/src/commands/serverless/env/import.js create mode 100644 packages/plugin-serverless/src/commands/serverless/env/list.js create mode 100644 packages/plugin-serverless/src/commands/serverless/env/unset.js create mode 100644 packages/serverless-api/src/api/utils/type-checks.ts create mode 100644 packages/serverless-api/src/types/env.ts create mode 100644 packages/twilio-run/src/commands/env/env-get.ts create mode 100644 packages/twilio-run/src/commands/env/env-import.ts create mode 100644 packages/twilio-run/src/commands/env/env-list.ts create mode 100644 packages/twilio-run/src/commands/env/env-set.ts create mode 100644 packages/twilio-run/src/commands/env/env-unset.ts create mode 100644 packages/twilio-run/src/commands/env/index.ts create mode 100644 packages/twilio-run/src/config/env/env-get.ts create mode 100644 packages/twilio-run/src/config/env/env-list.ts create mode 100644 packages/twilio-run/src/config/env/env-unset.ts create mode 100644 packages/twilio-run/src/printers/env/env-list.ts 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/commands/serverless/env/get.js b/packages/plugin-serverless/src/commands/serverless/env/get.js new file mode 100644 index 00000000..ff51e626 --- /dev/null +++ b/packages/plugin-serverless/src/commands/serverless/env/get.js @@ -0,0 +1,41 @@ +const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; + +const { + handler, + cliInfo, + describe, +} = require('twilio-run/dist/commands/env/env-get'); +const { + convertYargsOptionsToOclifFlags, + normalizeFlags, + createExternalCliOptions, + getRegionAndEdge, +} = require('../../../utils'); + +const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options); + +class EnvGet extends TwilioClientCommand { + async run() { + await super.run(); + + let { flags, args } = this.parse(EnvGet); + flags = normalizeFlags(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, args); + return handler(opts, externalOptions); + } +} + +EnvGet.description = describe; + +EnvGet.flags = Object.assign(flags, { + profile: TwilioClientCommand.flags.profile, +}); + +module.exports = EnvGet; 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..0094da3b --- /dev/null +++ b/packages/plugin-serverless/src/commands/serverless/env/import.js @@ -0,0 +1,41 @@ +const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; + +const { + handler, + cliInfo, + describe, +} = require('twilio-run/dist/commands/env/env-import'); +const { + convertYargsOptionsToOclifFlags, + normalizeFlags, + createExternalCliOptions, + getRegionAndEdge, +} = require('../../../utils'); + +const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options); + +class EnvironmentImport extends TwilioClientCommand { + async run() { + await super.run(); + + let { flags, args } = this.parse(EnvironmentImport); + flags = normalizeFlags(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, args); + return handler(opts, externalOptions); + } +} + +EnvironmentImport.description = describe; + +EnvironmentImport.flags = Object.assign(flags, { + profile: TwilioClientCommand.flags.profile, +}); + +module.exports = EnvironmentImport; 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..d9d4600e --- /dev/null +++ b/packages/plugin-serverless/src/commands/serverless/env/list.js @@ -0,0 +1,41 @@ +const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; + +const { + handler, + cliInfo, + describe, +} = require('twilio-run/dist/commands/env/env-list'); +const { + convertYargsOptionsToOclifFlags, + normalizeFlags, + createExternalCliOptions, + getRegionAndEdge, +} = require('../../../utils'); + +const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options); + +class EnvList extends TwilioClientCommand { + async run() { + await super.run(); + + let { flags, args } = this.parse(EnvList); + flags = normalizeFlags(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, args); + return handler(opts, externalOptions); + } +} + +EnvList.description = describe; + +EnvList.flags = Object.assign(flags, { + profile: TwilioClientCommand.flags.profile, +}); + +module.exports = EnvList; 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..525e2321 --- /dev/null +++ b/packages/plugin-serverless/src/commands/serverless/env/unset.js @@ -0,0 +1,41 @@ +const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; + +const { + handler, + cliInfo, + describe, +} = require('twilio-run/dist/commands/env/env-unset'); +const { + convertYargsOptionsToOclifFlags, + normalizeFlags, + createExternalCliOptions, + getRegionAndEdge, +} = require('../../../utils'); + +const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options); + +class EnvUnset extends TwilioClientCommand { + async run() { + await super.run(); + + let { flags, args } = this.parse(EnvUnset); + flags = normalizeFlags(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, args); + return handler(opts, externalOptions); + } +} + +EnvUnset.description = describe; + +EnvUnset.flags = Object.assign(flags, { + profile: TwilioClientCommand.flags.profile, +}); + +module.exports = EnvUnset; 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..8109a871 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'); @@ -182,3 +184,55 @@ export async function setEnvironmentVariables( await Promise.all(variableResources); } + +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; + } +} + +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..7f176f20 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,125 @@ export class TwilioServerlessApiClient extends events.EventEmitter { return this.client; } + 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; + } + + return { serviceSid, environmentSid }; + } + + 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 }; + } + + 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..b93acb0a --- /dev/null +++ b/packages/twilio-run/src/commands/env/env-import.ts @@ -0,0 +1,39 @@ +import { Argv } from 'yargs'; +import { + BASE_API_FLAG_NAMES, + BASE_CLI_FLAG_NAMES, + getRelevantFlags, +} from '../../flags'; +import { ExternalCliOptions } from '../shared'; +import { CliInfo } from '../types'; + +export async function handler( + flagsInput: {}, + externalCliOptions?: ExternalCliOptions +) {} + +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..f34cce89 --- /dev/null +++ b/packages/twilio-run/src/commands/env/env-set.ts @@ -0,0 +1,40 @@ +import { Argv } from 'yargs'; +import { + BASE_API_FLAG_NAMES, + BASE_CLI_FLAG_NAMES, + getRelevantFlags, +} from '../../flags'; +import { ExternalCliOptions } from '../shared'; +import { CliInfo } from '../types'; + +export async function handler( + flagsInput: {}, + externalCliOptions?: ExternalCliOptions +) {} + +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..0e01a30e --- /dev/null +++ b/packages/twilio-run/src/config/env/env-get.ts @@ -0,0 +1,95 @@ +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; + + 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-list.ts b/packages/twilio-run/src/config/env/env-list.ts new file mode 100644 index 00000000..f260a897 --- /dev/null +++ b/packages/twilio-run/src/config/env/env-list.ts @@ -0,0 +1,93 @@ +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; + + 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-unset.ts b/packages/twilio-run/src/config/env/env-unset.ts new file mode 100644 index 00000000..8a889925 --- /dev/null +++ b/packages/twilio-run/src/config/env/env-unset.ts @@ -0,0 +1,94 @@ +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; + + 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..b293393b --- /dev/null +++ b/packages/twilio-run/src/printers/env/env-list.ts @@ -0,0 +1,19 @@ +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) => { + return chalk`{bold ${entry.key}} {dim ${entry.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< From cfa4d912160678008db279763b46b9608033d89c Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Thu, 1 Jul 2021 16:23:54 -0700 Subject: [PATCH 2/3] chore: address PR comments and add remaining commands --- .../plugin-serverless/src/TwilioRunCommand.js | 49 ++++++++ .../src/commands/serverless/env/get.js | 46 +------- .../src/commands/serverless/env/import.js | 46 +------- .../src/commands/serverless/env/list.js | 46 +------- .../src/commands/serverless/env/set.js | 7 ++ .../src/commands/serverless/env/unset.js | 46 +------- .../tests/TwilioRunCommand.test.js | 85 ++++++++++++++ packages/serverless-api/src/api/variables.ts | 38 +++++- packages/serverless-api/src/client.ts | 9 ++ .../twilio-run/src/commands/env/env-import.ts | 63 +++++++++- .../twilio-run/src/commands/env/env-set.ts | 63 +++++++++- packages/twilio-run/src/config/env/env-get.ts | 4 + .../twilio-run/src/config/env/env-import.ts | 106 +++++++++++++++++ .../twilio-run/src/config/env/env-list.ts | 4 + packages/twilio-run/src/config/env/env-set.ts | 108 ++++++++++++++++++ .../twilio-run/src/config/env/env-unset.ts | 4 + .../twilio-run/src/printers/env/env-list.ts | 9 +- 17 files changed, 566 insertions(+), 167 deletions(-) create mode 100644 packages/plugin-serverless/src/TwilioRunCommand.js create mode 100644 packages/plugin-serverless/src/commands/serverless/env/set.js create mode 100644 packages/plugin-serverless/tests/TwilioRunCommand.test.js create mode 100644 packages/twilio-run/src/config/env/env-import.ts create mode 100644 packages/twilio-run/src/config/env/env-set.ts 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 index ff51e626..16f86277 100644 --- a/packages/plugin-serverless/src/commands/serverless/env/get.js +++ b/packages/plugin-serverless/src/commands/serverless/env/get.js @@ -1,41 +1,7 @@ -const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; +const { createTwilioRunCommand } = require('../../../TwilioRunCommand'); -const { - handler, - cliInfo, - describe, -} = require('twilio-run/dist/commands/env/env-get'); -const { - convertYargsOptionsToOclifFlags, - normalizeFlags, - createExternalCliOptions, - getRegionAndEdge, -} = require('../../../utils'); - -const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options); - -class EnvGet extends TwilioClientCommand { - async run() { - await super.run(); - - let { flags, args } = this.parse(EnvGet); - flags = normalizeFlags(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, args); - return handler(opts, externalOptions); - } -} - -EnvGet.description = describe; - -EnvGet.flags = Object.assign(flags, { - profile: TwilioClientCommand.flags.profile, -}); - -module.exports = EnvGet; +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 index 0094da3b..f194e99a 100644 --- a/packages/plugin-serverless/src/commands/serverless/env/import.js +++ b/packages/plugin-serverless/src/commands/serverless/env/import.js @@ -1,41 +1,7 @@ -const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; +const { createTwilioRunCommand } = require('../../../TwilioRunCommand'); -const { - handler, - cliInfo, - describe, -} = require('twilio-run/dist/commands/env/env-import'); -const { - convertYargsOptionsToOclifFlags, - normalizeFlags, - createExternalCliOptions, - getRegionAndEdge, -} = require('../../../utils'); - -const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options); - -class EnvironmentImport extends TwilioClientCommand { - async run() { - await super.run(); - - let { flags, args } = this.parse(EnvironmentImport); - flags = normalizeFlags(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, args); - return handler(opts, externalOptions); - } -} - -EnvironmentImport.description = describe; - -EnvironmentImport.flags = Object.assign(flags, { - profile: TwilioClientCommand.flags.profile, -}); - -module.exports = EnvironmentImport; +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 index d9d4600e..444a48c4 100644 --- a/packages/plugin-serverless/src/commands/serverless/env/list.js +++ b/packages/plugin-serverless/src/commands/serverless/env/list.js @@ -1,41 +1,7 @@ -const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; +const { createTwilioRunCommand } = require('../../../TwilioRunCommand'); -const { - handler, - cliInfo, - describe, -} = require('twilio-run/dist/commands/env/env-list'); -const { - convertYargsOptionsToOclifFlags, - normalizeFlags, - createExternalCliOptions, - getRegionAndEdge, -} = require('../../../utils'); - -const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options); - -class EnvList extends TwilioClientCommand { - async run() { - await super.run(); - - let { flags, args } = this.parse(EnvList); - flags = normalizeFlags(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, args); - return handler(opts, externalOptions); - } -} - -EnvList.description = describe; - -EnvList.flags = Object.assign(flags, { - profile: TwilioClientCommand.flags.profile, -}); - -module.exports = EnvList; +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 index 525e2321..730ce8cd 100644 --- a/packages/plugin-serverless/src/commands/serverless/env/unset.js +++ b/packages/plugin-serverless/src/commands/serverless/env/unset.js @@ -1,41 +1,7 @@ -const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; +const { createTwilioRunCommand } = require('../../../TwilioRunCommand'); -const { - handler, - cliInfo, - describe, -} = require('twilio-run/dist/commands/env/env-unset'); -const { - convertYargsOptionsToOclifFlags, - normalizeFlags, - createExternalCliOptions, - getRegionAndEdge, -} = require('../../../utils'); - -const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options); - -class EnvUnset extends TwilioClientCommand { - async run() { - await super.run(); - - let { flags, args } = this.parse(EnvUnset); - flags = normalizeFlags(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, args); - return handler(opts, externalOptions); - } -} - -EnvUnset.description = describe; - -EnvUnset.flags = Object.assign(flags, { - profile: TwilioClientCommand.flags.profile, -}); - -module.exports = EnvUnset; +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/variables.ts b/packages/serverless-api/src/api/variables.ts index 8109a871..e5ea59da 100644 --- a/packages/serverless-api/src/api/variables.ts +++ b/packages/serverless-api/src/api/variables.ts @@ -139,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, @@ -183,8 +185,32 @@ 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, @@ -203,6 +229,16 @@ export async function deleteEnvironmentVariable( } } +/** + * 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, diff --git a/packages/serverless-api/src/client.ts b/packages/serverless-api/src/client.ts index 7f176f20..e948ed23 100644 --- a/packages/serverless-api/src/client.ts +++ b/packages/serverless-api/src/client.ts @@ -189,6 +189,15 @@ export class TwilioServerlessApiClient extends events.EventEmitter { environmentSid = config.environment; } + const removeRedundantVariables = !config.append; + await setEnvironmentVariables( + config.env, + environmentSid, + serviceSid, + this, + removeRedundantVariables + ); + return { serviceSid, environmentSid }; } diff --git a/packages/twilio-run/src/commands/env/env-import.ts b/packages/twilio-run/src/commands/env/env-import.ts index b93acb0a..b6b01dc5 100644 --- a/packages/twilio-run/src/commands/env/env-import.ts +++ b/packages/twilio-run/src/commands/env/env-import.ts @@ -1,16 +1,75 @@ +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( - flagsInput: {}, + 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: { diff --git a/packages/twilio-run/src/commands/env/env-set.ts b/packages/twilio-run/src/commands/env/env-set.ts index f34cce89..4af95062 100644 --- a/packages/twilio-run/src/commands/env/env-set.ts +++ b/packages/twilio-run/src/commands/env/env-set.ts @@ -1,16 +1,75 @@ +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( - flagsInput: {}, + 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: { diff --git a/packages/twilio-run/src/config/env/env-get.ts b/packages/twilio-run/src/config/env/env-get.ts index 0e01a30e..57647585 100644 --- a/packages/twilio-run/src/config/env/env-get.ts +++ b/packages/twilio-run/src/config/env/env-get.ts @@ -39,6 +39,10 @@ export async function getConfigFromFlags( 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 || 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 index f260a897..630f9fa5 100644 --- a/packages/twilio-run/src/config/env/env-list.ts +++ b/packages/twilio-run/src/config/env/env-list.ts @@ -43,6 +43,10 @@ export async function getConfigFromFlags( 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 || 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 index 8a889925..7f4df885 100644 --- a/packages/twilio-run/src/config/env/env-unset.ts +++ b/packages/twilio-run/src/config/env/env-unset.ts @@ -39,6 +39,10 @@ export async function getConfigFromFlags( 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 || diff --git a/packages/twilio-run/src/printers/env/env-list.ts b/packages/twilio-run/src/printers/env/env-list.ts index b293393b..2343ee6b 100644 --- a/packages/twilio-run/src/printers/env/env-list.ts +++ b/packages/twilio-run/src/printers/env/env-list.ts @@ -10,8 +10,13 @@ export function outputVariables( writeOutput(JSON.stringify(result, null, '\t')); } else { const output = result.variables - .map((entry) => { - return chalk`{bold ${entry.key}} {dim ${entry.value}}`; + .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); From 1342545e012587f2023b97d41e5ea85d5d8468d9 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Fri, 2 Jul 2021 11:29:06 -0700 Subject: [PATCH 3/3] docs(serverless-api): update jsdoc headers --- packages/serverless-api/src/client.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/serverless-api/src/client.ts b/packages/serverless-api/src/client.ts index e948ed23..6a32d383 100644 --- a/packages/serverless-api/src/client.ts +++ b/packages/serverless-api/src/client.ts @@ -161,6 +161,14 @@ 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 { @@ -201,6 +209,14 @@ export class TwilioServerlessApiClient extends events.EventEmitter { 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 { @@ -251,6 +267,13 @@ export class TwilioServerlessApiClient extends events.EventEmitter { 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 {