From 1a2ff581eed2428c038d8f1fb7a7fa7dc84090c5 Mon Sep 17 00:00:00 2001 From: anabellabuckvar <41971124+anabellabuckvar@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:35:56 -0400 Subject: [PATCH] DOP-4481: Test-deploy all docs site button (#1022) * DOP-4490 scrappy changes * DOP-4490 slack changes * DOP-4490 deploy to preprd * DOP-4490 fixing nits * DOP-4490 fixing find options * DOP-4490-b new branch because of cdk error * DOP-4490-b commenting out * DOP-4490-b trying to put section back in * DOP-4490-b trying to put button back in * DOP-4490-b button back in * DOP-4490-b value out * DOP-4490-b value in * DOP-4490-b return value early * DOP-4490-b removing entire button section * DOP-4490-b removing entire button section * DOP-4490-b returning early * DOP-4490-b adding logging * DOP-4490-b return empty deploy repo * DOP-4490-b more logging * DOP-4490-b commenting out * DOP-4490-b logging values * DOP-4490-b add await * DOP-4490-b add button back in * DOP-4490-b add confirm modal * DOP-4490-b remove confirm modal * DOP-4490-b return from deployRepo early * DOP-4490-b return from deployRepo early, but later than before * DOP-4490-b return from deployRepo early, but later than before * DOP-4490-b return from deployRepo earlier * DOP-4490-b test conditional rendering of deploy button * DOP-4490-b test conditional rendering of deploy button * DOP-4490-b test conditional rendering if user is admin * DOP-4490-b test conditional rendering deploy button * DOP-4490-b test conditional rendering deploy button * DOP-4490-b parsing button response * DOP-4490-b fixing admin function * DOP-4490-b parsing button payload * DOP-4490-b parsing button payload * DOP-4490-b logging parsed values * DOP-4490-b logging parsed values * DOP-4490-b logging parsed values * DOP-4490-b logging parsed values * DOP-4490-b logging parsed values * DOP-4490-b changing button block type * DOP-4490-b changing button block type back * DOP-4490-b changing button block type to actions * DOP-4490-b changing button block type to actions * DOP-4490-b changing button block type to actions * DOP-4490-b testing radio button * DOP-4490-b updating radio button * DOP-4490-b prod deployable repo branches * DOP-4490-b parse selection correctly * DOP-4490-b logging getProdDeployableRepoBranches * DOP-4490-b logging getProdDeployableRepoBranches * DOP-4490-b undoing recent changes * DOP-4490-b readding some changes * DOP-4490-b undoing last change * DOP-4490-b changing parse function * DOP-4490-b adding back in * DOP-4490-b fix typo * DOP-4490-b cleaning up * DOP-4490-b logging type * DOP-4490-b logging type again * DOP-4490-b remove from preprd * DOP-4490-b repo selection optional * DOP-4490-b repo selection optional checks * DOP-4490-b redeploy to preprd * DOP-4490-b fix error catching * DOP-4490-b fix error catching * DOP-4490-b remove from preprd * DOP-4490-b nits * DOP-4490-b PR review nits * DOP-4490-b PR review nits * DOP-4490-b nits * DOP-4490-b change how errors are thrown * DOP-4490-b fix try catch for parsing selection --- api/controllers/v1/slack.ts | 85 +++++++++++------ api/controllers/v2/slack.ts | 15 ++- src/job/stagingJobHandler.ts | 2 +- src/repositories/repoBranchesRepository.ts | 9 +- .../repoEntitlementsRepository.ts | 9 ++ src/services/slack.ts | 92 ++++++++++++++++--- 6 files changed, 165 insertions(+), 47 deletions(-) diff --git a/api/controllers/v1/slack.ts b/api/controllers/v1/slack.ts index 6f208b7e2..45484e78a 100644 --- a/api/controllers/v1/slack.ts +++ b/api/controllers/v1/slack.ts @@ -4,6 +4,7 @@ import { RepoEntitlementsRepository } from '../../../src/repositories/repoEntitl import { RepoBranchesRepository } from '../../../src/repositories/repoBranchesRepository'; import { ConsoleLogger, ILogger } from '../../../src/services/logger'; import { SlackConnector } from '../../../src/services/slack'; +import { APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda'; import { JobRepository } from '../../../src/repositories/jobRepository'; import { buildEntitledBranchList, @@ -13,13 +14,23 @@ import { prepResponse, } from '../../handlers/slack'; import { DocsetsRepository } from '../../../src/repositories/docsetsRepository'; +import { Payload } from '../../../src/entities/job'; -export const DisplayRepoOptions = async (event: any = {}, context: any = {}): Promise => { +export const DisplayRepoOptions = async (event: APIGatewayEvent): Promise => { const consoleLogger = new ConsoleLogger(); const slackConnector = new SlackConnector(consoleLogger, c); + if (!slackConnector.validateSlackRequest(event)) { return prepResponse(401, 'text/plain', 'Signature Mismatch, Authentication Failed!'); } + + if (!event.body) { + return { + statusCode: 400, + body: 'Event body is undefined', + }; + } + const client = new mongodb.MongoClient(c.get('dbUrl')); await client.connect(); const db = client.db(process.env.DB_NAME); @@ -34,8 +45,11 @@ export const DisplayRepoOptions = async (event: any = {}, context: any = {}): Pr : 'User is not entitled!'; return prepResponse(401, 'text/plain', response); } + + const isAdmin = await repoEntitlementRepository.getIsAdmin(key_val['user_id']); + const entitledBranches = await buildEntitledBranchList(entitlement, repoBranchesRepository); - const resp = await slackConnector.displayRepoOptions(entitledBranches, key_val['trigger_id']); + const resp = await slackConnector.displayRepoOptions(entitledBranches, key_val['trigger_id'], isAdmin); if (resp?.status == 200 && resp?.data) { return { statusCode: 200, @@ -71,22 +85,29 @@ export const getDeployableJobs = async ( ) => { const deployable = []; - for (let i = 0; i < values.repo_option.length; i++) { - let repoOwner: string, repoName: string, branchName: string, directory: string | undefined; - const splitValues = values.repo_option[i].value.split('/'); - - if (splitValues.length === 3) { - // e.g. mongodb/docs-realm/master => (owner/repo/branch) - [repoOwner, repoName, branchName] = splitValues; - } else if (splitValues.length === 4 && process.env.FEATURE_FLAG_MONOREPO_PATH === 'true') { - // e.g. 10gen/docs-monorepo/cloud-docs/master => (owner/monorepo/repoDirectory/branch) - [repoOwner, repoName, directory, branchName] = splitValues; + for (let i = 0; i < values?.repo_option?.length; i++) { + let jobTitle: string, repoOwner: string, repoName: string, branchName: string, directory: string | undefined; + if (values.deploy_option == 'deploy_all') { + repoOwner = 'mongodb'; + branchName = 'master'; + repoName = values.repo_option[i].repoName; + jobTitle = `Slack deploy: ${repoOwner}/${repoName}/${branchName}, by ${entitlement.github_username}`; } else { - throw Error('Selected entitlement value is configured incorrectly. Check user entitlements!'); + const splitValues = values.repo_option[i].value.split('/'); + jobTitle = `Slack deploy: ${values.repo_option[i].value}, by ${entitlement.github_username}`; + + if (splitValues.length === 3) { + // e.g. mongodb/docs-realm/master => (owner/repo/branch) + [repoOwner, repoName, branchName] = splitValues; + } else if (splitValues.length === 4 && process.env.FEATURE_FLAG_MONOREPO_PATH === 'true') { + // e.g. 10gen/docs-monorepo/cloud-docs/master => (owner/monorepo/repoDirectory/branch) + [repoOwner, repoName, directory, branchName] = splitValues; + } else { + throw Error('Selected entitlement value is configured incorrectly. Check user entitlements!'); + } } const hashOption = values?.hash_option ?? null; - const jobTitle = `Slack deploy: ${values.repo_option[i].value}, by ${entitlement.github_username}`; const jobUserName = entitlement.github_username; const jobUserEmail = entitlement?.email ?? ''; @@ -96,12 +117,11 @@ export const getDeployableJobs = async ( const branchObject = await repoBranchesRepository.getRepoBranchAliases(repoName, branchName, repoInfo.project); if (!branchObject?.aliasObject) continue; - // TODO: Create strong typing for these rather than comments - const publishOriginalBranchName = branchObject.aliasObject.publishOriginalBranchName; // bool - let aliases = branchObject.aliasObject.urlAliases; // array or null - let urlSlug = branchObject.aliasObject.urlSlug; // string or null, string must match value in urlAliases or gitBranchName - const isStableBranch = branchObject.aliasObject.isStableBranch; // bool or Falsey - aliases = aliases?.filter((a) => a); + const publishOriginalBranchName: boolean = branchObject.aliasObject.publishOriginalBranchName; + const aliases: string[] | null = branchObject.aliasObject.urlAliases; + let urlSlug: string = branchObject.aliasObject.urlSlug; // string or null, string must match value in urlAliases or gitBranchName + const isStableBranch = !!branchObject.aliasObject.isStableBranch; // bool or Falsey, add strong typing + if (!urlSlug || !urlSlug.trim()) { urlSlug = branchName; } @@ -118,12 +138,10 @@ export const getDeployableJobs = async ( urlSlug, false, false, - false, + isStableBranch, directory ); - newPayload.stable = !!isStableBranch; - if (!aliases || aliases.length === 0) { if (non_versioned) { newPayload.urlSlug = ''; @@ -164,7 +182,7 @@ export const getDeployableJobs = async ( return deployable; }; -export const DeployRepo = async (event: any = {}, context: any = {}): Promise => { +export const DeployRepo = async (event: any = {}): Promise => { const consoleLogger = new ConsoleLogger(); const slackConnector = new SlackConnector(consoleLogger, c); if (!slackConnector.validateSlackRequest(event)) { @@ -173,6 +191,7 @@ export const DeployRepo = async (event: any = {}, context: any = {}): Promise 0) { await deployRepo(deployable, consoleLogger, jobRepository, c.get('jobsQueueUrl')); } @@ -235,7 +266,7 @@ function createPayload( }; } -function createJob(payload: any, jobTitle: string, jobUserName: string, jobUserEmail: string) { +function createJob(payload: Payload, jobTitle: string, jobUserName: string, jobUserEmail: string) { return { title: jobTitle, user: jobUserName, diff --git a/api/controllers/v2/slack.ts b/api/controllers/v2/slack.ts index b6d76f74a..fb3ef632f 100644 --- a/api/controllers/v2/slack.ts +++ b/api/controllers/v2/slack.ts @@ -20,6 +20,10 @@ export const DisplayRepoOptions = async (event: APIGatewayEvent): Promise 0) { diff --git a/src/job/stagingJobHandler.ts b/src/job/stagingJobHandler.ts index e543da9fb..df1d772b4 100644 --- a/src/job/stagingJobHandler.ts +++ b/src/job/stagingJobHandler.ts @@ -49,7 +49,7 @@ export class StagingJobHandler extends JobHandler { prepDeployCommands(): void { this.currJob.deployCommands = [ `cd repos/${getDirectory(this.currJob)}`, - `make next-gen-stage${this.currJob.payload.pathPrefix ? `MUT_PREFIX=${this.currJob.payload.mutPrefix}` : ''}`, + `make next-gen-stage${this.currJob.payload.pathPrefix ? ` MUT_PREFIX=${this.currJob.payload.mutPrefix}` : ''}`, ]; } diff --git a/src/repositories/repoBranchesRepository.ts b/src/repositories/repoBranchesRepository.ts index f2764c3ef..20bce0aeb 100644 --- a/src/repositories/repoBranchesRepository.ts +++ b/src/repositories/repoBranchesRepository.ts @@ -1,4 +1,4 @@ -import { Db } from 'mongodb'; +import { Document, Db } from 'mongodb'; import { BaseRepository } from './baseRepository'; import { ILogger } from '../services/logger'; import { IConfig } from 'config'; @@ -32,6 +32,13 @@ export class RepoBranchesRepository extends BaseRepository { return repo?.['branches'] ?? []; } + async getProdDeployableRepoBranches(): Promise { + const reposArray = await this._collection + .aggregate([{ $match: { prodDeployable: true, internalOnly: false } }, { $project: { _id: 0, repoName: 1 } }]) + .toArray(); + return reposArray ?? []; + } + async getRepoBranchAliases(repoName: string, branchName: string, project: string): Promise { const returnObject = { status: 'failure' }; const aliasArray = await this._collection diff --git a/src/repositories/repoEntitlementsRepository.ts b/src/repositories/repoEntitlementsRepository.ts index 2e73f8623..7cb5c8aaa 100644 --- a/src/repositories/repoEntitlementsRepository.ts +++ b/src/repositories/repoEntitlementsRepository.ts @@ -47,6 +47,15 @@ export class RepoEntitlementsRepository extends BaseRepository { } } + async getIsAdmin(slackUserId: string): Promise { + const query = { slack_user_id: slackUserId }; + const entitlementsObject = await this.findOne( + query, + `Mongo Timeout Error: Timedout while retrieving entitlements for ${slackUserId}` + ); + return entitlementsObject?.admin; + } + async getGatsbySiteIdByGithubUsername(githubUsername: string): Promise { return this.getBuildHookByGithubUsername(githubUsername, 'gatsby_site_id'); } diff --git a/src/services/slack.ts b/src/services/slack.ts index 4370a8500..20803d268 100644 --- a/src/services/slack.ts +++ b/src/services/slack.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import { ILogger } from './logger'; import { IConfig } from 'config'; import * as crypto from 'crypto'; +import { RepoBranchesRepository } from '../repositories/repoBranchesRepository'; export const axiosApi = axios.create(); function bufferEqual(a: Buffer, b: Buffer) { @@ -22,8 +23,8 @@ function timeSafeCompare(a: string, b: string) { export interface ISlackConnector { validateSlackRequest(payload: any): boolean; - displayRepoOptions(repos: Array, triggerId: string): Promise; - parseSelection(payload: any): any; + displayRepoOptions(repos: Array, triggerId: string, isAdmin: boolean): Promise; + parseSelection(payload: any, isAdmin: boolean, repoBranchesRepository: RepoBranchesRepository): any; sendMessage(message: any, user: string): Promise; } @@ -52,25 +53,44 @@ export class SlackConnector implements ISlackConnector { } return {}; } - parseSelection(stateValues: any): any { + + async parseSelection( + stateValues: any, + isAdmin: boolean, + repoBranchesRepository: RepoBranchesRepository + ): Promise { const values = {}; const inputMapping = { block_repo_option: 'repo_option', block_hash_option: 'hash_option', + block_deploy_option: 'deploy_option', }; + // if deploy all was selected: + if (stateValues['block_deploy_option']['deploy_option']?.selected_option?.value == 'deploy_all') { + if (!isAdmin) { + throw new Error('User is not an admin and therefore not entitled to deploy all repos'); + } + + values['deploy_option'] = 'deploy_all'; + values['repo_option'] = await repoBranchesRepository.getProdDeployableRepoBranches(); + return values; + } + + //if deploy indivual repos was selected: // get key and values to figure out what user wants to deploy for (const blockKey in inputMapping) { const blockInputKey = inputMapping[blockKey]; const stateValuesObj = stateValues[blockKey][blockInputKey]; - // selected value from dropdown - if (stateValuesObj?.selected_option?.value) { - values[blockInputKey] = stateValuesObj.selected_option.value; - } + // multi select is an array - else if (stateValuesObj?.selected_options && stateValuesObj.selected_options.length > 0) { + if (stateValuesObj?.selected_options?.length > 0) { values[blockInputKey] = stateValuesObj.selected_options; } + //return an error if radio button choice 'deploy individual repos' was selected but no repo was actually chosen + else if (blockInputKey == 'repo_option') { + throw new Error('Deploy individual repos was selected but no repo was actually chosen to be deployed'); + } // input value else if (stateValuesObj?.value) { values[blockInputKey] = stateValuesObj.value; @@ -99,8 +119,9 @@ export class SlackConnector implements ISlackConnector { return false; } - async displayRepoOptions(repos: string[], triggerId: string): Promise { - const repoOptView = this._buildDropdown(repos, triggerId); + async displayRepoOptions(repos: string[], triggerId: string, isAdmin: boolean): Promise { + const reposToShow = this._buildDropdown(repos); + const repoOptView = this._getDropDownView(triggerId, reposToShow, isAdmin); const slackToken = this._config.get('slackAuthToken'); const slackUrl = this._config.get('slackViewOpenUrl'); return await axiosApi.post(slackUrl, repoOptView, { @@ -110,8 +131,51 @@ export class SlackConnector implements ISlackConnector { }, }); } + private _getDropDownView(triggerId: string, repos: Array, isAdmin: boolean) { + const deployAll = isAdmin + ? { + type: 'section', + block_id: 'block_deploy_option', + text: { + type: 'plain_text', + text: 'How would you like to deploy docs sites?', + }, + accessory: { + type: 'radio_buttons', + action_id: 'deploy_option', + initial_option: { + value: 'deploy_individually', + text: { + type: 'plain_text', + text: 'Deploy individual repos', + }, + }, + options: [ + { + value: 'deploy_individually', + text: { + type: 'plain_text', + text: 'Deploy individual repos', + }, + }, + { + value: 'deploy_all', + text: { + type: 'plain_text', + text: 'Deploy all repos', + }, + }, + ], + }, + } + : { + type: 'section', + text: { + type: 'plain_text', + text: ' ', + }, + }; - private _getDropDownView(triggerId: string, repos: Array) { return { trigger_id: triggerId, view: { @@ -145,6 +209,7 @@ export class SlackConnector implements ISlackConnector { }, options: repos, }, + optional: true, label: { type: 'plain_text', text: 'Select Repo', @@ -168,12 +233,13 @@ export class SlackConnector implements ISlackConnector { text: 'Commit Hash', }, }, + deployAll, ], }, }; } - private _buildDropdown(branches: Array, triggerId: string): any { + private _buildDropdown(branches: Array): Array { let reposToShow: Array = []; branches.forEach((fullPath) => { const displayBranchPath = fullPath; @@ -208,6 +274,6 @@ export class SlackConnector implements ISlackConnector { .localeCompare(a.text.text.toString().replace(/\d+/g, (n) => +n + 100000)); }); - return this._getDropDownView(triggerId, reposToShow); + return reposToShow; } }