From 44fbbbb633e6dbdaae4e83ef24023f64413bd5d1 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Thu, 24 Jun 2021 17:24:11 -0700 Subject: [PATCH] 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<