Skip to content

Commit

Permalink
Added the ability to work with secrets in the CLI. set, delete and ge…
Browse files Browse the repository at this point in the history
…t list of all secrets keys, per region.
  • Loading branch information
Shaharshaki2 committed Sep 8, 2024
1 parent 08d710e commit 97247b5
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 0 deletions.
127 changes: 127 additions & 0 deletions src/commands/code/secret.ts
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")

Check failure on line 116 in src/commands/code/secret.ts

View workflow job for this annotation

GitHub Actions / Run validations

Replace `"1")` with `'1');`

await handleSecretRequest(appId, mode, key, value, selectedRegion);
console.log("4")

Check failure on line 119 in src/commands/code/secret.ts

View workflow job for this annotation

GitHub Actions / Run validations

Replace `"4")` with `'4');`
} catch (error: any) {
logger.debug(error, this.DEBUG_TAG);

// need to signal to the parent process that the command failed
process.exit(1);
}
}
}
5 changes: 5 additions & 0 deletions src/consts/manage-app-secret.ts
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',
}
8 changes: 8 additions & 0 deletions src/consts/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ export const appEnvironmentKeysUrl = (appId: AppId): string => {
return `/api/code/${appId}/env-keys`;
};

export const appSecretUrl = (appId: AppId, key: string): string => {
return `/api/code/${appId}/secrets/${key}`;
};

export const appSecretKeysUrl = (appId: AppId): string => {
return `/api/code/${appId}/secret-keys`;
};

export const appReleasesUrl = (appVersionId: AppId): string => {
return `/apps_ms/app-versions/${appVersionId}/releases`;
};
Expand Down
157 changes: 157 additions & 0 deletions src/services/manage-app-secret-service.ts
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,
) => {

Check warning on line 146 in src/services/manage-app-secret-service.ts

View workflow job for this annotation

GitHub Actions / Run validations

Async arrow function has too many parameters (5). Maximum allowed is 4
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!);
};
9 changes: 9 additions & 0 deletions src/services/schemas/manage-app-secret-service-schemas.ts
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);
9 changes: 9 additions & 0 deletions src/types/commands/manage-app-secret.ts
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;
};
5 changes: 5 additions & 0 deletions src/types/services/manage-app-secret-service.ts
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>;

0 comments on commit 97247b5

Please sign in to comment.