-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added the ability to work with secrets in the CLI. set, delete and ge…
…t list of all secrets keys, per region.
- Loading branch information
1 parent
08d710e
commit 97247b5
Showing
7 changed files
with
320 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import { Flags } from '@oclif/core'; | ||
import { Relationship } from '@oclif/core/lib/interfaces/parser'; | ||
|
||
import { AuthenticatedCommand } from 'commands-base/authenticated-command'; | ||
import { APP_SECRET_MANAGEMENT_MODES } from 'consts/manage-app-secret'; | ||
import { DynamicChoicesService } from 'services/dynamic-choices-service'; | ||
import { handleSecretRequest, listAppSecretKeys } from 'services/manage-app-secret-service'; | ||
import { PromptService } from 'services/prompt-service'; | ||
import { ManageAppSecretFlags } from 'types/commands/manage-app-secret'; | ||
import { AppId } from 'types/general'; | ||
import logger from 'utils/logger'; | ||
import { addRegionToFlags, chooseRegionIfNeeded, getRegionFromString } from 'utils/region'; | ||
|
||
const MODES_WITH_KEYS: Array<APP_SECRET_MANAGEMENT_MODES> = [ | ||
APP_SECRET_MANAGEMENT_MODES.SET, | ||
APP_SECRET_MANAGEMENT_MODES.DELETE, | ||
]; | ||
|
||
const isKeyRequired = (mode: APP_SECRET_MANAGEMENT_MODES) => MODES_WITH_KEYS.includes(mode); | ||
const isValueRequired = (mode: APP_SECRET_MANAGEMENT_MODES) => mode === APP_SECRET_MANAGEMENT_MODES.SET; | ||
|
||
const promptForModeIfNotProvided = async (mode?: APP_SECRET_MANAGEMENT_MODES) => { | ||
if (!mode) { | ||
mode = await PromptService.promptSelectionWithAutoComplete<APP_SECRET_MANAGEMENT_MODES>( | ||
'Select app secret variables management mode', | ||
Object.values(APP_SECRET_MANAGEMENT_MODES), | ||
); | ||
} | ||
|
||
return mode; | ||
}; | ||
|
||
const promptForKeyIfNotProvided = async (mode: APP_SECRET_MANAGEMENT_MODES, appId: AppId, key?: string) => { | ||
if (!key && isKeyRequired(mode)) { | ||
const existingKeys = await listAppSecretKeys(appId); | ||
key = await PromptService.promptSelectionWithAutoComplete('Enter key for secret variable', existingKeys, { | ||
includeInputInSelection: true, | ||
}); | ||
} | ||
|
||
return key; | ||
}; | ||
|
||
const promptForValueIfNotProvided = async (mode: APP_SECRET_MANAGEMENT_MODES, value?: string) => { | ||
if (!value && isValueRequired(mode)) { | ||
value = await PromptService.promptForHiddenInput( | ||
'value', | ||
'Enter value for secret variable', | ||
'You must enter a value value', | ||
); | ||
} | ||
|
||
return value; | ||
}; | ||
|
||
const flagsWithModeRelationships: Relationship = { | ||
type: 'all', | ||
flags: [ | ||
{ | ||
name: 'mode', | ||
|
||
when: async (flags: Record<string, unknown>) => isValueRequired(flags.mode as (typeof MODES_WITH_KEYS)[number]), | ||
}, | ||
], | ||
}; | ||
|
||
export default class Secret extends AuthenticatedCommand { | ||
static description = 'Manage secret variables for your app hosted on monday-code.'; | ||
|
||
static examples = ['<%= config.bin %> <%= command.id %>']; | ||
|
||
static flags = Secret.serializeFlags( | ||
addRegionToFlags({ | ||
appId: Flags.integer({ | ||
char: 'i', | ||
aliases: ['a'], | ||
description: 'The id of the app to manage secret variables for', | ||
}), | ||
mode: Flags.string({ | ||
char: 'm', | ||
description: 'management mode', | ||
options: Object.values(APP_SECRET_MANAGEMENT_MODES), | ||
}), | ||
key: Flags.string({ | ||
char: 'k', | ||
description: 'variable key [required for set and delete]]', | ||
relationships: [flagsWithModeRelationships], | ||
}), | ||
value: Flags.string({ | ||
char: 'v', | ||
description: 'variable value [required for set]', | ||
relationships: [flagsWithModeRelationships], | ||
}), | ||
}), | ||
); | ||
|
||
static args = {}; | ||
DEBUG_TAG = 'secret'; | ||
public async run(): Promise<void> { | ||
try { | ||
const { flags } = await this.parse(Secret); | ||
const { region: strRegion } = flags; | ||
const region = getRegionFromString(strRegion); | ||
let { mode, key, value, appId } = flags as ManageAppSecretFlags; | ||
|
||
if (!appId) { | ||
appId = Number(await DynamicChoicesService.chooseApp()); | ||
} | ||
|
||
const selectedRegion = await chooseRegionIfNeeded(region, { appId }); | ||
|
||
mode = await promptForModeIfNotProvided(mode); | ||
key = await promptForKeyIfNotProvided(mode, appId, key); | ||
value = await promptForValueIfNotProvided(mode, value); | ||
this.preparePrintCommand(this, { appId, mode, key, value, region: selectedRegion }); | ||
console.log("1") | ||
|
||
await handleSecretRequest(appId, mode, key, value, selectedRegion); | ||
console.log("4") | ||
} catch (error: any) { | ||
logger.debug(error, this.DEBUG_TAG); | ||
|
||
// need to signal to the parent process that the command failed | ||
process.exit(1); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export enum APP_SECRET_MANAGEMENT_MODES { | ||
LIST_KEYS = 'list-keys', | ||
SET = 'set', | ||
DELETE = 'delete', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { StatusCodes } from 'http-status-codes'; | ||
|
||
import { APP_SECRET_MANAGEMENT_MODES } from 'consts/manage-app-secret'; | ||
import { appSecretKeysUrl, appSecretUrl } from 'consts/urls'; | ||
import { execute } from 'services/api-service'; | ||
import { listAppSecretKeysResponseSchema } from 'services/schemas/manage-app-secret-service-schemas'; | ||
import { HttpError } from 'types/errors'; | ||
import { AppId } from 'types/general'; | ||
import { Region } from 'types/general/region'; | ||
import { HttpMethodTypes } from 'types/services/api-service'; | ||
import { ListAppSecretKeysResponse } from 'types/services/manage-app-secret-service'; | ||
import logger from 'utils/logger'; | ||
import { addRegionToQuery } from 'utils/region'; | ||
import { appsUrlBuilder } from 'utils/urls-builder'; | ||
|
||
const handleHttpErrors = (error: HttpError) => { | ||
switch (error.code) { | ||
case StatusCodes.NOT_FOUND: { | ||
throw new Error('monday-code deployment not found for the requested app'); | ||
} | ||
|
||
case StatusCodes.FORBIDDEN: { | ||
throw new Error('You are not authorized to access the requested app'); | ||
} | ||
|
||
default: { | ||
throw error; | ||
} | ||
} | ||
}; | ||
|
||
export const listAppSecretKeys = async (appId: AppId, region?: Region): Promise<Array<string>> => { | ||
try { | ||
const path = appSecretKeysUrl(appId); | ||
const url = appsUrlBuilder(path); | ||
const query = addRegionToQuery({}, region); | ||
|
||
const response = await execute<ListAppSecretKeysResponse>( | ||
{ | ||
query, | ||
url, | ||
headers: { Accept: 'application/json' }, | ||
method: HttpMethodTypes.GET, | ||
}, | ||
listAppSecretKeysResponseSchema, | ||
); | ||
|
||
return response.keys; | ||
} catch (error: any) { | ||
if (error instanceof HttpError) { | ||
handleHttpErrors(error); | ||
} | ||
|
||
throw new Error('failed to list app secret keys'); | ||
} | ||
}; | ||
|
||
export const setSecret = async (appId: AppId, key: string, value: string, region?: Region) => { | ||
try { | ||
const path = appSecretUrl(appId, key); | ||
const url = appsUrlBuilder(path); | ||
const query = addRegionToQuery({}, region); | ||
await execute({ | ||
query, | ||
url, | ||
headers: { Accept: 'application/json' }, | ||
method: HttpMethodTypes.PUT, | ||
body: { value }, | ||
}); | ||
} catch (error: any) { | ||
if (error instanceof HttpError) { | ||
handleHttpErrors(error); | ||
} | ||
|
||
throw new Error('failed to set secret variable'); | ||
} | ||
}; | ||
|
||
export const deleteSecret = async (appId: AppId, key: string, region?: Region) => { | ||
try { | ||
const path = appSecretUrl(appId, key); | ||
const url = appsUrlBuilder(path); | ||
const query = addRegionToQuery({}, region); | ||
|
||
await execute({ | ||
query, | ||
url, | ||
headers: { Accept: 'application/json' }, | ||
method: HttpMethodTypes.DELETE, | ||
}); | ||
|
||
return true; | ||
} catch (error: any) { | ||
if (error instanceof HttpError) { | ||
handleHttpErrors(error); | ||
} | ||
|
||
throw new Error('failed to delete secret variable'); | ||
} | ||
}; | ||
|
||
const handleSecretSet = async (appId: AppId, region: Region | undefined, key: string, value: string) => { | ||
if (!key || !value) { | ||
throw new Error('key and value are required'); | ||
} | ||
|
||
await setSecret(appId, key, value, region); | ||
logger.info(`Secret variable connected to key: "${key}", was set`); | ||
}; | ||
|
||
const handleSecretDelete = async (appId: AppId, region: Region | undefined, key: string) => { | ||
if (!key) { | ||
throw new Error('key is required'); | ||
} | ||
|
||
await deleteSecret(appId, key, region); | ||
logger.info(`Secret variable connected to key: "${key}", was deleted`); | ||
}; | ||
|
||
const handleSecretListKeys = async (appId: AppId, region: Region | undefined) => { | ||
const response = await listAppSecretKeys(appId, region); | ||
if (response?.length === 0) { | ||
logger.info('No secret variables found'); | ||
return; | ||
} | ||
|
||
logger.info('App secret variable keys:'); | ||
logger.table(response.map(key => ({ keys: key }))); | ||
}; | ||
|
||
const MAP_MODE_TO_HANDLER: Record< | ||
APP_SECRET_MANAGEMENT_MODES, | ||
(appId: AppId, region: Region | undefined, key: string, value: string) => Promise<void> | ||
> = { | ||
[APP_SECRET_MANAGEMENT_MODES.SET]: handleSecretSet, | ||
[APP_SECRET_MANAGEMENT_MODES.DELETE]: handleSecretDelete, | ||
[APP_SECRET_MANAGEMENT_MODES.LIST_KEYS]: handleSecretListKeys, | ||
}; | ||
|
||
export const handleSecretRequest = async ( | ||
appId: AppId, | ||
mode: APP_SECRET_MANAGEMENT_MODES, | ||
key?: string, | ||
value?: string, | ||
region?: Region, | ||
) => { | ||
if (!appId || !mode) { | ||
throw new Error('appId and mode are required'); | ||
} | ||
|
||
const modeHandler = MAP_MODE_TO_HANDLER[mode]; | ||
if (!modeHandler) { | ||
throw new Error('invalid mode'); | ||
} | ||
|
||
await modeHandler(appId, region, key!, value!); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { z } from 'zod'; | ||
|
||
import { baseResponseHttpMetaDataSchema } from 'services/schemas/api-service-schemas'; | ||
|
||
export const listAppSecretKeysResponseSchema = z | ||
.object({ | ||
keys: z.array(z.string()), | ||
}) | ||
.merge(baseResponseHttpMetaDataSchema); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { APP_SECRET_MANAGEMENT_MODES } from 'consts/manage-app-secret'; | ||
import { AppId } from 'types/general'; | ||
|
||
export type ManageAppSecretFlags = { | ||
mode?: APP_SECRET_MANAGEMENT_MODES; | ||
key?: string; | ||
value?: string; | ||
appId?: AppId; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { z } from 'zod'; | ||
|
||
import { listAppSecretKeysResponseSchema } from 'services/schemas/manage-app-secret-service-schemas'; | ||
|
||
export type ListAppSecretKeysResponse = z.infer<typeof listAppSecretKeysResponseSchema>; |