From 2da58ac2b870e11d38e2353dab261ea19992dad5 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Fri, 20 Mar 2020 18:34:59 -0700 Subject: [PATCH 01/15] create storage account --- src/commands/setup.test.ts | 3 ++ src/commands/setup.ts | 2 + src/lib/azure/storage.ts | 1 - src/lib/promptBuilder.ts | 13 +++++ src/lib/setup/azureStorage.test.ts | 67 ++++++++++++++++++++++++ src/lib/setup/azureStorage.ts | 41 +++++++++++++++ src/lib/setup/constants.ts | 3 ++ src/lib/setup/prompt.test.ts | 83 ++++++++++++++++++++++++++++-- src/lib/setup/prompt.ts | 13 ++++- src/lib/setup/setupLog.test.ts | 9 ++++ src/lib/setup/setupLog.ts | 4 +- 11 files changed, 231 insertions(+), 8 deletions(-) create mode 100644 src/lib/setup/azureStorage.test.ts create mode 100644 src/lib/setup/azureStorage.ts diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index 67a34d58e..f9f143b14 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -14,6 +14,7 @@ import * as projectService from "../lib/setup/projectService"; import * as promptInstance from "../lib/setup/prompt"; import * as scaffold from "../lib/setup/scaffold"; import * as setupLog from "../lib/setup/setupLog"; +import * as azureStorage from "../lib/setup/azureStorage"; import { deepClone } from "../lib/util"; import { ConfigYaml } from "../types"; import { @@ -324,10 +325,12 @@ const testCreateAppRepoTasks = async (prApproved = true): Promise => { servicePrincipalTenantId: "tenant", subscriptionId: "12344", acrName: "acr", + storageAccountName: "storage", workspace: "dummy" }; jest.spyOn(resourceService, "create").mockResolvedValueOnce(true); + jest.spyOn(azureStorage, "createStorage").mockResolvedValueOnce(); jest .spyOn(azureContainerRegistryService, "create") .mockResolvedValueOnce(true); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 484b78e8c..1a62d82e7 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -37,6 +37,7 @@ import { import { create as createSetupLog } from "../lib/setup/setupLog"; import { logger } from "../logger"; import decorator from "./setup.decorator.json"; +import { createStorage } from "../lib/setup/azureStorage"; interface CommandOptions { file: string | undefined; @@ -115,6 +116,7 @@ export const createAppRepoTasks = async ( RESOURCE_GROUP, RESOURCE_GROUP_LOCATION ); + await createStorage(rc); rc.createdACR = await createACR( rc.servicePrincipalId, rc.servicePrincipalPassword, diff --git a/src/lib/azure/storage.ts b/src/lib/azure/storage.ts index d376a4830..436803e8b 100644 --- a/src/lib/azure/storage.ts +++ b/src/lib/azure/storage.ts @@ -467,7 +467,6 @@ export const createTableIfNotExists = ( * * @param name The Azure resource group name * @param location The Azure resource group location - * */ export const createResourceGroupIfNotExists = async ( name: string, diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts index d47715c33..9b8edfbea 100644 --- a/src/lib/promptBuilder.ts +++ b/src/lib/promptBuilder.ts @@ -179,3 +179,16 @@ export const azureStorageAccessKey = ( validate: validator.validateStorageAccessKey }; }; + +export const storageAccountName = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + mask: "*", + message: `${i18n.prompt.storageAccessKey}\n`, + name: "azdo_storage_account_name", + type: "password", + validate: validator.validateStorageAccessKey + }; +}; diff --git a/src/lib/setup/azureStorage.test.ts b/src/lib/setup/azureStorage.test.ts new file mode 100644 index 000000000..c93773695 --- /dev/null +++ b/src/lib/setup/azureStorage.test.ts @@ -0,0 +1,67 @@ +import inquirer from "inquirer"; +import { createStorage, createStorageAccount } from "./azureStorage"; +import * as azureStorage from "./azureStorage"; +import { RequestContext } from "./constants"; +import * as azure from "../azure/storage"; + +describe("test createStorage function", () => { + it("positive test", async () => { + jest + .spyOn(azureStorage, "createStorageAccount") + .mockResolvedValueOnce(true); + const rc: RequestContext = { + orgName: "notUsed", + projectName: "notUsed", + accessToken: "notUsed", + workspace: "notUsed" + }; + await createStorage(rc); + expect(rc.createdStorageAccount).toBe(true); + }); + it("positive test: name used", async () => { + jest + .spyOn(azureStorage, "createStorageAccount") + .mockResolvedValueOnce(undefined); + jest + .spyOn(azureStorage, "createStorageAccount") + .mockResolvedValueOnce(true); + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + // eslint-disable-next-line @typescript-eslint/camelcase + azdo_storage_account_name: "teststore" + }); + const rc: RequestContext = { + orgName: "notUsed", + projectName: "notUsed", + accessToken: "notUsed", + workspace: "notUsed" + }; + await createStorage(rc); + expect(rc.createdStorageAccount).toBe(true); + expect(rc.storageAccountName).toBe("teststore"); + }); +}); + +describe("test createStorageAccount function", () => { + it("positive test: account already exist", async () => { + jest.spyOn(azure, "isStorageAccountExist").mockResolvedValueOnce(true); + const result = await createStorageAccount("temp"); + expect(result).toBeFalsy(); + }); + it("positive test: account doe not exist", async () => { + jest.spyOn(azure, "isStorageAccountExist").mockResolvedValueOnce(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + jest.spyOn(azure, "createStorageAccount").mockResolvedValueOnce({} as any); + + const result = await createStorageAccount("temp"); + expect(result).toBeTruthy(); + }); + it("negative test", async () => { + jest.spyOn(azure, "isStorageAccountExist").mockResolvedValueOnce(false); + jest + .spyOn(azure, "createStorageAccount") + .mockRejectedValueOnce(Error("fake")); + + const result = await createStorageAccount("temp"); + expect(result).toBeUndefined(); + }); +}); diff --git a/src/lib/setup/azureStorage.ts b/src/lib/setup/azureStorage.ts new file mode 100644 index 000000000..e86793f55 --- /dev/null +++ b/src/lib/setup/azureStorage.ts @@ -0,0 +1,41 @@ +import { + RequestContext, + RESOURCE_GROUP, + STORAGE_ACCOUNT_NAME, + RESOURCE_GROUP_LOCATION +} from "./constants"; +import { + createStorageAccount as createAccount, + isStorageAccountExist +} from "../azure/storage"; +import * as promptBuilder from "../promptBuilder"; +import inquirer from "inquirer"; + +export const createStorageAccount = async ( + name: string +): Promise => { + const oStorage = await isStorageAccountExist(RESOURCE_GROUP, name, {}); + if (!oStorage) { + try { + await createAccount(RESOURCE_GROUP, name, RESOURCE_GROUP_LOCATION); + return true; + } catch (e) { + return undefined; + } + } else { + return false; + } +}; + +export const createStorage = async (rc: RequestContext): Promise => { + rc.storageAccountName = rc.storageAccountName || STORAGE_ACCOUNT_NAME; + let res = await createStorageAccount(rc.storageAccountName); + while (res === undefined) { + const ans = await inquirer.prompt([ + promptBuilder.azureStorageAccountName() + ]); + rc.storageAccountName = ans.azdo_storage_account_name as string; + res = await createStorageAccount(rc.storageAccountName); + } + rc.createdStorageAccount = !!res; +}; diff --git a/src/lib/setup/constants.ts b/src/lib/setup/constants.ts index f246db274..9011a4f03 100644 --- a/src/lib/setup/constants.ts +++ b/src/lib/setup/constants.ts @@ -4,6 +4,7 @@ export interface RequestContext { accessToken: string; workspace: string; acrName?: string; + storageAccountName?: string; toCreateAppRepo?: boolean; toCreateSP?: boolean; createdProject?: boolean; @@ -15,6 +16,7 @@ export interface RequestContext { createdLifecyclePipeline?: boolean; createdBuildPipeline?: boolean; createServicePrincipal?: boolean; + createdStorageAccount?: boolean; servicePrincipalId?: string; servicePrincipalPassword?: string; servicePrincipalTenantId?: string; @@ -37,6 +39,7 @@ export const RESOURCE_GROUP_LOCATION = "westus2"; export const ACR_NAME = "quickStartACR"; export const VARIABLE_GROUP = "quick-start-vg"; export const APP_REPO_BUILD = "quick-start-app-build"; +export const STORAGE_ACCOUNT_NAME = "quickstartstore"; export const SETUP_LOG = "setup.log"; export const HLD_DEFAULT_COMPONENT_NAME = "traefik2"; diff --git a/src/lib/setup/prompt.test.ts b/src/lib/setup/prompt.test.ts index 07b5c513a..7e46d2b32 100644 --- a/src/lib/setup/prompt.test.ts +++ b/src/lib/setup/prompt.test.ts @@ -12,7 +12,8 @@ import { prompt, promptForACRName, promptForApprovingHLDPullRequest, - promptForServicePrincipalCreation + promptForServicePrincipalCreation, + validationServicePrincipalInfoFromFile } from "./prompt"; import * as promptInstance from "./prompt"; import * as gitService from "./gitService"; @@ -136,18 +137,24 @@ describe("test getAnswerFromFile function", () => { const data = [ "azdo_org_name=orgname", "azdo_pat=pat", - "azdo_project_name=project" + "azdo_project_name=project", + "az_storage_account_name=teststore" ]; fs.writeFileSync(file, data.join("\n")); const requestContext = getAnswerFromFile(file); expect(requestContext.orgName).toBe("orgname"); expect(requestContext.accessToken).toBe("pat"); expect(requestContext.projectName).toBe("project"); + expect(requestContext.storageAccountName).toBe("teststore"); }); it("positive test: without project name", () => { const dir = createTempDir(); const file = path.join(dir, "testfile"); - const data = ["azdo_org_name=orgname", "azdo_pat=pat"]; + const data = [ + "azdo_org_name=orgname", + "azdo_pat=pat", + "az_storage_account_name=teststore" + ]; fs.writeFileSync(file, data.join("\n")); const requestContext = getAnswerFromFile(file); expect(requestContext.orgName).toBe("orgname"); @@ -202,7 +209,8 @@ describe("test getAnswerFromFile function", () => { "az_sp_id=b510c1ff-358c-4ed4-96c8-eb23f42bb65b", "az_sp_password=a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", "az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47", - "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48" + "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48", + "az_storage_account_name=teststore" ]; fs.writeFileSync(file, data.join("\n")); const requestContext = getAnswerFromFile(file); @@ -390,3 +398,70 @@ describe("test promptForApprovingHLDPullRequest function", () => { expect(ans).toBeTruthy(); }); }); + +const testValidationServicePrincipalInfoFromFile = (vals: { + [key: string]: string; +}): void => { + validationServicePrincipalInfoFromFile( + { + orgName: "orgName", + projectName: "project", + accessToken: "notuse", + workspace: "notused", + toCreateAppRepo: true, + toCreateSP: false + }, + vals + ); +}; + +describe("test validationServicePrincipalInfoFromFile function", () => { + it("positive test", () => { + testValidationServicePrincipalInfoFromFile({ + az_sp_id: "f2f988bf-86f1-41af-91ab-2d7cd011db48", + az_sp_password: "d2f988bf-86f1-41af-91ab-2d7cd011db48", + az_sp_tenant: "b2f988bf-86f1-41af-91ab-2d7cd011db48", + az_subscription_id: "a2f988bf-86f1-41af-91ab-2d7cd011db45" + }); + }); + it("negative test: sp id is invalid", () => { + expect(() => { + testValidationServicePrincipalInfoFromFile({ + az_sp_id: "id", + az_sp_password: "d2f988bf-86f1-41af-91ab-2d7cd011db48", + az_sp_tenant: "b2f988bf-86f1-41af-91ab-2d7cd011db48", + az_subscription_id: "a2f988bf-86f1-41af-91ab-2d7cd011db45" + }); + }).toThrow(); + }); + it("negative test: sp password is invalid", () => { + expect(() => { + testValidationServicePrincipalInfoFromFile({ + az_sp_id: "f2f988bf-86f1-41af-91ab-2d7cd011db48", + az_sp_password: "pwd", + az_sp_tenant: "b2f988bf-86f1-41af-91ab-2d7cd011db48", + az_subscription_id: "a2f988bf-86f1-41af-91ab-2d7cd011db45" + }); + }).toThrow(); + }); + it("negative test: sp id is invalid", () => { + expect(() => { + testValidationServicePrincipalInfoFromFile({ + az_sp_id: "f2f988bf-86f1-41af-91ab-2d7cd011db48", + az_sp_password: "d2f988bf-86f1-41af-91ab-2d7cd011db48", + az_sp_tenant: "tenant", + az_subscription_id: "a2f988bf-86f1-41af-91ab-2d7cd011db45" + }); + }).toThrow(); + }); + it("negative test: sp id is invalid", () => { + expect(() => { + testValidationServicePrincipalInfoFromFile({ + az_sp_id: "f2f988bf-86f1-41af-91ab-2d7cd011db48", + az_sp_password: "d2f988bf-86f1-41af-91ab-2d7cd011db48", + az_sp_tenant: "b2f988bf-86f1-41af-91ab-2d7cd011db48", + az_subscription_id: "subid" + }); + }).toThrow(); + }); +}); diff --git a/src/lib/setup/prompt.ts b/src/lib/setup/prompt.ts index 1fb7c458a..1f1747ac8 100644 --- a/src/lib/setup/prompt.ts +++ b/src/lib/setup/prompt.ts @@ -9,7 +9,8 @@ import { validateServicePrincipalId, validateServicePrincipalPassword, validateServicePrincipalTenantId, - validateSubscriptionId + validateSubscriptionId, + validateStorageAccountName } from "../validator"; import { ACR_NAME, @@ -170,7 +171,7 @@ export const prompt = async (): Promise => { return rc; }; -const validationServicePrincipalInfoFromFile = ( +export const validationServicePrincipalInfoFromFile = ( rc: RequestContext, map: { [key: string]: string } ): void => { @@ -247,6 +248,13 @@ export const getAnswerFromFile = (file: string): RequestContext => { throw new Error(vToken); } + const vStorageAccountName = validateStorageAccountName( + map.az_storage_account_name + ); + if (typeof vStorageAccountName === "string") { + throw new Error(vStorageAccountName); + } + const rc: RequestContext = { accessToken: map.azdo_pat, orgName: map.azdo_org_name, @@ -255,6 +263,7 @@ export const getAnswerFromFile = (file: string): RequestContext => { servicePrincipalPassword: map.az_sp_password, servicePrincipalTenantId: map.az_sp_tenant, acrName: map.az_acr_name || ACR_NAME, + storageAccountName: map.az_storage_account_name, workspace: WORKSPACE }; diff --git a/src/lib/setup/setupLog.test.ts b/src/lib/setup/setupLog.test.ts index ca30c9d69..b5259e4e4 100644 --- a/src/lib/setup/setupLog.test.ts +++ b/src/lib/setup/setupLog.test.ts @@ -36,10 +36,13 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { (rc.servicePrincipalId = "b510c1ff-358c-4ed4-96c8-eb23f42bb65b"); rc.servicePrincipalPassword = "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b"; rc.servicePrincipalTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; + rc.storageAccountName = "teststore"; rc.createdResourceGroup = true; rc.createdACR = true; rc.createdLifecyclePipeline = true; rc.createdBuildPipeline = true; + rc.createdStorageAccount = true; + rc.createdStorageAccount = true; } create(rc, file); @@ -57,6 +60,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47", "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48", "az_acr_name=testacr", + "az_storage_account_name=teststore", "workspace: workspace", "Project Created: yes", "High Level Definition Repo Scaffolded: yes", @@ -69,6 +73,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "Lifecycle Pipeline Created: yes", "Build Pipeline Created: yes", "ACR Created: yes", + "Storage Account Created: yes", "Status: Completed" ]); } else { @@ -83,6 +88,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "az_sp_tenant=", "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48", "az_acr_name=testacr", + "az_storage_account_name=", "workspace: workspace", "Project Created: yes", "High Level Definition Repo Scaffolded: yes", @@ -95,6 +101,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "Lifecycle Pipeline Created: no", "Build Pipeline Created: no", "ACR Created: no", + "Storage Account Created: no", "Status: Completed" ]); } @@ -149,6 +156,7 @@ describe("test create function", () => { "az_sp_tenant=", "az_subscription_id=", "az_acr_name=", + "az_storage_account_name=", "workspace: workspace", "Project Created: yes", "High Level Definition Repo Scaffolded: yes", @@ -161,6 +169,7 @@ describe("test create function", () => { "Lifecycle Pipeline Created: no", "Build Pipeline Created: no", "ACR Created: no", + "Storage Account Created: no", "Error: things broke", "Status: Incomplete" ]); diff --git a/src/lib/setup/setupLog.ts b/src/lib/setup/setupLog.ts index 61d93e610..e31c46c0e 100644 --- a/src/lib/setup/setupLog.ts +++ b/src/lib/setup/setupLog.ts @@ -22,6 +22,7 @@ export const create = (rc: RequestContext | undefined, file?: string): void => { `az_sp_tenant=${rc.servicePrincipalTenantId || ""}`, `az_subscription_id=${rc.subscriptionId || ""}`, `az_acr_name=${rc.acrName || ""}`, + `az_storage_account_name=${rc.storageAccountName || ""}`, `workspace: ${rc.workspace}`, `Project Created: ${getBooleanVal(rc.createdProject)}`, `High Level Definition Repo Scaffolded: ${getBooleanVal(rc.scaffoldHLD)}`, @@ -37,7 +38,8 @@ export const create = (rc: RequestContext | undefined, file?: string): void => { rc.createdLifecyclePipeline )}`, `Build Pipeline Created: ${getBooleanVal(rc.createdBuildPipeline)}`, - `ACR Created: ${getBooleanVal(rc.createdACR)}` + `ACR Created: ${getBooleanVal(rc.createdACR)}`, + `Storage Account Created: ${getBooleanVal(rc.createdStorageAccount)}` ]; if (rc.error) { buff.push(`Error: ${rc.error}`); From c209b63db615ae8228d1dfe56ff4cd37dc364450 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sat, 21 Mar 2020 16:51:22 -0700 Subject: [PATCH 02/15] stage --- src/commands/setup.test.ts | 24 +++- src/commands/setup.ts | 65 ++++++++--- src/lib/azure/storage.test.ts | 169 ++++++++++++---------------- src/lib/azure/storage.ts | 4 +- src/lib/setup/azureStorage.test.ts | 101 +++++++++++++++-- src/lib/setup/azureStorage.ts | 39 ++++++- src/lib/setup/constants.ts | 5 + src/lib/setup/prompt.test.ts | 9 +- src/lib/setup/prompt.ts | 9 +- src/lib/setup/scaffold.test.ts | 3 +- src/lib/setup/scaffold.ts | 13 +-- src/lib/setup/setupLog.test.ts | 9 +- src/lib/setup/setupLog.ts | 4 +- src/lib/setup/variableGroup.test.ts | 53 +++++++++ src/lib/setup/variableGroup.ts | 78 +++++++++++++ 15 files changed, 438 insertions(+), 147 deletions(-) create mode 100644 src/lib/setup/variableGroup.test.ts create mode 100644 src/lib/setup/variableGroup.ts diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index f9f143b14..6be37de87 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -49,13 +49,29 @@ describe("test createSPKConfig function", () => { project: "project" }); }); + it("positive test with toCreateAppRepo = false", () => { + const tmpFile = path.join(createTempDir(), "config.yaml"); + jest.spyOn(config, "defaultConfigFile").mockReturnValueOnce(tmpFile); + const oData = deepClone(mockRequestContext); + oData.toCreateAppRepo = false; + createSPKConfig(oData); + const data = readYaml(tmpFile); + expect(data.azure_devops).toStrictEqual({ + access_token: "pat", + org: "orgname", + project: "project" + }); + }); it("positive test: with service principal", () => { const tmpFile = path.join(createTempDir(), "config.yaml"); jest.spyOn(config, "defaultConfigFile").mockReturnValueOnce(tmpFile); const rc: RequestContext = deepClone(mockRequestContext); rc.toCreateAppRepo = true; rc.toCreateSP = true; - rc.servicePrincipalId = "1eba2d04-1506-4278-8f8c-b1eb2fc462a8"; + (rc.storageAccountName = "storageAccount"), + (rc.storageTableName = "storageTable"), + (rc.storageAccountAccessKey = "storageAccessKey"), + (rc.servicePrincipalId = "1eba2d04-1506-4278-8f8c-b1eb2fc462a8"); rc.servicePrincipalPassword = "e4c19d72-96d6-4172-b195-66b3b1c36db1"; rc.servicePrincipalTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; rc.subscriptionId = "72f988bf-86f1-41af-91ab-2d7cd011db48"; @@ -72,7 +88,11 @@ describe("test createSPKConfig function", () => { service_principal_id: rc.servicePrincipalId, service_principal_secret: rc.servicePrincipalPassword, subscription_id: rc.subscriptionId, - tenant_id: rc.servicePrincipalTenantId + tenant_id: rc.servicePrincipalTenantId, + account_name: "storageAccount", + table_name: "storageTable", + key: "storageAccessKey", + partition_key: "quick-start-part-key" } }); }); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 1a62d82e7..6ba593b9b 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -13,6 +13,7 @@ import { RequestContext, RESOURCE_GROUP, RESOURCE_GROUP_LOCATION, + STORAGE_PARTITION_KEY, WORKSPACE } from "../lib/setup/constants"; import { createDirectory } from "../lib/setup/fsUtil"; @@ -38,6 +39,7 @@ import { create as createSetupLog } from "../lib/setup/setupLog"; import { logger } from "../logger"; import decorator from "./setup.decorator.json"; import { createStorage } from "../lib/setup/azureStorage"; +import { ConfigYaml } from "../types"; interface CommandOptions { file: string | undefined; @@ -54,30 +56,56 @@ interface APIError { * @param answers Answers provided to the commander */ export const createSPKConfig = (rc: RequestContext): void => { - const data = rc.toCreateAppRepo - ? { + if (!rc.toCreateAppRepo) { + fs.writeFileSync( + defaultConfigFile(), + yaml.safeDump({ azure_devops: { access_token: rc.accessToken, org: rc.orgName, project: rc.projectName - }, - introspection: { - azure: { - service_principal_id: rc.servicePrincipalId, - service_principal_secret: rc.servicePrincipalPassword, - subscription_id: rc.subscriptionId, - tenant_id: rc.servicePrincipalTenantId - } } + }) + ); + return; + } + + const data: ConfigYaml = { + azure_devops: { + access_token: rc.accessToken, + org: rc.orgName, + project: rc.projectName + }, + introspection: { + azure: { + service_principal_id: rc.servicePrincipalId, + service_principal_secret: rc.servicePrincipalPassword, + subscription_id: rc.subscriptionId, + tenant_id: rc.servicePrincipalTenantId } - : { - azure_devops: { - access_token: rc.accessToken, - org: rc.orgName, - project: rc.projectName - } - }; - fs.writeFileSync(defaultConfigFile(), yaml.safeDump(data)); + } + }; + + if (data.introspection && data.introspection.azure) { + const azure = data.introspection.azure; + if (rc.storageAccountName) { + azure.account_name = rc.storageAccountName; + } + if (rc.storageAccountAccessKey) { + azure.key = rc.storageAccountAccessKey; + } + if (rc.storageTableName) { + azure.table_name = rc.storageTableName; + } + azure.partition_key = STORAGE_PARTITION_KEY; + } + + fs.writeFileSync( + defaultConfigFile(), + yaml.safeDump(data, { + lineWidth: 5000 + }) + ); }; export const getErrorMessage = ( @@ -174,6 +202,7 @@ export const execute = async ( await createHLDtoManifestPipeline(buildAPI, rc); await createAppRepoTasks(gitAPI, buildAPI, rc); + createSPKConfig(rc); // to write storage account information. createSetupLog(rc); await exitFn(0); } catch (err) { diff --git a/src/lib/azure/storage.test.ts b/src/lib/azure/storage.test.ts index 3754bf648..d3e589a22 100644 --- a/src/lib/azure/storage.test.ts +++ b/src/lib/azure/storage.test.ts @@ -1,12 +1,11 @@ /* eslint-disable @typescript-eslint/camelcase */ -// Mocks + jest.mock("@azure/arm-storage"); jest.mock("azure-storage"); jest.mock("../../config"); import uuid from "uuid/v4"; import { disableVerboseLogging, enableVerboseLogging } from "../../logger"; -import { StorageAccount } from "@azure/arm-storage/esm/models"; import { Config } from "../../config"; import { getStorageAccount, validateStorageAccount } from "./storage"; import * as storage from "./storage"; @@ -30,37 +29,33 @@ const mockGetStorageManagementClient = ( nameAvaible = true, hasResourceGroups = true ): void => { - jest.spyOn(storage, "getStorageManagementClient").mockImplementationOnce( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (): Promise => { - return { - storageAccounts: { - checkNameAvailability: (): unknown => { - return { nameAvailable: nameAvaible }; - }, - listByResourceGroup: (): unknown => { - if (hasResourceGroups) { - return [ - { name: "testAccountName" }, - { name: "otherTestAccountName" } - ]; - } - return undefined; - }, - listKeys: (): unknown => { - return {}; - }, - create(): Promise { - return new Promise(resolve => { - resolve({ - status: "created" - }); - }); - } + jest.spyOn(storage, "getStorageManagementClient").mockResolvedValueOnce({ + storageAccounts: { + checkNameAvailability: (): unknown => { + return { nameAvailable: nameAvaible }; + }, + listByResourceGroup: (): unknown => { + if (hasResourceGroups) { + return [ + { name: "testAccountName" }, + { name: "otherTestAccountName" } + ]; } - }; + return undefined; + }, + listKeys: (): unknown => { + return {}; + }, + create(): Promise { + return new Promise(resolve => { + resolve({ + status: "created" + }); + }); + } } - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); }; beforeAll(() => { @@ -101,35 +96,27 @@ describe("get storage account key", () => { await expect(storage.getStorageAccountKey("", "")).rejects.toThrow(); }); test("negative test", async () => { - jest.spyOn(storage, "getStorageManagementClient").mockImplementationOnce( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (): Promise => { - return { - storageAccounts: { - listKeys: (): never => { - throw Error("fake"); - } - } - }; + jest.spyOn(storage, "getStorageManagementClient").mockResolvedValueOnce({ + storageAccounts: { + listKeys: (): never => { + throw Error("fake"); + } } - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); await expect( storage.getStorageAccountKey(resourceGroupName, storageAccountName) ).rejects.toThrow(); }); test("negative test: key not found", async () => { - jest.spyOn(storage, "getStorageManagementClient").mockImplementationOnce( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (): Promise => { - return { - storageAccounts: { - listKeys: (): unknown => { - return {}; - } - } - }; + jest.spyOn(storage, "getStorageManagementClient").mockResolvedValueOnce({ + storageAccounts: { + listKeys: (): unknown => { + return {}; + } } - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); const key = await storage.getStorageAccountKey( "testResourceGroup", "testAccountName" @@ -137,18 +124,14 @@ describe("get storage account key", () => { expect(key).toBeUndefined(); }); test("get storage account key", async () => { - jest.spyOn(storage, "getStorageManagementClient").mockImplementationOnce( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (): Promise => { - return { - storageAccounts: { - listKeys: (): unknown => { - return { keys: [{ value: "testkey" }] }; - } - } - }; + jest.spyOn(storage, "getStorageManagementClient").mockResolvedValueOnce({ + storageAccounts: { + listKeys: (): unknown => { + return { keys: [{ value: "testkey" }] }; + } } - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); const key = await storage.getStorageAccountKey( "testResourceGroup", "testAccountName" @@ -161,6 +144,7 @@ describe("create resource group", () => { test("invalid name", async () => { try { await storage.createResourceGroupIfNotExists("", "westus"); + expect(true).toBe(false); } catch (err) { expect(err.message).toEqual("\nInvalid name"); } @@ -169,6 +153,7 @@ describe("create resource group", () => { test("invalid location", async () => { try { await storage.createResourceGroupIfNotExists("testResourceGroup", ""); + expect(true).toBe(false); } catch (err) { expect(err.message).toEqual("\nInvalid location"); } @@ -179,6 +164,7 @@ describe("get storage account keys", () => { test("invalid account name", async () => { try { await storage.getStorageAccountKeys("", "resourceGroup"); + expect(true).toBe(false); } catch (err) { expect(err.message).toEqual("\nInvalid accountName"); } @@ -187,24 +173,21 @@ describe("get storage account keys", () => { test("invalid resource group", async () => { try { await storage.getStorageAccountKeys("accountName", ""); + expect(true).toBe(false); } catch (err) { expect(err.message).toEqual("\nInvalid resourceGroup"); } }); test("should get storage account key", async () => { - jest.spyOn(storage, "getStorageManagementClient").mockImplementationOnce( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (): Promise => { - return { - storageAccounts: { - listKeys: (): unknown => { - return { keys: [{ value: "testkey" }] }; - } - } - }; + jest.spyOn(storage, "getStorageManagementClient").mockResolvedValueOnce({ + storageAccounts: { + listKeys: (): unknown => { + return { keys: [{ value: "testkey" }] }; + } } - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); const keys = await storage.getStorageAccountKeys( "accountName", "resourceGroup" @@ -237,29 +220,23 @@ describe("storage account name available", () => { describe("storage account exists", () => { test("invalid resource group", async () => { - jest.spyOn(storage, "getStorageAccount").mockImplementationOnce( - async (): Promise => { - return { - enableHttpsTrafficOnly: true, - location: "uswest" - }; - } - ); + jest.spyOn(storage, "getStorageAccount").mockResolvedValueOnce({ + enableHttpsTrafficOnly: true, + location: "uswest" + }); try { await storage.isStorageAccountExist("", "testAccountName"); + expect(true).toBe(false); } catch (err) { expect(err.message).toEqual("\nInvalid resourceGroup"); } }); test("invalid account name", async () => { - jest.spyOn(storage, "getStorageAccount").mockImplementationOnce( - async (): Promise => { - return undefined; - } - ); + jest.spyOn(storage, "getStorageAccount").mockResolvedValueOnce(undefined); try { await storage.isStorageAccountExist("testResourceGroup", ""); + expect(true).toBe(false); } catch (err) { expect(err.message).toEqual("\nInvalid accountName"); } @@ -286,6 +263,7 @@ describe("get storage account", () => { test("invalid resource group", async () => { try { await storage.getStorageAccount("", "testAccountName"); + expect(true).toBe(false); } catch (err) { expect(err.message).toEqual("\nInvalid resourceGroup"); } @@ -294,6 +272,7 @@ describe("get storage account", () => { test("invalid account name", async () => { try { await storage.getStorageAccount("testResourceGroup", ""); + expect(true).toBe(false); } catch (err) { expect(err.message).toEqual("\nInvalid accountName"); } @@ -336,7 +315,7 @@ describe("test validateStorageAccount function", () => { it("negative test: account does not exist", async () => { jest .spyOn(storage, "isStorageAccountNameAvailable") - .mockReturnValueOnce(Promise.resolve(true)); + .mockResolvedValueOnce(true); const res = await validateStorageAccount( resourceGroupName, "testAccountName", @@ -347,10 +326,8 @@ describe("test validateStorageAccount function", () => { it("negative test: key does not exist", async () => { jest .spyOn(storage, "isStorageAccountNameAvailable") - .mockReturnValueOnce(Promise.resolve(false)); - jest - .spyOn(storage, "getStorageAccountKeys") - .mockReturnValueOnce(Promise.resolve([])); + .mockResolvedValueOnce(false); + jest.spyOn(storage, "getStorageAccountKeys").mockResolvedValueOnce([]); const res = await validateStorageAccount( resourceGroupName, "testAccountName", @@ -361,7 +338,7 @@ describe("test validateStorageAccount function", () => { it("negative test: exception thrown", async () => { jest .spyOn(storage, "isStorageAccountNameAvailable") - .mockReturnValueOnce(Promise.reject(new Error("fake"))); + .mockRejectedValueOnce(Error("fake")); await expect( validateStorageAccount(resourceGroupName, "testAccountName", "testkey") ).rejects.toThrow(); @@ -369,10 +346,10 @@ describe("test validateStorageAccount function", () => { it("positive test", async () => { jest .spyOn(storage, "isStorageAccountNameAvailable") - .mockReturnValueOnce(Promise.resolve(false)); + .mockResolvedValueOnce(false); jest .spyOn(storage, "getStorageAccountKeys") - .mockReturnValueOnce(Promise.resolve(["testkey"])); + .mockResolvedValueOnce(["testkey"]); const res = await validateStorageAccount( resourceGroupName, "testAccountName", diff --git a/src/lib/azure/storage.ts b/src/lib/azure/storage.ts index 436803e8b..d0f8fcd4f 100644 --- a/src/lib/azure/storage.ts +++ b/src/lib/azure/storage.ts @@ -437,8 +437,8 @@ export const createTableIfNotExists = ( accountName: string, tableName: string, accessKey: string -): Promise => { - return new Promise((resolve, reject) => { +): Promise => { + return new Promise((resolve, reject) => { try { validateValuesForCreateStorageTable(accountName, tableName); const createTblService = storage.createTableService( diff --git a/src/lib/setup/azureStorage.test.ts b/src/lib/setup/azureStorage.test.ts index c93773695..36b860004 100644 --- a/src/lib/setup/azureStorage.test.ts +++ b/src/lib/setup/azureStorage.test.ts @@ -1,22 +1,72 @@ import inquirer from "inquirer"; -import { createStorage, createStorageAccount } from "./azureStorage"; +import { + createStorage, + createStorageAccount, + tryToCreateStorageAccount, + waitForStorageAccountToBeProvisioned +} from "./azureStorage"; import * as azureStorage from "./azureStorage"; import { RequestContext } from "./constants"; import * as azure from "../azure/storage"; +const testCreateStorage = async (positive: boolean): Promise => { + jest.spyOn(azureStorage, "tryToCreateStorageAccount").mockImplementationOnce( + (rc: RequestContext): Promise => { + return new Promise(resolve => { + rc.createdStorageAccount = true; + resolve(); + }); + } + ); + if (positive) { + jest.spyOn(azure, "getStorageAccountKey").mockResolvedValueOnce("key==="); + jest.spyOn(azure, "createTableIfNotExists").mockResolvedValueOnce(true); + } else { + jest.spyOn(azure, "getStorageAccountKey").mockResolvedValueOnce(undefined); + } + + const rc: RequestContext = { + orgName: "notUsed", + projectName: "notUsed", + accessToken: "notUsed", + workspace: "notUsed" + }; + + if (positive) { + await createStorage(rc); + expect(rc.createdStorageAccount).toBeTruthy(); + expect(rc.createdStorageTable).toBeTruthy(); + } else { + await expect(createStorage(rc)).rejects.toThrow(); + } +}; + describe("test createStorage function", () => { + it("positive test", async () => { + testCreateStorage(true); + }); + it("negative test", async () => { + testCreateStorage(false); + }); +}); + +describe("test tryToCreateStorageAccount function", () => { it("positive test", async () => { jest .spyOn(azureStorage, "createStorageAccount") .mockResolvedValueOnce(true); + jest + .spyOn(azureStorage, "waitForStorageAccountToBeProvisioned") + .mockResolvedValueOnce(); + const rc: RequestContext = { orgName: "notUsed", projectName: "notUsed", accessToken: "notUsed", workspace: "notUsed" }; - await createStorage(rc); - expect(rc.createdStorageAccount).toBe(true); + await tryToCreateStorageAccount(rc); + expect(rc.createdStorageAccount).toBeTruthy(); }); it("positive test: name used", async () => { jest @@ -29,26 +79,31 @@ describe("test createStorage function", () => { // eslint-disable-next-line @typescript-eslint/camelcase azdo_storage_account_name: "teststore" }); + jest + .spyOn(azureStorage, "waitForStorageAccountToBeProvisioned") + .mockResolvedValueOnce(); + const rc: RequestContext = { orgName: "notUsed", projectName: "notUsed", accessToken: "notUsed", workspace: "notUsed" }; - await createStorage(rc); - expect(rc.createdStorageAccount).toBe(true); + await tryToCreateStorageAccount(rc); + expect(rc.createdStorageAccount).toBeTruthy(); expect(rc.storageAccountName).toBe("teststore"); }); }); describe("test createStorageAccount function", () => { it("positive test: account already exist", async () => { - jest.spyOn(azure, "isStorageAccountExist").mockResolvedValueOnce(true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + jest.spyOn(azure, "getStorageAccount").mockResolvedValueOnce({} as any); const result = await createStorageAccount("temp"); expect(result).toBeFalsy(); }); it("positive test: account doe not exist", async () => { - jest.spyOn(azure, "isStorageAccountExist").mockResolvedValueOnce(false); + jest.spyOn(azure, "getStorageAccount").mockResolvedValueOnce(undefined); // eslint-disable-next-line @typescript-eslint/no-explicit-any jest.spyOn(azure, "createStorageAccount").mockResolvedValueOnce({} as any); @@ -56,7 +111,7 @@ describe("test createStorageAccount function", () => { expect(result).toBeTruthy(); }); it("negative test", async () => { - jest.spyOn(azure, "isStorageAccountExist").mockResolvedValueOnce(false); + jest.spyOn(azure, "getStorageAccount").mockResolvedValueOnce(undefined); jest .spyOn(azure, "createStorageAccount") .mockRejectedValueOnce(Error("fake")); @@ -65,3 +120,33 @@ describe("test createStorageAccount function", () => { expect(result).toBeUndefined(); }); }); + +describe("test waitForStorageAccountToBeProvisioned function", () => { + it("sanity test", async () => { + const fn = jest.spyOn(azure, "getStorageAccount"); + fn.mockReset(); + fn.mockResolvedValueOnce({ + provisioningState: "Succeeded" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + await waitForStorageAccountToBeProvisioned("dummy"); + expect(fn).toBeCalledTimes(1); + }); + it("sanity test: poll twicw", async () => { + const fn = jest.spyOn(azure, "getStorageAccount"); + fn.mockReset(); + + fn.mockResolvedValueOnce({ + provisioningState: "" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + fn.mockResolvedValueOnce({ + provisioningState: "Succeeded" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + await waitForStorageAccountToBeProvisioned("dummy"); + expect(fn).toBeCalledTimes(2); + }); +}); diff --git a/src/lib/setup/azureStorage.ts b/src/lib/setup/azureStorage.ts index e86793f55..530a6c97e 100644 --- a/src/lib/setup/azureStorage.ts +++ b/src/lib/setup/azureStorage.ts @@ -2,11 +2,14 @@ import { RequestContext, RESOURCE_GROUP, STORAGE_ACCOUNT_NAME, + STORAGE_TABLE_NAME, RESOURCE_GROUP_LOCATION } from "./constants"; import { createStorageAccount as createAccount, - isStorageAccountExist + createTableIfNotExists, + getStorageAccount, + getStorageAccountKey } from "../azure/storage"; import * as promptBuilder from "../promptBuilder"; import inquirer from "inquirer"; @@ -14,7 +17,7 @@ import inquirer from "inquirer"; export const createStorageAccount = async ( name: string ): Promise => { - const oStorage = await isStorageAccountExist(RESOURCE_GROUP, name, {}); + const oStorage = await getStorageAccount(RESOURCE_GROUP, name); if (!oStorage) { try { await createAccount(RESOURCE_GROUP, name, RESOURCE_GROUP_LOCATION); @@ -27,7 +30,18 @@ export const createStorageAccount = async ( } }; -export const createStorage = async (rc: RequestContext): Promise => { +export const waitForStorageAccountToBeProvisioned = async ( + name: string +): Promise => { + let oStorage = await getStorageAccount(RESOURCE_GROUP, name); + while (oStorage && oStorage.provisioningState !== "Succeeded") { + oStorage = await getStorageAccount(RESOURCE_GROUP, name); + } +}; + +export const tryToCreateStorageAccount = async ( + rc: RequestContext +): Promise => { rc.storageAccountName = rc.storageAccountName || STORAGE_ACCOUNT_NAME; let res = await createStorageAccount(rc.storageAccountName); while (res === undefined) { @@ -37,5 +51,24 @@ export const createStorage = async (rc: RequestContext): Promise => { rc.storageAccountName = ans.azdo_storage_account_name as string; res = await createStorageAccount(rc.storageAccountName); } + await waitForStorageAccountToBeProvisioned(rc.storageAccountName); rc.createdStorageAccount = !!res; }; + +export const createStorage = async (rc: RequestContext): Promise => { + await tryToCreateStorageAccount(rc); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const accountName = rc.storageAccountName!; + const key = await getStorageAccountKey(RESOURCE_GROUP, accountName); + if (!key) { + throw Error("cannot get storage access key."); + } + rc.storageAccountAccessKey = key; + rc.storageTableName = STORAGE_TABLE_NAME; + rc.createdStorageTable = await createTableIfNotExists( + accountName, + STORAGE_TABLE_NAME, + key + ); +}; diff --git a/src/lib/setup/constants.ts b/src/lib/setup/constants.ts index 9011a4f03..d8c6aeea2 100644 --- a/src/lib/setup/constants.ts +++ b/src/lib/setup/constants.ts @@ -5,6 +5,8 @@ export interface RequestContext { workspace: string; acrName?: string; storageAccountName?: string; + storageTableName?: string; + storageAccountAccessKey?: string; toCreateAppRepo?: boolean; toCreateSP?: boolean; createdProject?: boolean; @@ -17,6 +19,7 @@ export interface RequestContext { createdBuildPipeline?: boolean; createServicePrincipal?: boolean; createdStorageAccount?: boolean; + createdStorageTable?: boolean; servicePrincipalId?: string; servicePrincipalPassword?: string; servicePrincipalTenantId?: string; @@ -40,6 +43,8 @@ export const ACR_NAME = "quickStartACR"; export const VARIABLE_GROUP = "quick-start-vg"; export const APP_REPO_BUILD = "quick-start-app-build"; export const STORAGE_ACCOUNT_NAME = "quickstartstore"; +export const STORAGE_TABLE_NAME = "quickstartstoragetable"; +export const STORAGE_PARTITION_KEY = "quick-start-part-key"; export const SETUP_LOG = "setup.log"; export const HLD_DEFAULT_COMPONENT_NAME = "traefik2"; diff --git a/src/lib/setup/prompt.test.ts b/src/lib/setup/prompt.test.ts index 7e46d2b32..84e7914a6 100644 --- a/src/lib/setup/prompt.test.ts +++ b/src/lib/setup/prompt.test.ts @@ -138,7 +138,8 @@ describe("test getAnswerFromFile function", () => { "azdo_org_name=orgname", "azdo_pat=pat", "azdo_project_name=project", - "az_storage_account_name=teststore" + "az_storage_account_name=teststore", + "az_storage_table=storagetable" ]; fs.writeFileSync(file, data.join("\n")); const requestContext = getAnswerFromFile(file); @@ -153,7 +154,8 @@ describe("test getAnswerFromFile function", () => { const data = [ "azdo_org_name=orgname", "azdo_pat=pat", - "az_storage_account_name=teststore" + "az_storage_account_name=teststore", + "az_storage_table=storagetable" ]; fs.writeFileSync(file, data.join("\n")); const requestContext = getAnswerFromFile(file); @@ -210,7 +212,8 @@ describe("test getAnswerFromFile function", () => { "az_sp_password=a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", "az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47", "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48", - "az_storage_account_name=teststore" + "az_storage_account_name=teststore", + "az_storage_table=storagetable" ]; fs.writeFileSync(file, data.join("\n")); const requestContext = getAnswerFromFile(file); diff --git a/src/lib/setup/prompt.ts b/src/lib/setup/prompt.ts index 1f1747ac8..e562e85e7 100644 --- a/src/lib/setup/prompt.ts +++ b/src/lib/setup/prompt.ts @@ -10,7 +10,8 @@ import { validateServicePrincipalPassword, validateServicePrincipalTenantId, validateSubscriptionId, - validateStorageAccountName + validateStorageAccountName, + validateStorageTableName } from "../validator"; import { ACR_NAME, @@ -255,6 +256,11 @@ export const getAnswerFromFile = (file: string): RequestContext => { throw new Error(vStorageAccountName); } + const vStorageTable = validateStorageTableName(map.az_storage_table); + if (typeof vStorageTable === "string") { + throw new Error(vStorageTable); + } + const rc: RequestContext = { accessToken: map.azdo_pat, orgName: map.azdo_org_name, @@ -264,6 +270,7 @@ export const getAnswerFromFile = (file: string): RequestContext => { servicePrincipalTenantId: map.az_sp_tenant, acrName: map.az_acr_name || ACR_NAME, storageAccountName: map.az_storage_account_name, + storageTableName: map.az_storage_table, workspace: WORKSPACE }; diff --git a/src/lib/setup/scaffold.test.ts b/src/lib/setup/scaffold.test.ts index 81ede5322..a3dcba032 100644 --- a/src/lib/setup/scaffold.test.ts +++ b/src/lib/setup/scaffold.test.ts @@ -6,6 +6,7 @@ import * as cmdCreateVariableGroup from "../../commands/project/create-variable- import * as projectInit from "../../commands/project/init"; import * as createService from "../../commands/service/create"; import * as variableGroup from "../../lib/pipelines/variableGroup"; +import * as sVariableGroup from "../setup/variableGroup"; import { createTempDir } from "../ioUtil"; import { APP_REPO, @@ -173,7 +174,7 @@ describe("test appRepo function", () => { jest .spyOn(variableGroup, "deleteVariableGroup") .mockResolvedValueOnce(true); - jest.spyOn(cmdCreateVariableGroup, "create").mockResolvedValueOnce({}); + jest.spyOn(sVariableGroup, "create").mockResolvedValueOnce(); jest .spyOn(cmdCreateVariableGroup, "setVariableGroupInBedrockFile") .mockReturnValueOnce(); diff --git a/src/lib/setup/scaffold.ts b/src/lib/setup/scaffold.ts index bf52c7fd5..b76f9dfb4 100644 --- a/src/lib/setup/scaffold.ts +++ b/src/lib/setup/scaffold.ts @@ -4,7 +4,6 @@ import path from "path"; import simplegit from "simple-git/promise"; import { initialize as hldInitialize } from "../../commands/hld/init"; import { - create as createVariableGroup, setVariableGroupInBedrockFile, updateLifeCyclePipeline } from "../../commands/project/create-variable-group"; @@ -12,6 +11,7 @@ import { initialize as projectInitialize } from "../../commands/project/init"; import { createService } from "../../commands/service/create"; import { AzureDevOpsOpts } from "../../lib/git"; import { deleteVariableGroup } from "../../lib/pipelines/variableGroup"; +import { create as createVariableGroup } from "../../lib/setup/variableGroup"; import { logger } from "../../logger"; import { APP_REPO, @@ -188,16 +188,7 @@ export const setupVariableGroup = async (rc: RequestContext): Promise => { }; await deleteVariableGroup(accessOpts, VARIABLE_GROUP); - await createVariableGroup( - VARIABLE_GROUP, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - rc.acrName!, - getAzureRepoUrl(rc.orgName, rc.projectName, HLD_REPO), - rc.servicePrincipalId, - rc.servicePrincipalPassword, - rc.servicePrincipalTenantId, - accessOpts - ); + await createVariableGroup(rc, VARIABLE_GROUP); logger.info(`Successfully created variable group, ${VARIABLE_GROUP}`); setVariableGroupInBedrockFile(".", VARIABLE_GROUP); diff --git a/src/lib/setup/setupLog.test.ts b/src/lib/setup/setupLog.test.ts index b5259e4e4..7d821f1df 100644 --- a/src/lib/setup/setupLog.test.ts +++ b/src/lib/setup/setupLog.test.ts @@ -37,12 +37,13 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { rc.servicePrincipalPassword = "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b"; rc.servicePrincipalTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; rc.storageAccountName = "teststore"; + rc.storageTableName = "storagetable"; rc.createdResourceGroup = true; rc.createdACR = true; rc.createdLifecyclePipeline = true; rc.createdBuildPipeline = true; rc.createdStorageAccount = true; - rc.createdStorageAccount = true; + rc.createdStorageTable = true; } create(rc, file); @@ -61,6 +62,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48", "az_acr_name=testacr", "az_storage_account_name=teststore", + "az_storage_table=storagetable", "workspace: workspace", "Project Created: yes", "High Level Definition Repo Scaffolded: yes", @@ -74,6 +76,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "Build Pipeline Created: yes", "ACR Created: yes", "Storage Account Created: yes", + "Storage Table Created: yes", "Status: Completed" ]); } else { @@ -89,6 +92,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48", "az_acr_name=testacr", "az_storage_account_name=", + "az_storage_table=", "workspace: workspace", "Project Created: yes", "High Level Definition Repo Scaffolded: yes", @@ -102,6 +106,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "Build Pipeline Created: no", "ACR Created: no", "Storage Account Created: no", + "Storage Table Created: no", "Status: Completed" ]); } @@ -157,6 +162,7 @@ describe("test create function", () => { "az_subscription_id=", "az_acr_name=", "az_storage_account_name=", + "az_storage_table=", "workspace: workspace", "Project Created: yes", "High Level Definition Repo Scaffolded: yes", @@ -170,6 +176,7 @@ describe("test create function", () => { "Build Pipeline Created: no", "ACR Created: no", "Storage Account Created: no", + "Storage Table Created: no", "Error: things broke", "Status: Incomplete" ]); diff --git a/src/lib/setup/setupLog.ts b/src/lib/setup/setupLog.ts index e31c46c0e..4ff01e847 100644 --- a/src/lib/setup/setupLog.ts +++ b/src/lib/setup/setupLog.ts @@ -23,6 +23,7 @@ export const create = (rc: RequestContext | undefined, file?: string): void => { `az_subscription_id=${rc.subscriptionId || ""}`, `az_acr_name=${rc.acrName || ""}`, `az_storage_account_name=${rc.storageAccountName || ""}`, + `az_storage_table=${rc.storageTableName || ""}`, `workspace: ${rc.workspace}`, `Project Created: ${getBooleanVal(rc.createdProject)}`, `High Level Definition Repo Scaffolded: ${getBooleanVal(rc.scaffoldHLD)}`, @@ -39,7 +40,8 @@ export const create = (rc: RequestContext | undefined, file?: string): void => { )}`, `Build Pipeline Created: ${getBooleanVal(rc.createdBuildPipeline)}`, `ACR Created: ${getBooleanVal(rc.createdACR)}`, - `Storage Account Created: ${getBooleanVal(rc.createdStorageAccount)}` + `Storage Account Created: ${getBooleanVal(rc.createdStorageAccount)}`, + `Storage Table Created: ${getBooleanVal(rc.createdStorageTable)}` ]; if (rc.error) { buff.push(`Error: ${rc.error}`); diff --git a/src/lib/setup/variableGroup.test.ts b/src/lib/setup/variableGroup.test.ts new file mode 100644 index 000000000..4db69e4a4 --- /dev/null +++ b/src/lib/setup/variableGroup.test.ts @@ -0,0 +1,53 @@ +import { create, createVariableData } from "./variableGroup"; +import * as service from "../pipelines/variableGroup"; +import { RequestContext } from "./constants"; +import { deepClone } from "../util"; + +const mockRequestContext: RequestContext = { + orgName: "dummy", + projectName: "dummy", + accessToken: "notused", + workspace: "notused", + acrName: "acrName", + servicePrincipalId: "servicePrincipalId", + servicePrincipalPassword: "servicePrincipalPassword", + servicePrincipalTenantId: "servicePrincipalTenantId", + storageAccountAccessKey: "storageAccountAccessKey", + storageAccountName: "storageAccountName", + storageTableName: "storageTableName" +}; + +describe("test create function", () => { + it("sanity test", async () => { + jest.spyOn(service, "addVariableGroup").mockResolvedValueOnce({}); + await create(mockRequestContext, "name"); + }); +}); + +describe("test createVariableData function", () => { + it("sanity test", () => { + createVariableData(mockRequestContext); + }); + it("negative test", () => { + const properties = [ + "orgName", + "projectName", + "accessToken", + "acrName", + "servicePrincipalId", + "servicePrincipalPassword", + "servicePrincipalTenantId", + "storageAccountAccessKey", + "storageAccountName", + "storageTableName" + ]; + properties.forEach(prop => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const clone = deepClone(mockRequestContext) as any; + delete clone[prop]; + expect(() => { + createVariableData(clone); + }).toThrow(); + }); + }); +}); diff --git a/src/lib/setup/variableGroup.ts b/src/lib/setup/variableGroup.ts new file mode 100644 index 000000000..3852b369c --- /dev/null +++ b/src/lib/setup/variableGroup.ts @@ -0,0 +1,78 @@ +import { VariableGroupDataVariable } from "../../types"; +import { logger } from "../../logger"; +import { addVariableGroup } from "../pipelines/variableGroup"; +import { HLD_REPO, RequestContext, STORAGE_PARTITION_KEY } from "./constants"; +import { getAzureRepoUrl } from "./gitService"; + +const validateData = (rc: RequestContext): void => { + if (!rc.acrName) throw Error("Missing Azure Container Registry Name."); + if (!rc.orgName) throw Error("Missing Organization Name."); + if (!rc.projectName) throw Error("Missing Project Name."); + if (!rc.accessToken) throw Error("Missing Personal Access Token."); + if (!rc.servicePrincipalId) throw Error("Missing Service Principal Id."); + if (!rc.servicePrincipalPassword) + throw Error("Missing Service Principal Secret."); + if (!rc.servicePrincipalTenantId) + throw Error("Missing Service Principal Tenant Id."); + if (!rc.storageAccountAccessKey) + throw Error("Missing Storage Account Access Key."); + if (!rc.storageAccountName) throw Error("Missing Storage Account Name."); + if (!rc.storageTableName) throw Error("Missing Storage Table Name."); +}; + +export const createVariableData = ( + rc: RequestContext +): VariableGroupDataVariable => { + validateData(rc); + return { + ACR_NAME: { + value: rc.acrName + }, + HLD_REPO: { + value: getAzureRepoUrl(rc.orgName, rc.projectName, HLD_REPO) + }, + PAT: { + isSecret: true, + value: rc.accessToken + }, + SP_APP_ID: { + isSecret: true, + value: rc.servicePrincipalId + }, + SP_PASS: { + isSecret: true, + value: rc.servicePrincipalPassword + }, + SP_TENANT: { + isSecret: true, + value: rc.servicePrincipalTenantId + }, + INTROSPECTION_ACCOUNT_KEY: { + isSecret: true, + value: rc.storageAccountAccessKey + }, + INTROSPECTION_ACCOUNT_NAME: { + value: rc.storageAccountName + }, + INTROSPECTION_PARTITION_KEY: { + value: STORAGE_PARTITION_KEY + }, + INTROSPECTION_TABLE_NAME: { + value: rc.storageTableName + } + }; +}; + +export const create = async ( + rc: RequestContext, + name: string +): Promise => { + logger.info(`Creating Variable Group from group definition '${name}'`); + + await addVariableGroup({ + description: "Created by spk quick start command", + name, + type: "Vsts", + variables: createVariableData(rc) + }); +}; From 3a0dd7828e96b3f069288bc2baba9de7f7158e92 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sat, 21 Mar 2020 18:18:57 -0700 Subject: [PATCH 03/15] prompt merging second PR --- src/commands/setup.test.ts | 3 +++ src/commands/setup.ts | 5 ++++- src/lib/promptBuilder.ts | 12 ++++++++++++ src/lib/setup/prompt.test.ts | 36 ++++++++++++++++++++++++++++++++++++ src/lib/setup/prompt.ts | 12 +++--------- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index 6be37de87..ac33dc55f 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -364,6 +364,9 @@ const testCreateAppRepoTasks = async (prApproved = true): Promise => { .mockResolvedValueOnce(prApproved); if (prApproved) { jest.spyOn(pipelineService, "createBuildPipeline").mockResolvedValueOnce(); + jest + .spyOn(promptInstance, "promptForApprovingHLDPullRequest") + .mockResolvedValueOnce(prApproved); } const res = await createAppRepoTasks( diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 6ba593b9b..5ce653351 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -161,7 +161,10 @@ export const createAppRepoTasks = async ( if (approved) { await createBuildPipeline(buildAPI, rc); - return true; + + if (await promptForApprovingHLDPullRequest(rc)) { + return true; + } } logger.warn("HLD Pull Request is not approved."); diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts index 9b8edfbea..87a592edd 100644 --- a/src/lib/promptBuilder.ts +++ b/src/lib/promptBuilder.ts @@ -192,3 +192,15 @@ export const storageAccountName = ( validate: validator.validateStorageAccessKey }; }; + +export const approvingHLDPullRequest = ( + url: string, + defaultValue = true +): QuestionCollection => { + return { + default: defaultValue, + message: `Please approve and merge the Pull Request at ${url}? Refresh the page if you do not see an active Pull Request.`, + name: "approve_hld_pr", + type: "confirm" + }; +}; diff --git a/src/lib/setup/prompt.test.ts b/src/lib/setup/prompt.test.ts index 84e7914a6..b37a44fa8 100644 --- a/src/lib/setup/prompt.test.ts +++ b/src/lib/setup/prompt.test.ts @@ -243,6 +243,8 @@ describe("test getAnswerFromFile function", () => { "azdo_pat=pat", "azdo_project_name=project", "az_create_app=true", + "az_storage_account_name=abc1234", + "az_storage_table=abc1234", "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48" ]; [".", ".##", ".abc"].forEach((v, i) => { @@ -263,6 +265,40 @@ describe("test getAnswerFromFile function", () => { }).toThrow(); }); }); + it("negative test: with app creation, incorrect storage account name", () => { + const dir = createTempDir(); + const file = path.join(dir, "testfile"); + const data = [ + "azdo_org_name=orgname", + "azdo_pat=pat", + "azdo_project_name=project", + "az_create_app=true", + "az_storage_account_name=ab", + "az_storage_table=abc1234", + "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48" + ]; + fs.writeFileSync(file, data.join("\n")); + expect(() => { + getAnswerFromFile(file); + }).toThrow(); + }); + it("negative test: with app creation, incorrect storage table name", () => { + const dir = createTempDir(); + const file = path.join(dir, "testfile"); + const data = [ + "azdo_org_name=orgname", + "azdo_pat=pat", + "azdo_project_name=project", + "az_create_app=true", + "az_storage_account_name=abx1234", + "az_storage_table=*a", + "az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48" + ]; + fs.writeFileSync(file, data.join("\n")); + expect(() => { + getAnswerFromFile(file); + }).toThrow(); + }); it("negative test: with app creation, incorrect subscription id value", () => { const dir = createTempDir(); const file = path.join(dir, "testfile"); diff --git a/src/lib/setup/prompt.ts b/src/lib/setup/prompt.ts index e562e85e7..64e9e1482 100644 --- a/src/lib/setup/prompt.ts +++ b/src/lib/setup/prompt.ts @@ -288,14 +288,8 @@ export const promptForApprovingHLDPullRequest = async ( rc.projectName, HLD_REPO )}/pullrequest`; - const questions = [ - { - default: true, - message: `Please approve and merge the Pull Request at ${urlPR}? Refresh the page if you do not see an active Pull Request.`, - name: "approve_hld_pr", - type: "confirm" - } - ]; - const answers = await inquirer.prompt(questions); + const answers = await inquirer.prompt([ + promptBuilder.approvingHLDPullRequest(urlPR) + ]); return !!answers.approve_hld_pr; }; From b766c4807188150cd246971bf2c5441e79d5d2dd Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sat, 21 Mar 2020 18:22:23 -0700 Subject: [PATCH 04/15] Update setup.md --- src/commands/setup.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/commands/setup.md b/src/commands/setup.md index 1ea6def89..f745e6134 100644 --- a/src/commands/setup.md +++ b/src/commands/setup.md @@ -57,15 +57,18 @@ The followings shall be created 5. A High Level Definition (HLD) to Manifest pipeline. 6. If user chose to create sample app repo 1. A Service Principal (if requested) - 2. A resource group, `quick-start-rg` if it does not exist. - 3. A Azure Container Registry, `quickStartACR` in resource group, + 1. A resource group, `quick-start-rg` if it does not exist. + 1. A storage account if it does not exist. Storage Account name has to be + unqiue acess Azure. + 1. A storage table in the storage account. + 1. A Azure Container Registry, `quickStartACR` in resource group, `quick-start-rg` if it does not exist. - 4. A Git Repo, `quick-start-helm`, it shall be deleted and recreated if is + 1. A Git Repo, `quick-start-helm`, it shall be deleted and recreated if is already exists. - 5. A Git Repo, `quick-start-app`, it shall be deleted and recreated if is + 1. A Git Repo, `quick-start-app`, it shall be deleted and recreated if is already exists. - 6. A Lifecycle pipeline. - 7. A Build pipeline. + 1. A Lifecycle pipeline. + 1. A Build pipeline. ## Setup log From 359107c58a3a58490635a7be77eb13c62c2c9e18 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sun, 22 Mar 2020 08:49:12 -0700 Subject: [PATCH 05/15] finishing touch --- src/lib/setup/setupLog.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib/setup/setupLog.ts b/src/lib/setup/setupLog.ts index 4ff01e847..4e42ae3dc 100644 --- a/src/lib/setup/setupLog.ts +++ b/src/lib/setup/setupLog.ts @@ -50,6 +50,16 @@ export const create = (rc: RequestContext | undefined, file?: string): void => { buff.push("Status: Completed"); } + console.log(""); + console.log(buff.join("\n")); + console.log(""); + + if (rc.toCreateAppRepo && !rc.error) { + console.log( + `type "spk deployment get" or "spk deployment dashboard" command to view deployments information.` + ); + } + fs.writeFileSync(file, buff.join("\n")); } }; From 8528dfa4378573f584a5c1aab67ef91865eb689b Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sun, 22 Mar 2020 15:43:39 -0700 Subject: [PATCH 06/15] update config.yaml with repo info --- src/commands/setup.test.ts | 12 ++++++++++ src/commands/setup.ts | 47 +++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index ac33dc55f..93f18a737 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -45,6 +45,10 @@ describe("test createSPKConfig function", () => { const data = readYaml(tmpFile); expect(data.azure_devops).toStrictEqual({ access_token: "pat", + hld_repository: + "https://dev.azure.com/orgname/project/_git/quick-start-hld", + manifest_repository: + "https://dev.azure.com/orgname/project/_git/quick-start-manifest", org: "orgname", project: "project" }); @@ -58,6 +62,10 @@ describe("test createSPKConfig function", () => { const data = readYaml(tmpFile); expect(data.azure_devops).toStrictEqual({ access_token: "pat", + hld_repository: + "https://dev.azure.com/orgname/project/_git/quick-start-hld", + manifest_repository: + "https://dev.azure.com/orgname/project/_git/quick-start-manifest", org: "orgname", project: "project" }); @@ -80,6 +88,10 @@ describe("test createSPKConfig function", () => { const data = readYaml(tmpFile); expect(data.azure_devops).toStrictEqual({ access_token: "pat", + hld_repository: + "https://dev.azure.com/orgname/project/_git/quick-start-hld", + manifest_repository: + "https://dev.azure.com/orgname/project/_git/quick-start-manifest", org: "orgname", project: "project" }); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 5ce653351..07c72bc10 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -10,6 +10,8 @@ import { create as createACR } from "../lib/azure/containerRegistryService"; import { create as createResourceGroup } from "../lib/azure/resourceService"; import { build as buildCmd, exit as exitCmd } from "../lib/commandBuilder"; import { + HLD_REPO, + MANIFEST_REPO, RequestContext, RESOURCE_GROUP, RESOURCE_GROUP_LOCATION, @@ -17,7 +19,7 @@ import { WORKSPACE } from "../lib/setup/constants"; import { createDirectory } from "../lib/setup/fsUtil"; -import { getGitApi } from "../lib/setup/gitService"; +import { getAzureRepoUrl, getGitApi } from "../lib/setup/gitService"; import { createBuildPipeline, createHLDtoManifestPipeline, @@ -56,33 +58,30 @@ interface APIError { * @param answers Answers provided to the commander */ export const createSPKConfig = (rc: RequestContext): void => { - if (!rc.toCreateAppRepo) { - fs.writeFileSync( - defaultConfigFile(), - yaml.safeDump({ - azure_devops: { - access_token: rc.accessToken, - org: rc.orgName, - project: rc.projectName - } - }) - ); - return; - } - const data: ConfigYaml = { azure_devops: { access_token: rc.accessToken, org: rc.orgName, - project: rc.projectName - }, - introspection: { - azure: { - service_principal_id: rc.servicePrincipalId, - service_principal_secret: rc.servicePrincipalPassword, - subscription_id: rc.subscriptionId, - tenant_id: rc.servicePrincipalTenantId - } + project: rc.projectName, + hld_repository: getAzureRepoUrl(rc.orgName, rc.projectName, HLD_REPO), + manifest_repository: getAzureRepoUrl( + rc.orgName, + rc.projectName, + MANIFEST_REPO + ) + } + }; + if (!rc.toCreateAppRepo) { + fs.writeFileSync(defaultConfigFile(), yaml.safeDump(data)); + return; + } + + data.introspection = { + azure: { + service_principal_id: rc.servicePrincipalId, + service_principal_secret: rc.servicePrincipalPassword, + subscription_id: rc.subscriptionId, + tenant_id: rc.servicePrincipalTenantId } }; From 9bc0faf8247efe18a4482a5d0dd4ff25b5abf383 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Tue, 24 Mar 2020 08:43:03 -0700 Subject: [PATCH 07/15] Update jest.config.js --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index a93551fca..3f878c46d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,4 @@ module.exports = { preset: "ts-jest", - testEnvironment: "node", + testEnvironment: "node" }; From ade991a8976db7285feebf5bf7b7460f1f8360b5 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Tue, 24 Mar 2020 08:46:18 -0700 Subject: [PATCH 08/15] added storage tests --- jest.config.js | 2 +- src/lib/azure/storage.test.ts | 70 ++++++++++++++++++++++------------- src/lib/azure/storage.ts | 5 ++- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/jest.config.js b/jest.config.js index 3f878c46d..a93551fca 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,4 @@ module.exports = { preset: "ts-jest", - testEnvironment: "node" + testEnvironment: "node", }; diff --git a/src/lib/azure/storage.test.ts b/src/lib/azure/storage.test.ts index 6686ffb7a..5ba9ad0ae 100644 --- a/src/lib/azure/storage.test.ts +++ b/src/lib/azure/storage.test.ts @@ -9,6 +9,7 @@ import { disableVerboseLogging, enableVerboseLogging } from "../../logger"; import { Config } from "../../config"; import { getStorageAccount, validateStorageAccount } from "./storage"; import * as storage from "./storage"; +import * as azureStorage from "azure-storage"; const resourceGroupName = uuid(); const storageAccountName = uuid(); @@ -261,40 +262,59 @@ describe("storage account exists", () => { describe("get storage account", () => { test("invalid resource group", async () => { - try { - await storage.getStorageAccount("", "testAccountName"); - expect(true).toBe(false); - } catch (err) { - expect(err.message).toEqual("\nInvalid resourceGroup"); - } + await expect( + storage.getStorageAccount("", "testAccountName") + ).rejects.toThrow("\nInvalid resourceGroup"); }); - test("invalid account name", async () => { - try { - await storage.getStorageAccount("testResourceGroup", ""); - expect(true).toBe(false); - } catch (err) { - expect(err.message).toEqual("\nInvalid accountName"); - } + await expect( + storage.getStorageAccount("testResourceGroup", "") + ).rejects.toThrow("\nInvalid accountName"); }); }); describe("create table if it doesn't exist", () => { test("invalid account name", async () => { - try { - await storage.createTableIfNotExists("", "tableName", "accessKey"); - expect(true).toBe(false); - } catch (err) { - expect(err.message).toEqual("\nInvalid accountName"); - } + await expect( + storage.createTableIfNotExists("", "tableName", "accessKey") + ).rejects.toThrow("\nInvalid accountName"); }); test("invalid account name", async () => { - try { - await storage.createTableIfNotExists("testAccountName", "", "accesKey"); - expect(true).toBe(false); - } catch (err) { - expect(err.message).toEqual("\nInvalid tableName"); - } + await expect( + storage.createTableIfNotExists("testAccountName", "", "accessKey") + ).rejects.toThrow("\nInvalid tableName"); + }); + test("positive test", async () => { + jest.spyOn(azureStorage, "createTableService").mockReturnValueOnce({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createTableIfNotExists: (tableName: string, callbackFn: any) => { + callbackFn(null, { + created: true, + }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + await storage.createTableIfNotExists( + "testAccountName", + "tableName", + "accessKey" + ); + }); + test("negative test", async () => { + jest.spyOn(azureStorage, "createTableService").mockReturnValueOnce({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createTableIfNotExists: (tableName: string, callbackFn: any) => { + callbackFn(Error("fake message"), null); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + await expect( + storage.createTableIfNotExists( + "testAccountName", + "tableName", + "accessKey" + ) + ).rejects.toThrow("fake message"); }); }); diff --git a/src/lib/azure/storage.ts b/src/lib/azure/storage.ts index 0fc7ec17d..82292218f 100644 --- a/src/lib/azure/storage.ts +++ b/src/lib/azure/storage.ts @@ -452,9 +452,10 @@ export const createTableIfNotExists = ( `Unable to create table in storage account ${accountName} \n ${err}` ); reject(err); + } else { + logger.debug(`table result: ${JSON.stringify(result)}`); + resolve(result.created); } - logger.debug(`table result: ${JSON.stringify(result)}`); - resolve(result.created); }); } catch (err) { reject(err); From 61379936d557083242c889d5b60e1bd34bec3d46 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Tue, 24 Mar 2020 09:28:43 -0700 Subject: [PATCH 09/15] minor fixes --- src/lib/azure/storage.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/lib/azure/storage.ts b/src/lib/azure/storage.ts index 82292218f..b5d24ef97 100644 --- a/src/lib/azure/storage.ts +++ b/src/lib/azure/storage.ts @@ -14,7 +14,10 @@ import { logger } from "../../logger"; import { AzureAccessOpts } from "../../types"; import { getManagementCredentials } from "./azurecredentials"; -let storageManagementClient: StorageManagementClient | undefined; // singleton Storage Management Client so it can be reused +// for caching Storage Management Client so it can be reused +// so we need not have to fetch client every time. +// there is an huge overhead in fetching it. +let storageManagementClient: StorageManagementClient | undefined; /** * Creates Azure storage management client @@ -92,7 +95,7 @@ export const getStorageAccountKeys = async ( } if (errors.length !== 0) { - throw new Error(`\n${errors.join("\n")}`); + throw Error(`\n${errors.join("\n")}`); } const storageAccountKeys: string[] = []; @@ -182,7 +185,7 @@ export const getStorageAccount = async ( } if (errors.length !== 0) { - throw new Error(`\n${errors.join("\n")}`); + throw Error(`\n${errors.join("\n")}`); } const message = `Azure storage account ${accountName} in resource group ${resourceGroup}`; @@ -239,7 +242,7 @@ export const isStorageAccountExist = async ( } if (errors.length !== 0) { - throw new Error(`\n${errors.join("\n")}`); + throw Error(`\n${errors.join("\n")}`); } const message = `Azure storage account ${accountName} in resource group ${resourceGroup}`; @@ -278,7 +281,7 @@ const validateInputsForCreateAccount = ( errors.push(`Invalid location`); } if (errors.length !== 0) { - throw new Error(`\n${errors.join("\n")}`); + throw Error(`\n${errors.join("\n")}`); } }; @@ -314,7 +317,7 @@ export const createStorageAccount = async ( if (response.nameAvailable === false) { const nameErrorMessage = `Storage account name ${accountName} is not available. Please choose a different name.`; logger.error(nameErrorMessage); - throw new Error(nameErrorMessage); + throw Error(nameErrorMessage); } logger.verbose(`Storage account name ${accountName} is available`); @@ -369,7 +372,7 @@ export const getStorageAccountKey = async ( } if (errors.length !== 0) { - throw new Error(`\n${errors.join("\n")}`); + throw Error(`\n${errors.join("\n")}`); } try { @@ -422,7 +425,7 @@ const validateValuesForCreateStorageTable = ( } if (errors.length !== 0) { - throw new Error(`\n${errors.join("\n")}`); + throw Error(`\n${errors.join("\n")}`); } }; @@ -485,7 +488,7 @@ export const createResourceGroupIfNotExists = async ( } if (errors.length !== 0) { - throw new Error(`\n${errors.join("\n")}`); + throw Error(`\n${errors.join("\n")}`); } const message = `Azure resource group ${name} in ${location} location`; From 9502a9c74b3ddf3ce634065823d4aa6a4da8d9f1 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 25 Mar 2020 20:10:38 -0700 Subject: [PATCH 10/15] add docker image in config,yaml --- src/commands/setup.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 1616f74a2..0e7ee1c49 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -77,6 +77,10 @@ export const createSPKConfig = (rc: RequestContext): void => { } data.introspection = { + dashboard: { + image: "mcr.microsoft.com/k8s/bedrock/spektate:latest", + name: "spektate", + }, azure: { service_principal_id: rc.servicePrincipalId, service_principal_secret: rc.servicePrincipalPassword, @@ -86,6 +90,7 @@ export const createSPKConfig = (rc: RequestContext): void => { }; if (data.introspection && data.introspection.azure) { + // to due to eslint error const azure = data.introspection.azure; if (rc.storageAccountName) { azure.account_name = rc.storageAccountName; @@ -188,8 +193,7 @@ export const execute = async ( try { requestContext = opts.file ? getAnswerFromFile(opts.file) : await prompt(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const rc = requestContext!; + const rc = requestContext; createDirectory(WORKSPACE, true); createSPKConfig(rc); From c7fc26310b5af98b6b284794927d152088be9b29 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Fri, 27 Mar 2020 21:35:53 -0700 Subject: [PATCH 11/15] final touch --- docs/commands/data.json | 2 +- src/commands/hld/pipeline.test.ts | 59 +++++++++++-------------------- src/commands/setup.md | 3 ++ src/commands/setup.test.ts | 4 +++ src/lib/setup/scaffold.test.ts | 5 +++ src/lib/setup/scaffold.ts | 7 ++++ 6 files changed, 41 insertions(+), 39 deletions(-) diff --git a/docs/commands/data.json b/docs/commands/data.json index 8515a6c3e..966eda283 100644 --- a/docs/commands/data.json +++ b/docs/commands/data.json @@ -26,7 +26,7 @@ "description": "Path to the file that contains answers to the questions." } ], - "markdown": "## Description\n\nThis command assists in creating resources in Azure DevOps so that you can get\nstarted with using Bedrock. It creates\n\n1. An Azure DevOps project.\n\nBy Default, it runs in an interactive mode where you are prompted for answers\nfor a few questions\n\n1. Azure DevOps Organization Name\n2. Azure DevOps Project Name, the project to be created.\n3. Azure DevOps Personal Access Token. The token needs to have these permissions\n 1. Read and write projects.\n 2. Read and write codes.\n4. To create a sample application Repo\n 1. If Yes, a Azure Service Principal is needed. You have 2 options\n 1. have the command line tool to create it. Azure command line tool shall\n be used. You will be prompted to select a subscription identifier.\n 2. Provide the Service Principal Id, Password, and Tenant Id. From this\n information, the tool will retrieve the subscription identifier.\n\nIt can also run in a non interactive mode by providing a file that contains\nanswers to the above questions.\n\n```\nspk setup --file \n```\n\nContent of this file is as follow\n\n```\nazdo_org_name=\nazdo_project_name=\nazdo_pat=\naz_create_app=\naz_create_sp=\naz_sp_id=\naz_sp_password=\naz_sp_tenant=\naz_subscription_id=\naz_acr_name=\n```\n\n`azdo_project_name` is optional and default value is `BedrockRocks`.\n\nThe followings shall be created\n\n1. A working directory, `quick-start-env`\n2. Project shall not be created if it already exists.\n3. A Git Repo, `quick-start-hld`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n4. A Git Repo, `quick-start-manifest`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n5. A High Level Definition (HLD) to Manifest pipeline.\n6. If user chose to create sample app repo\n 1. A Service Principal (if requested)\n 1. A resource group, `quick-start-rg` if it does not exist.\n 1. A storage account if it does not exist. Storage Account name has to be\n unqiue acess Azure.\n 1. A storage table in the storage account.\n 1. A Azure Container Registry, `quickStartACR` in resource group,\n `quick-start-rg` if it does not exist.\n 1. A Git Repo, `quick-start-helm`, it shall be deleted and recreated if is\n already exists.\n 1. A Git Repo, `quick-start-app`, it shall be deleted and recreated if is\n already exists.\n 1. A Lifecycle pipeline.\n 1. A Build pipeline.\n\n## Setup log\n\nA `setup.log` file is created after running this command. This file contains\ninformation about what are created and the execution status (completed or\nincomplete). This file will not be created if input validation failed.\n\n## Note\n\nTo remove the service principal that it is created by the tool, you can do the\nfollowings:\n\n1. Get the identifier from `setup.log` (look for `az_sp_id`)\n2. run on terminal `az ad sp delete --id `\n" + "markdown": "## Description\n\nThis command assists in creating resources in Azure DevOps so that you can get\nstarted with using Bedrock. It creates\n\n1. An Azure DevOps project.\n\nBy Default, it runs in an interactive mode where you are prompted for answers\nfor a few questions\n\n1. Azure DevOps Organization Name\n2. Azure DevOps Project Name, the project to be created.\n3. Azure DevOps Personal Access Token. The token needs to have these permissions\n 1. Read and write projects.\n 2. Read and write codes.\n4. To create a sample application Repo\n 1. If Yes, a Azure Service Principal is needed. You have 2 options\n 1. have the command line tool to create it. Azure command line tool shall\n be used. You will be prompted to select a subscription identifier.\n 2. Provide the Service Principal Id, Password, and Tenant Id. From this\n information, the tool will retrieve the subscription identifier.\n\nIt can also run in a non interactive mode by providing a file that contains\nanswers to the above questions.\n\nAfter this command is successfully executed, you can launch the introspection\ndashboard to view the status of pipelines.\n\n```\nspk setup --file \n```\n\nContent of this file is as follow\n\n```\nazdo_org_name=\nazdo_project_name=\nazdo_pat=\naz_create_app=\naz_create_sp=\naz_sp_id=\naz_sp_password=\naz_sp_tenant=\naz_subscription_id=\naz_acr_name=\n```\n\n`azdo_project_name` is optional and default value is `BedrockRocks`.\n\nThe followings shall be created\n\n1. A working directory, `quick-start-env`\n2. Project shall not be created if it already exists.\n3. A Git Repo, `quick-start-hld`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n4. A Git Repo, `quick-start-manifest`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n5. A High Level Definition (HLD) to Manifest pipeline.\n6. If user chose to create sample app repo\n 1. A Service Principal (if requested)\n 1. A resource group, `quick-start-rg` if it does not exist.\n 1. A storage account if it does not exist. Storage Account name has to be\n unqiue acess Azure.\n 1. A storage table in the storage account.\n 1. A Azure Container Registry, `quickStartACR` in resource group,\n `quick-start-rg` if it does not exist.\n 1. A Git Repo, `quick-start-helm`, it shall be deleted and recreated if is\n already exists.\n 1. A Git Repo, `quick-start-app`, it shall be deleted and recreated if is\n already exists.\n 1. A Lifecycle pipeline.\n 1. A Build pipeline.\n\n## Setup log\n\nA `setup.log` file is created after running this command. This file contains\ninformation about what are created and the execution status (completed or\nincomplete). This file will not be created if input validation failed.\n\n## Note\n\nTo remove the service principal that it is created by the tool, you can do the\nfollowings:\n\n1. Get the identifier from `setup.log` (look for `az_sp_id`)\n2. run on terminal `az ad sp delete --id `\n" }, "deployment create": { "command": "create", diff --git a/src/commands/hld/pipeline.test.ts b/src/commands/hld/pipeline.test.ts index a2d6e8590..71c3150a3 100644 --- a/src/commands/hld/pipeline.test.ts +++ b/src/commands/hld/pipeline.test.ts @@ -97,6 +97,7 @@ const orgNameTest = (hasVal: boolean): void => { }; const projectNameTest = (hasVal: boolean): void => { + jest.spyOn(config, "Config").mockReturnValueOnce({}); const data = { buildScriptUrl: "", devopsProject: hasVal ? "project\\abc" : "", @@ -122,11 +123,7 @@ const projectNameTest = (hasVal: boolean): void => { describe("test populateValues function", () => { it("with all values in command opts", () => { - jest.spyOn(config, "Config").mockImplementationOnce( - (): ConfigYaml => { - return MOCKED_CONFIG; - } - ); + jest.spyOn(config, "Config").mockReturnValueOnce(MOCKED_CONFIG); const mockedObject = getMockObject(); expect(populateValues(mockedObject)).toEqual(mockedObject); }); @@ -195,15 +192,11 @@ describe("test populateValues function", () => { describe("test execute function", () => { it("positive test", async () => { - jest.spyOn(config, "Config").mockImplementationOnce( - (): ConfigYaml => { - return MOCKED_CONFIG; - } - ); + jest.spyOn(config, "Config").mockReturnValueOnce(MOCKED_CONFIG); const exitFn = jest.fn(); jest .spyOn(pipeline, "installHldToManifestPipeline") - .mockReturnValueOnce(Promise.resolve()); + .mockResolvedValueOnce(); await execute(MOCKED_VALUES, exitFn); expect(exitFn).toBeCalledTimes(1); @@ -243,43 +236,33 @@ describe("required pipeline variables", () => { describe("create hld to manifest pipeline test", () => { it("should create a pipeline", async () => { - (createPipelineForDefinition as jest.Mock).mockReturnValue({ id: 10 }); + (createPipelineForDefinition as jest.Mock).mockReturnValueOnce({ id: 10 }); await installHldToManifestPipeline(getMockObject()); }); it("should fail if the build client cant be instantiated", async () => { - (getBuildApiClient as jest.Mock).mockReturnValue(Promise.reject("Error")); - try { - await installHldToManifestPipeline(getMockObject()); - expect(true).toBe(false); - } catch (err) { - expect(err).toBeDefined(); - } + (getBuildApiClient as jest.Mock).mockRejectedValueOnce(Error("fake Error")); + await expect( + installHldToManifestPipeline(getMockObject()) + ).rejects.toThrow(); }); it("should fail if the pipeline definition cannot be created", async () => { - (getBuildApiClient as jest.Mock).mockReturnValue({}); - (createPipelineForDefinition as jest.Mock).mockReturnValue( - Promise.reject("Error") + (getBuildApiClient as jest.Mock).mockReturnValueOnce({}); + (createPipelineForDefinition as jest.Mock).mockRejectedValueOnce( + Error("fake error") ); - try { - await installHldToManifestPipeline(getMockObject()); - expect(true).toBe(false); - } catch (err) { - expect(err).toBeDefined(); - } + await expect( + installHldToManifestPipeline(getMockObject()) + ).rejects.toThrow(); }); it("should fail if a build cannot be queued on the pipeline", async () => { - (getBuildApiClient as jest.Mock).mockReturnValue({}); - (createPipelineForDefinition as jest.Mock).mockReturnValue({ id: 10 }); - (queueBuild as jest.Mock).mockReturnValue(Promise.reject("Error")); - - try { - await installHldToManifestPipeline(getMockObject()); - expect(true).toBe(false); - } catch (err) { - expect(err).toBeDefined(); - } + (getBuildApiClient as jest.Mock).mockReturnValueOnce({}); + (createPipelineForDefinition as jest.Mock).mockReturnValueOnce({ id: 10 }); + (queueBuild as jest.Mock).mockRejectedValueOnce(Error("fake error")); + await expect( + installHldToManifestPipeline(getMockObject()) + ).rejects.toThrow(); }); }); diff --git a/src/commands/setup.md b/src/commands/setup.md index f745e6134..4aae59143 100644 --- a/src/commands/setup.md +++ b/src/commands/setup.md @@ -23,6 +23,9 @@ for a few questions It can also run in a non interactive mode by providing a file that contains answers to the above questions. +After this command is successfully executed, you can launch the introspection +dashboard to view the status of pipelines. + ``` spk setup --file ``` diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index 5f4dec438..ef566c991 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -95,6 +95,10 @@ describe("test createSPKConfig function", () => { project: "project", }); expect(data.introspection).toStrictEqual({ + dashboard: { + image: "mcr.microsoft.com/k8s/bedrock/spektate:latest", + name: "spektate", + }, azure: { service_principal_id: rc.servicePrincipalId, service_principal_secret: rc.servicePrincipalPassword, diff --git a/src/lib/setup/scaffold.test.ts b/src/lib/setup/scaffold.test.ts index d0df20910..35c2a2406 100644 --- a/src/lib/setup/scaffold.test.ts +++ b/src/lib/setup/scaffold.test.ts @@ -5,6 +5,7 @@ import simpleGit from "simple-git/promise"; import * as cmdCreateVariableGroup from "../../commands/project/create-variable-group"; import * as projectInit from "../../commands/project/init"; import * as createService from "../../commands/service/create"; +import * as fileutils from "../../lib/fileutils"; import * as variableGroup from "../../lib/pipelines/variableGroup"; import * as sVariableGroup from "../setup/variableGroup"; import { createTempDir } from "../ioUtil"; @@ -77,6 +78,10 @@ describe("test hldRepo function", () => { jest .spyOn(gitService, "commitAndPushToRemote") .mockReturnValueOnce({} as any); + jest + .spyOn(fileutils, "appendVariableGroupToPipelineYaml") + .mockReturnValueOnce(); + const git = simpleGit(); git.init = jest.fn(); diff --git a/src/lib/setup/scaffold.ts b/src/lib/setup/scaffold.ts index 61bc91e42..dd4da43ea 100644 --- a/src/lib/setup/scaffold.ts +++ b/src/lib/setup/scaffold.ts @@ -9,6 +9,8 @@ import { } from "../../commands/project/create-variable-group"; import { initialize as projectInitialize } from "../../commands/project/init"; import { createService } from "../../commands/service/create"; +import { RENDER_HLD_PIPELINE_FILENAME } from "../../lib/constants"; +import { appendVariableGroupToPipelineYaml } from "../../lib/fileutils"; import { AzureDevOpsOpts } from "../../lib/git"; import { deleteVariableGroup } from "../../lib/pipelines/variableGroup"; import { create as createVariableGroup } from "../../lib/setup/variableGroup"; @@ -112,6 +114,11 @@ export const hldRepo = async ( HLD_DEFAULT_COMPONENT_NAME, HLD_DEFAULT_DEF_PATH ); + appendVariableGroupToPipelineYaml( + process.cwd(), + RENDER_HLD_PIPELINE_FILENAME, + VARIABLE_GROUP + ); await git.add("./*"); await commitAndPushToRemote(git, rc, repoName); From 35363b8adee654bde1aa69cbd8d9d9f1da1410b7 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Fri, 27 Mar 2020 23:16:16 -0700 Subject: [PATCH 12/15] fix unit test --- src/commands/hld/pipeline.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/hld/pipeline.test.ts b/src/commands/hld/pipeline.test.ts index 71c3150a3..cf956e782 100644 --- a/src/commands/hld/pipeline.test.ts +++ b/src/commands/hld/pipeline.test.ts @@ -73,6 +73,7 @@ describe("test emptyStringIfUndefined function", () => { }); const orgNameTest = (hasVal: boolean): void => { + jest.spyOn(config, "Config").mockReturnValueOnce({}); const data = { buildScriptUrl: "", devopsProject: "project", From 6cec63dd514208e43d6ed3181df1373af0baa540 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sat, 28 Mar 2020 19:11:12 -0700 Subject: [PATCH 13/15] Update setup.test.ts --- src/commands/setup.test.ts | 108 +++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index ef566c991..56c37749a 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -120,7 +120,7 @@ const testExecuteFunc = async ( jest .spyOn(gitService, "getGitApi") // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockReturnValueOnce(Promise.resolve({} as any)); + .mockResolvedValueOnce({} as any); jest.spyOn(fsUtil, "createDirectory").mockReturnValueOnce(); jest.spyOn(scaffold, "hldRepo").mockResolvedValueOnce(); jest.spyOn(scaffold, "manifestRepo").mockResolvedValueOnce(); @@ -136,39 +136,37 @@ const testExecuteFunc = async ( if (usePrompt) { jest .spyOn(promptInstance, "prompt") - .mockReturnValueOnce(Promise.resolve(mockRequestContext)); + .mockResolvedValueOnce(mockRequestContext); } else { jest .spyOn(promptInstance, "getAnswerFromFile") .mockReturnValueOnce(mockRequestContext); } jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce(); - jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce( - Promise.resolve({ - getCoreApi: async () => { - return {}; - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - ); + jest.spyOn(azdoClient, "getWebApi").mockResolvedValueOnce({ + getCoreApi: async () => { + return {}; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); jest .spyOn(azdoClient, "getBuildApi") // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockReturnValueOnce(Promise.resolve({} as any)); + .mockResolvedValueOnce({} as any); if (hasProject) { jest .spyOn(projectService, "getProject") // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockReturnValueOnce(Promise.resolve({} as any)); + .mockResolvedValueOnce({} as any); } else { jest .spyOn(projectService, "getProject") // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockReturnValueOnce(Promise.resolve(undefined as any)); + .mockResolvedValueOnce(undefined as any); } const fncreateProject = jest .spyOn(projectService, "createProject") - .mockReturnValueOnce(Promise.resolve()); + .mockResolvedValueOnce(); if (usePrompt) { await execute( @@ -213,19 +211,17 @@ describe("test execute function", () => { const exitFn = jest.fn(); jest .spyOn(promptInstance, "prompt") - .mockReturnValueOnce(Promise.resolve(mockRequestContext)); + .mockResolvedValueOnce(mockRequestContext); jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce(); - jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce( - Promise.resolve({ - getCoreApi: () => { - throw { - message: "Authentication failure", - statusCode: 401, - }; - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - ); + jest.spyOn(azdoClient, "getWebApi").mockResolvedValueOnce({ + getCoreApi: () => { + throw { + message: "Authentication failure", + statusCode: 401, + }; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); jest.spyOn(setupLog, "create").mockReturnValueOnce(); await execute( @@ -243,18 +239,16 @@ describe("test execute function", () => { jest .spyOn(promptInstance, "prompt") - .mockReturnValueOnce(Promise.resolve(mockRequestContext)); + .mockResolvedValueOnce(mockRequestContext); jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce(); - jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce( - Promise.resolve({ - getCoreApi: () => { - throw { - message: "VS402392: ", - }; - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - ); + jest.spyOn(azdoClient, "getWebApi").mockResolvedValueOnce({ + getCoreApi: () => { + throw { + message: "VS402392: ", + }; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); jest.spyOn(setupLog, "create").mockReturnValueOnce(); await execute( @@ -272,18 +266,16 @@ describe("test execute function", () => { jest .spyOn(promptInstance, "prompt") - .mockReturnValueOnce(Promise.resolve(mockRequestContext)); + .mockResolvedValueOnce(mockRequestContext); jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce(); - jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce( - Promise.resolve({ - getCoreApi: () => { - throw { - message: "other error", - }; - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - ); + jest.spyOn(azdoClient, "getWebApi").mockResolvedValueOnce({ + getCoreApi: () => { + throw { + message: "other error", + }; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); jest.spyOn(setupLog, "create").mockReturnValueOnce(); await execute( @@ -301,18 +293,16 @@ describe("test execute function", () => { jest .spyOn(promptInstance, "prompt") - .mockReturnValueOnce(Promise.resolve(mockRequestContext)); + .mockResolvedValueOnce(mockRequestContext); jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce(); - jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce( - Promise.resolve({ - getCoreApi: () => { - throw { - message: "other error", - }; - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - ); + jest.spyOn(azdoClient, "getWebApi").mockResolvedValueOnce({ + getCoreApi: () => { + throw { + message: "other error", + }; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); await execute( { file: undefined, From 41929113a27ff8a3d22ba84068562c68a72e7ac9 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sun, 29 Mar 2020 15:53:45 -0700 Subject: [PATCH 14/15] Update validator.test.ts --- src/lib/validator.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/lib/validator.test.ts b/src/lib/validator.test.ts index a1e9814bf..b55e9370b 100644 --- a/src/lib/validator.test.ts +++ b/src/lib/validator.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - import path from "path"; import { Config, loadConfiguration } from "../config"; import { @@ -117,9 +115,15 @@ const testValidatePrereqs = ( if (global) { const config = Config(); - expect(config.infra!).toBeDefined(); - expect(config.infra!.checks).toBeDefined(); - expect(config.infra!.checks![cmd]!).toBe(expectedResult); + expect(config.infra).toBeDefined(); + + if (config.infra) { + expect(config.infra.checks).toBeDefined(); + + if (config.infra.checks) { + expect(config.infra.checks[cmd]).toBe(expectedResult); + } + } } else { expect(result).toBe(expectedResult); } From eddde6e4544e854c4dfe58a12cf022b0f92a2e9e Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sun, 29 Mar 2020 16:02:17 -0700 Subject: [PATCH 15/15] Update pipeline.test.ts --- src/commands/hld/pipeline.test.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/commands/hld/pipeline.test.ts b/src/commands/hld/pipeline.test.ts index cf956e782..698643b89 100644 --- a/src/commands/hld/pipeline.test.ts +++ b/src/commands/hld/pipeline.test.ts @@ -58,7 +58,7 @@ afterAll(() => { disableVerboseLogging(); }); -jest.spyOn(azdo, "validateRepository").mockReturnValue(Promise.resolve()); +jest.spyOn(azdo, "validateRepository").mockResolvedValue(); describe("test emptyStringIfUndefined function", () => { it("pass in undefined", () => { @@ -242,28 +242,28 @@ describe("create hld to manifest pipeline test", () => { }); it("should fail if the build client cant be instantiated", async () => { - (getBuildApiClient as jest.Mock).mockRejectedValueOnce(Error("fake Error")); - await expect( - installHldToManifestPipeline(getMockObject()) - ).rejects.toThrow(); + (getBuildApiClient as jest.Mock).mockRejectedValueOnce(Error("Error")); + await expect(installHldToManifestPipeline(getMockObject())).rejects.toThrow( + "Error" + ); }); it("should fail if the pipeline definition cannot be created", async () => { (getBuildApiClient as jest.Mock).mockReturnValueOnce({}); (createPipelineForDefinition as jest.Mock).mockRejectedValueOnce( - Error("fake error") + Error("Error") + ); + await expect(installHldToManifestPipeline(getMockObject())).rejects.toThrow( + "Error" ); - await expect( - installHldToManifestPipeline(getMockObject()) - ).rejects.toThrow(); }); it("should fail if a build cannot be queued on the pipeline", async () => { (getBuildApiClient as jest.Mock).mockReturnValueOnce({}); (createPipelineForDefinition as jest.Mock).mockReturnValueOnce({ id: 10 }); - (queueBuild as jest.Mock).mockRejectedValueOnce(Error("fake error")); - await expect( - installHldToManifestPipeline(getMockObject()) - ).rejects.toThrow(); + (queueBuild as jest.Mock).mockRejectedValueOnce(Error("Error")); + await expect(installHldToManifestPipeline(getMockObject())).rejects.toThrow( + "Error" + ); }); });