diff --git a/package.json b/package.json index 94f80ea72..e3195204b 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,7 @@ "webpack-cli": "^3.3.9" }, "prettier": { - "proseWrap": "always", - "quote-props": "preserve" + "proseWrap": "always" }, "husky": { "hooks": { diff --git a/src/commands/deployment/dashboard.test.ts b/src/commands/deployment/dashboard.test.ts index 4dd933522..7a8d64c8e 100644 --- a/src/commands/deployment/dashboard.test.ts +++ b/src/commands/deployment/dashboard.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/camelcase */ jest.mock("open"); import open from "open"; jest.mock("../../config"); @@ -31,18 +30,18 @@ afterAll(() => { const mockConfig = (): void => { (Config as jest.Mock).mockReturnValueOnce({ - azure_devops: { - access_token: uuid(), + "azure_devops": { + "access_token": uuid(), org: uuid(), project: uuid() }, introspection: { azure: { - account_name: uuid(), + "account_name": uuid(), key: uuid(), - partition_key: uuid(), - source_repo_access_token: "test_token", - table_name: uuid() + "partition_key": uuid(), + "source_repo_access_token": "test_token", + "table_name": uuid() } } }); @@ -209,8 +208,8 @@ describe("Fallback to azure devops access token", () => { describe("Extract manifest repository information", () => { test("Manifest repository information is successfully extracted", () => { (Config as jest.Mock).mockReturnValue({ - azure_devops: { - manifest_repository: + "azure_devops": { + "manifest_repository": "https://dev.azure.com/bhnook/fabrikam/_git/materialized" } }); diff --git a/src/commands/deployment/validate.test.ts b/src/commands/deployment/validate.test.ts index 6ea33ded3..bfa690b2e 100644 --- a/src/commands/deployment/validate.test.ts +++ b/src/commands/deployment/validate.test.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/camelcase */ -// imports import uuid from "uuid/v4"; import * as deploymenttable from "../../lib/azure/deploymenttable"; import { diff --git a/src/commands/infra/generate.ts b/src/commands/infra/generate.ts index 4a143cbc4..b0b6e2204 100644 --- a/src/commands/infra/generate.ts +++ b/src/commands/infra/generate.ts @@ -336,14 +336,14 @@ export const generateConfigWithParentEqProjectPath = async ( writeTfvarsFile(spkTfvarsObject, parentDirectory, SPK_TFVARS); await copyTfTemplate(templatePath, parentDirectory, true); } else { - logger.warning(`Variables are not defined in the definition.yaml`); + logger.warn(`Variables are not defined in the definition.yaml`); } if (parentInfraConfig.backend) { const backendTfvarsObject = generateTfvars(parentInfraConfig.backend); checkTfvars(parentDirectory, BACKEND_TFVARS); writeTfvarsFile(backendTfvarsObject, parentDirectory, BACKEND_TFVARS); } else { - logger.warning( + logger.warn( `A remote backend configuration is not defined in the definition.yaml` ); } diff --git a/src/commands/setup.md b/src/commands/setup.md index 0a3829750..1ea6def89 100644 --- a/src/commands/setup.md +++ b/src/commands/setup.md @@ -16,11 +16,9 @@ for a few questions 4. To create a sample application Repo 1. If Yes, a Azure Service Principal is needed. You have 2 options 1. have the command line tool to create it. Azure command line tool shall - be used - 2. provide the Service Principal Id, Password and Tenant Id. - 2. Subscription Id is automatically retrieved with the Service Principal - credential. In case, there are two or more subscriptions, you will be - prompt to select one of them. + be used. You will be prompted to select a subscription identifier. + 2. Provide the Service Principal Id, Password, and Tenant Id. From this + information, the tool will retrieve the subscription identifier. It can also run in a non interactive mode by providing a file that contains answers to the above questions. @@ -40,6 +38,8 @@ az_create_sp= az_sp_id= az_sp_password= az_sp_tenant= +az_subscription_id= +az_acr_name= ``` `azdo_project_name` is optional and default value is `BedrockRocks`. @@ -64,6 +64,8 @@ The followings shall be created already exists. 5. A Git Repo, `quick-start-app`, it shall be deleted and recreated if is already exists. + 6. A Lifecycle pipeline. + 7. A Build pipeline. ## Setup log diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index 6b7d5c4f0..67a34d58e 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -16,7 +16,12 @@ import * as scaffold from "../lib/setup/scaffold"; import * as setupLog from "../lib/setup/setupLog"; import { deepClone } from "../lib/util"; import { ConfigYaml } from "../types"; -import { createSPKConfig, execute, getErrorMessage } from "./setup"; +import { + createAppRepoTasks, + createSPKConfig, + execute, + getErrorMessage +} from "./setup"; import * as setup from "./setup"; const mockRequestContext: RequestContext = { @@ -83,11 +88,9 @@ const testExecuteFunc = async ( jest.spyOn(fsUtil, "createDirectory").mockReturnValueOnce(); jest.spyOn(scaffold, "hldRepo").mockResolvedValueOnce(); jest.spyOn(scaffold, "manifestRepo").mockResolvedValueOnce(); - jest.spyOn(scaffold, "helmRepo").mockResolvedValueOnce(); - jest.spyOn(scaffold, "appRepo").mockResolvedValueOnce(); jest .spyOn(pipelineService, "createHLDtoManifestPipeline") - .mockReturnValueOnce(Promise.resolve()); + .mockResolvedValueOnce(); jest.spyOn(resourceService, "create").mockResolvedValue(true); jest.spyOn(azureContainerRegistryService, "create").mockResolvedValue(true); jest.spyOn(setupLog, "create").mockReturnValueOnce(); @@ -309,3 +312,50 @@ describe("test getErrorMessage function", () => { ); }); }); + +const testCreateAppRepoTasks = async (prApproved = true): Promise => { + const mockRc: RequestContext = { + orgName: "org", + projectName: "project", + accessToken: "pat", + toCreateAppRepo: true, + servicePrincipalId: "fakeId", + servicePrincipalPassword: "fakePassword", + servicePrincipalTenantId: "tenant", + subscriptionId: "12344", + acrName: "acr", + workspace: "dummy" + }; + + jest.spyOn(resourceService, "create").mockResolvedValueOnce(true); + jest + .spyOn(azureContainerRegistryService, "create") + .mockResolvedValueOnce(true); + jest.spyOn(scaffold, "helmRepo").mockResolvedValueOnce(); + jest.spyOn(scaffold, "appRepo").mockResolvedValueOnce(); + jest + .spyOn(pipelineService, "createLifecyclePipeline") + .mockResolvedValueOnce(); + jest + .spyOn(promptInstance, "promptForApprovingHLDPullRequest") + .mockResolvedValueOnce(prApproved); + if (prApproved) { + jest.spyOn(pipelineService, "createBuildPipeline").mockResolvedValueOnce(); + } + + const res = await createAppRepoTasks( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, // gitAPI + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, // buildAPI + mockRc + ); + expect(res).toBe(prApproved); +}; + +describe("test createAppRepoTasks function", () => { + it("positive test", async () => { + await testCreateAppRepoTasks(); + await testCreateAppRepoTasks(false); + }); +}); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index fb2d5f7ca..484b78e8c 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/camelcase */ +import { IBuildApi } from "azure-devops-node-api/BuildApi"; +import { IGitApi } from "azure-devops-node-api/GitApi"; import commander from "commander"; import fs from "fs"; import yaml from "js-yaml"; @@ -15,9 +17,17 @@ import { } from "../lib/setup/constants"; import { createDirectory } from "../lib/setup/fsUtil"; import { getGitApi } from "../lib/setup/gitService"; -import { createHLDtoManifestPipeline } from "../lib/setup/pipelineService"; +import { + createBuildPipeline, + createHLDtoManifestPipeline, + createLifecyclePipeline +} from "../lib/setup/pipelineService"; import { createProjectIfNotExist } from "../lib/setup/projectService"; -import { getAnswerFromFile, prompt } from "../lib/setup/prompt"; +import { + getAnswerFromFile, + prompt, + promptForApprovingHLDPullRequest +} from "../lib/setup/prompt"; import { appRepo, helmRepo, @@ -27,7 +37,6 @@ import { import { create as createSetupLog } from "../lib/setup/setupLog"; import { logger } from "../logger"; import decorator from "./setup.decorator.json"; -import { IGitApi } from "azure-devops-node-api/GitApi"; interface CommandOptions { file: string | undefined; @@ -87,8 +96,9 @@ export const getErrorMessage = ( export const createAppRepoTasks = async ( gitAPI: IGitApi, + buildAPI: IBuildApi, rc: RequestContext -): Promise => { +): Promise => { if ( rc.toCreateAppRepo && rc.servicePrincipalId && @@ -116,6 +126,18 @@ export const createAppRepoTasks = async ( ); await helmRepo(gitAPI, rc); await appRepo(gitAPI, rc); + await createLifecyclePipeline(buildAPI, rc); + const approved = await promptForApprovingHLDPullRequest(rc); + + if (approved) { + await createBuildPipeline(buildAPI, rc); + return true; + } + + logger.warn("HLD Pull Request is not approved."); + return false; + } else { + return false; } }; @@ -134,21 +156,23 @@ 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!; createDirectory(WORKSPACE, true); - createSPKConfig(requestContext); + createSPKConfig(rc); const webAPI = await getWebApi(); const coreAPI = await webAPI.getCoreApi(); const gitAPI = await getGitApi(webAPI); const buildAPI = await getBuildApi(); - await createProjectIfNotExist(coreAPI, requestContext); - await hldRepo(gitAPI, requestContext); - await manifestRepo(gitAPI, requestContext); - await createHLDtoManifestPipeline(buildAPI, requestContext); - await createAppRepoTasks(gitAPI, requestContext); + await createProjectIfNotExist(coreAPI, rc); + await hldRepo(gitAPI, rc); + await manifestRepo(gitAPI, rc); + await createHLDtoManifestPipeline(buildAPI, rc); + await createAppRepoTasks(gitAPI, buildAPI, rc); - createSetupLog(requestContext); + createSetupLog(rc); await exitFn(0); } catch (err) { const msg = getErrorMessage(requestContext, err); diff --git a/src/lib/azure/containerRegistryService.test.ts b/src/lib/azure/containerRegistryService.test.ts index 2ce1c6dd9..927461fed 100644 --- a/src/lib/azure/containerRegistryService.test.ts +++ b/src/lib/azure/containerRegistryService.test.ts @@ -7,6 +7,7 @@ import * as restAuth from "@azure/ms-rest-nodeauth"; import { create, getContainerRegistries, + getContainerRegistry, isExist } from "./containerRegistryService"; import * as containerRegistryService from "./containerRegistryService"; @@ -109,7 +110,6 @@ describe("test container registries function", () => { servicePrincipalPassword, servicePrincipalTenantId, subscriptionId, - RESOURCE_GROUP, "test" ); expect(res).toBeTruthy(); @@ -123,7 +123,6 @@ describe("test container registries function", () => { servicePrincipalPassword, servicePrincipalTenantId, subscriptionId, - RESOURCE_GROUP, "test" ); expect(res).toBeFalsy(); @@ -143,7 +142,6 @@ describe("test container registries function", () => { servicePrincipalPassword, servicePrincipalTenantId, subscriptionId, - RESOURCE_GROUP, "test" ); expect(res).toBeFalsy(); @@ -177,3 +175,40 @@ describe("test container registries function", () => { expect(created).toBeTruthy(); }); }); + +describe("test getContainerRegistry function", () => { + it("match", async () => { + const entry = { + id: + "/subscriptions/dd831253-787f-4dc8-8eb0-ac9d052177d9/resourceGroups/quick-start-rg/providers/Microsoft.ContainerRegistry/registries/quickStartACR", + name: "quickStartACR", + resourceGroup: "quick-start-rg" + }; + jest + .spyOn(containerRegistryService, "getContainerRegistries") + .mockResolvedValueOnce([entry]); + const reg = await getContainerRegistry( + servicePrincipalId, + servicePrincipalPassword, + servicePrincipalTenantId, + subscriptionId, + RESOURCE_GROUP, + "quickStartACR" + ); + expect(reg).toStrictEqual(entry); + }); + it("no matches", async () => { + jest + .spyOn(containerRegistryService, "getContainerRegistries") + .mockResolvedValueOnce([]); + const reg = await getContainerRegistry( + servicePrincipalId, + servicePrincipalPassword, + servicePrincipalTenantId, + subscriptionId, + RESOURCE_GROUP, + "quickStartACR" + ); + expect(reg).toBeUndefined(); + }); +}); diff --git a/src/lib/azure/containerRegistryService.ts b/src/lib/azure/containerRegistryService.ts index 7fcf336ee..99a8b4bd5 100644 --- a/src/lib/azure/containerRegistryService.ts +++ b/src/lib/azure/containerRegistryService.ts @@ -74,6 +74,33 @@ export const getContainerRegistries = async ( }); }; +/** + * Returns container registry with matching name + * + * @param servicePrincipalId Service Principal Id + * @param servicePrincipalPassword Service Principal Password + * @param servicePrincipalTenantId Service Principal Tenant Id + * @param subscriptionId Subscription Id + */ +export const getContainerRegistry = async ( + servicePrincipalId: string, + servicePrincipalPassword: string, + servicePrincipalTenantId: string, + subscriptionId: string, + resourceGroup: string, + name: string +): Promise => { + const registries = await getContainerRegistries( + servicePrincipalId, + servicePrincipalPassword, + servicePrincipalTenantId, + subscriptionId + ); + return registries.find( + r => r.resourceGroup === resourceGroup && r.name === name + ); +}; + /** * Returns true of container register exists * @@ -89,7 +116,6 @@ export const isExist = async ( servicePrincipalPassword: string, servicePrincipalTenantId: string, subscriptionId: string, - resourceGroup: string, name: string ): Promise => { const registries = await getContainerRegistries( @@ -100,7 +126,7 @@ export const isExist = async ( ); return (registries || []).some( - r => r.resourceGroup === resourceGroup && r.name === name + r => r.name === name // ACR name will be unique across Azure so only check the name. ); }; @@ -131,13 +157,12 @@ export const create = async ( servicePrincipalPassword, servicePrincipalTenantId, subscriptionId, - resourceGroup, name ); if (exist) { logger.info( - `Azure container registry, ${name} in ${resourceGroup} already existed` + `Azure container registry, ${name} already exists in subscription` ); return false; } diff --git a/src/lib/azure/servicePrincipalService.test.ts b/src/lib/azure/servicePrincipalService.test.ts index 426d41c72..7bf2f559b 100644 --- a/src/lib/azure/servicePrincipalService.test.ts +++ b/src/lib/azure/servicePrincipalService.test.ts @@ -1,16 +1,20 @@ import * as shell from "../shell"; import { azCLILogin, createWithAzCLI } from "./servicePrincipalService"; -import * as servicePrincipalService from "./servicePrincipalService"; describe("test azCLILogin function", () => { it("positive test", async () => { - jest.spyOn(shell, "exec").mockReturnValueOnce(Promise.resolve("")); + jest.spyOn(shell, "exec").mockResolvedValueOnce( + JSON.stringify([ + { + id: "subid", + name: "subname" + } + ]) + ); await azCLILogin(); }); it("negative test", async () => { - jest - .spyOn(shell, "exec") - .mockReturnValueOnce(Promise.reject(new Error("fake"))); + jest.spyOn(shell, "exec").mockRejectedValueOnce(Error("fake")); await expect(azCLILogin()).rejects.toThrow(); }); }); @@ -22,20 +26,14 @@ describe("test createWithAzCLI function", () => { password: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", tenant: "72f988bf-86f1-41af-91ab-2d7cd011db47" }; - jest - .spyOn(servicePrincipalService, "azCLILogin") - .mockReturnValueOnce(Promise.resolve()); - jest - .spyOn(shell, "exec") - .mockReturnValueOnce(Promise.resolve(JSON.stringify(result))); - const sp = await createWithAzCLI(); - expect(sp.id).toBe(result.appId); - expect(sp.password).toBe(result.password); - expect(sp.tenantId).toBe(result.tenant); + jest.spyOn(shell, "exec").mockResolvedValueOnce(JSON.stringify(result)); + const sub = await createWithAzCLI("subscriptionId"); + expect(sub.id).toBe(result.appId); + expect(sub.password).toBe(result.password); + expect(sub.tenantId).toBe(result.tenant); }); it("negative test", async () => { - jest.spyOn(servicePrincipalService, "azCLILogin").mockResolvedValueOnce(); jest.spyOn(shell, "exec").mockRejectedValueOnce(Error("fake")); - await expect(createWithAzCLI()).rejects.toThrow(); + await expect(createWithAzCLI("subscriptionId")).rejects.toThrow(); }); }); diff --git a/src/lib/azure/servicePrincipalService.ts b/src/lib/azure/servicePrincipalService.ts index d2287aba6..cc8a03e61 100644 --- a/src/lib/azure/servicePrincipalService.ts +++ b/src/lib/azure/servicePrincipalService.ts @@ -7,16 +7,27 @@ export interface ServicePrincipal { tenantId: string; } +export interface SubscriptionData { + id: string; + name: string; +} + /** * Login to az command line tool. This is done by * doing a shell exec with `az login`; then browser opens * prompting user to select the identity. */ -export const azCLILogin = async (): Promise => { +export const azCLILogin = async (): Promise => { try { logger.info("attempting to login to az command line"); - await exec("az", ["login"]); + const result = await exec("az", ["login"]); logger.info("Successfully login to az command line"); + return JSON.parse(result).map((item: SubscriptionData) => { + return { + id: item.id, + name: item.name + }; + }); } catch (err) { logger.error("Unable to execute az login"); logger.error(err); @@ -30,11 +41,18 @@ export const azCLILogin = async (): Promise => { * Request context will have the service principal information * when service principal is successfully created. */ -export const createWithAzCLI = async (): Promise => { - await azCLILogin(); +export const createWithAzCLI = async ( + subscriptionId: string +): Promise => { try { logger.info("attempting to create service principal with az command line"); - const result = await exec("az", ["ad", "sp", "create-for-rbac"]); + const result = await exec("az", [ + "ad", + "sp", + "create-for-rbac", + "--scope", + `/subscriptions/${subscriptionId}` + ]); const oResult = JSON.parse(result); logger.info("Successfully created service principal with az command line"); return { diff --git a/src/lib/azure/subscriptionService.test.ts b/src/lib/azure/subscriptionService.test.ts index 295f701a7..399d57656 100644 --- a/src/lib/azure/subscriptionService.test.ts +++ b/src/lib/azure/subscriptionService.test.ts @@ -51,6 +51,9 @@ describe("test getSubscriptions function", () => { } ]); }); + it("negative test: missing values", async () => { + await expect(getSubscriptions("", "", "")).rejects.toThrow(); + }); it("negative test", async () => { jest .spyOn(restAuth, "loginWithServicePrincipalSecret") diff --git a/src/lib/azure/subscriptionService.ts b/src/lib/azure/subscriptionService.ts index 13749e7d1..507f85d77 100644 --- a/src/lib/azure/subscriptionService.ts +++ b/src/lib/azure/subscriptionService.ts @@ -1,8 +1,5 @@ import { SubscriptionClient } from "@azure/arm-subscriptions"; -import { - ApplicationTokenCredentials, - loginWithServicePrincipalSecret -} from "@azure/ms-rest-nodeauth"; +import { loginWithServicePrincipalSecret } from "@azure/ms-rest-nodeauth"; import { logger } from "../../logger"; export interface SubscriptionItem { @@ -17,44 +14,36 @@ export interface SubscriptionItem { * @param servicePrincipalPassword Service Principal Password * @param servicePrincipalTenantId Service Principal TenantId */ -export const getSubscriptions = ( +export const getSubscriptions = async ( servicePrincipalId: string, servicePrincipalPassword: string, servicePrincipalTenantId: string ): Promise => { logger.info("attempting to get subscription list"); - return new Promise((resolve, reject) => { - if ( - !servicePrincipalId || - !servicePrincipalPassword || - !servicePrincipalTenantId - ) { - reject(Error("Service Principal information was missing.")); - } else { - loginWithServicePrincipalSecret( - servicePrincipalId, - servicePrincipalPassword, - servicePrincipalTenantId - ) - .then(async (creds: ApplicationTokenCredentials) => { - const client = new SubscriptionClient(creds); - const subsciptions = await client.subscriptions.list(); - const result: SubscriptionItem[] = []; + if ( + !servicePrincipalId || + !servicePrincipalPassword || + !servicePrincipalTenantId + ) { + throw Error("Service Principal information was missing."); + } + const creds = await loginWithServicePrincipalSecret( + servicePrincipalId, + servicePrincipalPassword, + servicePrincipalTenantId + ); + const client = new SubscriptionClient(creds); + const subsciptions = await client.subscriptions.list(); + const result: SubscriptionItem[] = []; - (subsciptions || []).forEach(s => { - if (s.subscriptionId && s.displayName) { - result.push({ - id: s.subscriptionId, - name: s.displayName - }); - } - }); - logger.info("Successfully acquired subscription list"); - resolve(result); - }) - .catch(err => { - reject(err); - }); + (subsciptions || []).forEach(s => { + if (s.subscriptionId && s.displayName) { + result.push({ + id: s.subscriptionId, + name: s.displayName + }); } }); + logger.info("Successfully acquired subscription list"); + return result; }; diff --git a/src/lib/setup/constants.ts b/src/lib/setup/constants.ts index 6271c8fb1..f246db274 100644 --- a/src/lib/setup/constants.ts +++ b/src/lib/setup/constants.ts @@ -12,6 +12,8 @@ export interface RequestContext { scaffoldHelm?: boolean; scaffoldAppService?: boolean; createdHLDtoManifestPipeline?: boolean; + createdLifecyclePipeline?: boolean; + createdBuildPipeline?: boolean; createServicePrincipal?: boolean; servicePrincipalId?: string; servicePrincipalPassword?: string; @@ -34,9 +36,8 @@ export const RESOURCE_GROUP = "quick-start-rg"; 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 SETUP_LOG = "setup.log"; -export const HLD_DEFAULT_GIT_URL = - "https://github.com/microsoft/fabrikate-definitions.git"; export const HLD_DEFAULT_COMPONENT_NAME = "traefik2"; export const HLD_DEFAULT_DEF_PATH = "definitions/traefik2"; diff --git a/src/lib/setup/helmTemplates.ts b/src/lib/setup/helmTemplates.ts index 4803c5d1d..333cc9556 100644 --- a/src/lib/setup/helmTemplates.ts +++ b/src/lib/setup/helmTemplates.ts @@ -11,6 +11,8 @@ image: tag: latest pullPolicy: IfNotPresent +serviceName: "service" + service: type: ClusterIP port: 80 @@ -21,14 +23,13 @@ export const mainTemplate = `--- apiVersion: apps/v1 kind: Deployment metadata: - name: { { .Chart.Name } } + name: {{ .Chart.Name }} spec: - replicas: { { .Values.replicaCount } } + replicas: {{ .Values.replicaCount }} selector: matchLabels: - app.kubernetes.io/name: { { .Chart.Name } } - app.kubernetes.io/instance: { { .Release.Name } } - minReadySeconds: { { .Values.minReadySeconds } } + app: {{ .Values.serviceName }} + minReadySeconds: {{ .Values.minReadySeconds }} strategy: type: RollingUpdate # describe how we do rolling updates rollingUpdate: @@ -37,31 +38,23 @@ spec: template: metadata: labels: - app: { { .Chart.Name } } - app.kubernetes.io/name: { { .Chart.Name } } - app.kubernetes.io/instance: { { .Release.Name } } - annotations: - prometheus.io/port: "{{ .Values.service.containerPort}}" - prometheus.io/scrape: "true" + app: {{ .Values.serviceName }} spec: containers: - - name: { { .Chart.Name } } + - name: {{ .Values.serviceName }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: { { .Values.image.pullPolicy } } + imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - - containerPort: { { .Values.service.containerPort } } + - containerPort: {{ .Values.service.containerPort }} --- apiVersion: v1 kind: Service metadata: - name: { { .Chart.Name } } - labels: - app: { { .Chart.Name } } + name: {{ .Values.serviceName }} spec: - type: LoadBalancer ports: - - port: 8080 - name: http + - port: {{ .Values.service.port }} + protocol: TCP selector: - app: { { .Chart.Name } } + app: {{ .Values.serviceName }} `; diff --git a/src/lib/setup/pipelineService.test.ts b/src/lib/setup/pipelineService.test.ts index 633cf2aaa..a34d5ca0b 100644 --- a/src/lib/setup/pipelineService.test.ts +++ b/src/lib/setup/pipelineService.test.ts @@ -1,10 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { BuildStatus } from "azure-devops-node-api/interfaces/BuildInterfaces"; import * as hldPipeline from "../../commands/hld/pipeline"; +import * as projectPipeline from "../../commands/project/pipeline"; +import * as servicePipeline from "../../commands/service/pipeline"; import { deepClone } from "../util"; import { RequestContext, WORKSPACE } from "./constants"; import { + createBuildPipeline, createHLDtoManifestPipeline, + createLifecyclePipeline, deletePipeline, getBuildStatusString, getPipelineBuild, @@ -270,3 +274,97 @@ describe("test createHLDtoManifestPipeline function", () => { await expect(createHLDtoManifestPipeline({} as any, rc)).rejects.toThrow(); }); }); + +describe("test createLifecyclePipeline function", () => { + it("positive test: pipeline does not exist previously", async () => { + jest + .spyOn(pipelineService, "getPipelineByName") + .mockResolvedValueOnce(undefined); + jest + .spyOn(projectPipeline, "installLifecyclePipeline") + .mockResolvedValueOnce(); + jest + .spyOn(pipelineService, "pollForPipelineStatus") + .mockReturnValueOnce(Promise.resolve()); + + const rc = getMockRequestContext(); + await createLifecyclePipeline({} as any, rc); + expect(rc.createdLifecyclePipeline).toBeTruthy(); + }); + it("positive test: pipeline already exists", async () => { + jest.spyOn(pipelineService, "getPipelineByName").mockReturnValueOnce( + Promise.resolve({ + id: 1 + }) + ); + const fnDeletePipeline = jest + .spyOn(pipelineService, "deletePipeline") + .mockReturnValueOnce(Promise.resolve()); + jest + .spyOn(projectPipeline, "installLifecyclePipeline") + .mockResolvedValueOnce(); + jest + .spyOn(pipelineService, "pollForPipelineStatus") + .mockReturnValueOnce(Promise.resolve()); + + const rc = getMockRequestContext(); + await createLifecyclePipeline({} as any, rc); + expect(rc.createdLifecyclePipeline).toBeTruthy(); + expect(fnDeletePipeline).toBeCalledTimes(1); + fnDeletePipeline.mockReset(); + }); + it("negative test", async () => { + jest + .spyOn(pipelineService, "getPipelineByName") + .mockReturnValueOnce(Promise.reject(Error("fake"))); + const rc = getMockRequestContext(); + await expect(createLifecyclePipeline({} as any, rc)).rejects.toThrow(); + }); +}); + +describe("test createBuildPipeline function", () => { + it("positive test: pipeline does not exist previously", async () => { + jest + .spyOn(pipelineService, "getPipelineByName") + .mockResolvedValueOnce(undefined); + jest + .spyOn(servicePipeline, "installBuildUpdatePipeline") + .mockResolvedValueOnce(); + jest + .spyOn(pipelineService, "pollForPipelineStatus") + .mockReturnValueOnce(Promise.resolve()); + + const rc = getMockRequestContext(); + await createBuildPipeline({} as any, rc); + expect(rc.createdBuildPipeline).toBeTruthy(); + }); + it("positive test: pipeline already exists", async () => { + jest.spyOn(pipelineService, "getPipelineByName").mockReturnValueOnce( + Promise.resolve({ + id: 1 + }) + ); + const fnDeletePipeline = jest + .spyOn(pipelineService, "deletePipeline") + .mockReturnValueOnce(Promise.resolve()); + jest + .spyOn(servicePipeline, "installBuildUpdatePipeline") + .mockResolvedValueOnce(); + jest + .spyOn(pipelineService, "pollForPipelineStatus") + .mockReturnValueOnce(Promise.resolve()); + + const rc = getMockRequestContext(); + await createBuildPipeline({} as any, rc); + expect(rc.createdBuildPipeline).toBeTruthy(); + expect(fnDeletePipeline).toBeCalledTimes(1); + fnDeletePipeline.mockReset(); + }); + it("negative test", async () => { + jest + .spyOn(pipelineService, "getPipelineByName") + .mockReturnValueOnce(Promise.reject(Error("fake"))); + const rc = getMockRequestContext(); + await expect(createBuildPipeline({} as any, rc)).rejects.toThrow(); + }); +}); diff --git a/src/lib/setup/pipelineService.ts b/src/lib/setup/pipelineService.ts index 98b183efe..e3cccf3ab 100644 --- a/src/lib/setup/pipelineService.ts +++ b/src/lib/setup/pipelineService.ts @@ -4,11 +4,24 @@ import { BuildDefinitionReference, BuildStatus } from "azure-devops-node-api/interfaces/BuildInterfaces"; +import path from "path"; import { installHldToManifestPipeline } from "../../commands/hld/pipeline"; -import { BUILD_SCRIPT_URL } from "../../lib/constants"; +import { installLifecyclePipeline } from "../../commands/project/pipeline"; +import { installBuildUpdatePipeline } from "../../commands/service/pipeline"; +import { + BUILD_SCRIPT_URL, + SERVICE_PIPELINE_FILENAME +} from "../../lib/constants"; import { sleep } from "../../lib/util"; import { logger } from "../../logger"; -import { HLD_REPO, RequestContext, MANIFEST_REPO } from "./constants"; +import { + APP_REPO, + APP_REPO_BUILD, + APP_REPO_LIFECYCLE, + HLD_REPO, + RequestContext, + MANIFEST_REPO +} from "./constants"; import { getAzureRepoUrl } from "./gitService"; /** @@ -149,6 +162,22 @@ export const pollForPipelineStatus = async ( } while (!build || build.result === 0); }; +const deletePipelineIfExist = async ( + buildApi: IBuildApi, + rc: RequestContext, + pipelineName: string +): Promise => { + const pipeline = await getPipelineByName( + buildApi, + rc.projectName, + pipelineName + ); + if (pipeline && pipeline.id) { + logger.info(`Pipeline ${pipelineName} was found - deleting pipeline`); + await deletePipeline(buildApi, rc.projectName, pipelineName, pipeline.id); + } +}; + /** * Creates HLD to Manifest pipeline * @@ -168,15 +197,7 @@ export const createHLDtoManifestPipeline = async ( const pipelineName = `${HLD_REPO}-to-${MANIFEST_REPO}`; try { - const pipeline = await getPipelineByName( - buildApi, - rc.projectName, - pipelineName - ); - if (pipeline && pipeline.id !== undefined) { - logger.info(`${pipelineName} is found, deleting it`); - await deletePipeline(buildApi, rc.projectName, pipelineName, pipeline.id); - } + await deletePipelineIfExist(buildApi, rc, pipelineName); await installHldToManifestPipeline({ buildScriptUrl: BUILD_SCRIPT_URL, devopsProject: rc.projectName, @@ -195,3 +216,73 @@ export const createHLDtoManifestPipeline = async ( throw err; } }; + +/** + * Creates Lifecycle pipeline + * + * @param buildApi Build API client + * @param rc Request context + */ +export const createLifecyclePipeline = async ( + buildApi: IBuildApi, + rc: RequestContext +): Promise => { + const pipelineName = APP_REPO_LIFECYCLE; + + try { + await deletePipelineIfExist(buildApi, rc, pipelineName); + + await installLifecyclePipeline({ + buildScriptUrl: BUILD_SCRIPT_URL, + devopsProject: rc.projectName, + orgName: rc.orgName, + personalAccessToken: rc.accessToken, + pipelineName, + repoName: APP_REPO, + repoUrl: getAzureRepoUrl(rc.orgName, rc.projectName, APP_REPO), + yamlFileBranch: "master" + }); + await pollForPipelineStatus(buildApi, rc.projectName, pipelineName); + rc.createdLifecyclePipeline = true; + } catch (err) { + logger.error(`An error occured in create Lifecycle Pipeline`); + throw err; + } +}; + +/** + * Creates Build pipeline + * + * @param buildApi Build API client + * @param rc Request context + */ +export const createBuildPipeline = async ( + buildApi: IBuildApi, + rc: RequestContext +): Promise => { + const pipelineName = APP_REPO_BUILD; + + try { + await deletePipelineIfExist(buildApi, rc, pipelineName); + + await installBuildUpdatePipeline( + path.join(".", SERVICE_PIPELINE_FILENAME), + { + buildScriptUrl: BUILD_SCRIPT_URL, + devopsProject: rc.projectName, + orgName: rc.orgName, + packagesDir: undefined, + personalAccessToken: rc.accessToken, + pipelineName, + repoName: APP_REPO, + repoUrl: getAzureRepoUrl(rc.orgName, rc.projectName, APP_REPO), + yamlFileBranch: "master" + } + ); + await pollForPipelineStatus(buildApi, rc.projectName, pipelineName); + rc.createdBuildPipeline = true; + } catch (err) { + logger.error(`An error occured in create Build Pipeline`); + throw err; + } +}; diff --git a/src/lib/setup/prompt.test.ts b/src/lib/setup/prompt.test.ts index aadc437f5..07b5c513a 100644 --- a/src/lib/setup/prompt.test.ts +++ b/src/lib/setup/prompt.test.ts @@ -8,10 +8,14 @@ import { createTempDir } from "../../lib/ioUtil"; import { DEFAULT_PROJECT_NAME, RequestContext, WORKSPACE } from "./constants"; import { getAnswerFromFile, + getSubscriptionId, prompt, promptForACRName, - promptForSubscriptionId + promptForApprovingHLDPullRequest, + promptForServicePrincipalCreation } from "./prompt"; +import * as promptInstance from "./prompt"; +import * as gitService from "./gitService"; import * as servicePrincipalService from "../azure/servicePrincipalService"; import * as subscriptionService from "../azure/subscriptionService"; @@ -44,6 +48,17 @@ describe("test prompt function", () => { jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ create_service_principal: true }); + + jest.spyOn(servicePrincipalService, "azCLILogin").mockResolvedValueOnce([ + { + id: "72f988bf-86f1-41af-91ab-2d7cd011db48", + name: "subname" + } + ]); + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + az_subscription: "subname" + }); + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ acr_name: "testACR" }); @@ -55,12 +70,6 @@ describe("test prompt function", () => { password: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", tenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47" }); - jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ - { - id: "72f988bf-86f1-41af-91ab-2d7cd011db48", - name: "test" - } - ]); const ans = await prompt(); expect(ans).toStrictEqual({ @@ -263,7 +272,7 @@ describe("test getAnswerFromFile function", () => { }); }); -describe("test promptForSubscriptionId function", () => { +describe("test getSubscriptions function", () => { it("no subscriptions", async () => { jest .spyOn(subscriptionService, "getSubscriptions") @@ -274,7 +283,7 @@ describe("test promptForSubscriptionId function", () => { projectName: "project", workspace: WORKSPACE }; - await expect(promptForSubscriptionId(mockRc)).rejects.toThrow(); + await expect(getSubscriptionId(mockRc)).rejects.toThrow(); }); it("2 subscriptions", async () => { jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ @@ -296,9 +305,31 @@ describe("test promptForSubscriptionId function", () => { projectName: "project", workspace: WORKSPACE }; - await promptForSubscriptionId(mockRc); + await getSubscriptionId(mockRc); expect(mockRc.subscriptionId).toBe("12334567890"); }); + it("no subscriptions selected", async () => { + jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ + { + id: "123345", + name: "subscription1" + }, + { + id: "12334567890", + name: "subscription2" + } + ]); + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + az_subscription: "subscription3" + }); + const mockRc: RequestContext = { + accessToken: "pat", + orgName: "org", + projectName: "project", + workspace: WORKSPACE + }; + await expect(getSubscriptionId(mockRc)).rejects.toThrow(); + }); }); describe("test promptForACRName function", () => { @@ -316,3 +347,46 @@ describe("test promptForACRName function", () => { expect(mockRc.acrName).toBe("testACR"); }); }); + +describe("test promptForServicePrincipalCreation function", () => { + it("covering the test gap: negative test", async () => { + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + create_service_principal: true + }); + jest.spyOn(servicePrincipalService, "azCLILogin").mockResolvedValueOnce([ + { + id: "72f988bf-86f1-41af-91ab-2d7cd011db48", + name: "subname" + } + ]); + jest + .spyOn(promptInstance, "promptForSubscriptionId") + .mockResolvedValueOnce(undefined); + const mockRc: RequestContext = { + accessToken: "pat", + orgName: "org", + projectName: "project", + workspace: WORKSPACE + }; + await expect(promptForServicePrincipalCreation(mockRc)).rejects.toThrow(); + }); +}); + +describe("test promptForApprovingHLDPullRequest function", () => { + it("positive test", async () => { + jest + .spyOn(gitService, "getAzureRepoUrl") + .mockReturnValueOnce("https://sample/example"); + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + approve_hld_pr: true + }); + const mockRc: RequestContext = { + accessToken: "pat", + orgName: "org", + projectName: "project", + workspace: WORKSPACE + }; + const ans = await promptForApprovingHLDPullRequest(mockRc); + expect(ans).toBeTruthy(); + }); +}); diff --git a/src/lib/setup/prompt.ts b/src/lib/setup/prompt.ts index 47918e7cf..1fb7c458a 100644 --- a/src/lib/setup/prompt.ts +++ b/src/lib/setup/prompt.ts @@ -14,19 +14,47 @@ import { import { ACR_NAME, DEFAULT_PROJECT_NAME, + HLD_REPO, RequestContext, WORKSPACE } from "./constants"; -import { createWithAzCLI } from "../azure/servicePrincipalService"; -import { getSubscriptions } from "../azure/subscriptionService"; +import { getAzureRepoUrl } from "./gitService"; +import { + azCLILogin, + createWithAzCLI, + SubscriptionData +} from "../azure/servicePrincipalService"; +import { + getSubscriptions, + SubscriptionItem +} from "../azure/subscriptionService"; export const promptForSubscriptionId = async ( - rc: RequestContext -): Promise => { + subscriptions: SubscriptionItem[] | SubscriptionData[] +): Promise => { + const questions = [ + { + choices: subscriptions.map(s => s.name), + message: "Select one of the subscriptions\n", + name: "az_subscription", + type: "list" + } + ]; + const ans = await inquirer.prompt(questions); + const found = subscriptions.find( + s => s.name === (ans.az_subscription as string) + ); + return found ? found.id : undefined; +}; + +export const getSubscriptionId = async (rc: RequestContext): Promise => { const subscriptions = await getSubscriptions( - rc.servicePrincipalId as string, - rc.servicePrincipalPassword as string, - rc.servicePrincipalTenantId as string + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + rc.servicePrincipalId!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + rc.servicePrincipalPassword!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + rc.servicePrincipalTenantId! ); if (subscriptions.length === 0) { throw Error("no subscriptions found"); @@ -34,13 +62,11 @@ export const promptForSubscriptionId = async ( if (subscriptions.length === 1) { rc.subscriptionId = subscriptions[0].id; } else { - const ans = await inquirer.prompt([ - promptBuilder.chooseSubscriptionId(subscriptions.map(s => s.name)) - ]); - const found = subscriptions.find( - s => s.name === (ans.az_subscription as string) - ); - rc.subscriptionId = found ? found.id : undefined; + const subId = await promptForSubscriptionId(subscriptions); + if (!subId) { + throw Error("Subscription Identifier is missing."); + } + rc.subscriptionId = subId; } }; @@ -93,7 +119,13 @@ export const promptForServicePrincipalCreation = async ( const answers = await inquirer.prompt(questions); if (answers.create_service_principal) { rc.toCreateSP = true; - const sp = await createWithAzCLI(); + const subscriptions = await azCLILogin(); + const subscriptionId = await promptForSubscriptionId(subscriptions); + if (!subscriptionId) { + throw Error("Subscription Identifier is missing."); + } + rc.subscriptionId = subscriptionId; + const sp = await createWithAzCLI(rc.subscriptionId); rc.createServicePrincipal = true; rc.servicePrincipalId = sp.id; rc.servicePrincipalPassword = sp.password; @@ -101,8 +133,8 @@ export const promptForServicePrincipalCreation = async ( } else { rc.toCreateSP = false; await promptForServicePrincipal(rc); + await getSubscriptionId(rc); } - await promptForSubscriptionId(rc); }; /** @@ -117,7 +149,7 @@ export const prompt = async (): Promise => { promptBuilder.azureAccessToken(), { default: true, - message: `Do you like create a sample application repository?`, + message: "Would you like to create a sample application repository?", name: "create_app_repo", type: "confirm" } @@ -215,12 +247,6 @@ export const getAnswerFromFile = (file: string): RequestContext => { throw new Error(vToken); } - const acrName = map.az_acr_name || ACR_NAME; - const vACRName = validateACRName(acrName); - if (typeof vACRName === "string") { - throw new Error(vACRName); - } - const rc: RequestContext = { accessToken: map.azdo_pat, orgName: map.azdo_org_name, @@ -228,7 +254,7 @@ export const getAnswerFromFile = (file: string): RequestContext => { servicePrincipalId: map.az_sp_id, servicePrincipalPassword: map.az_sp_password, servicePrincipalTenantId: map.az_sp_tenant, - acrName, + acrName: map.az_acr_name || ACR_NAME, workspace: WORKSPACE }; @@ -237,3 +263,23 @@ export const getAnswerFromFile = (file: string): RequestContext => { return rc; }; + +export const promptForApprovingHLDPullRequest = async ( + rc: RequestContext +): Promise => { + const urlPR = `${getAzureRepoUrl( + rc.orgName, + 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); + return !!answers.approve_hld_pr; +}; diff --git a/src/lib/setup/scaffold.test.ts b/src/lib/setup/scaffold.test.ts index 1b1df3b31..81ede5322 100644 --- a/src/lib/setup/scaffold.test.ts +++ b/src/lib/setup/scaffold.test.ts @@ -167,7 +167,7 @@ describe("test appRepo function", () => { }); it("sanity test, initService", async () => { jest.spyOn(createService, "createService").mockResolvedValueOnce(); - await initService("test"); + await initService(createRequestContext("test"), "test"); }); it("sanity test on setupVariableGroup", async () => { jest diff --git a/src/lib/setup/scaffold.ts b/src/lib/setup/scaffold.ts index 4933d8d00..bf52c7fd5 100644 --- a/src/lib/setup/scaffold.ts +++ b/src/lib/setup/scaffold.ts @@ -18,14 +18,17 @@ import { HELM_REPO, HLD_DEFAULT_COMPONENT_NAME, HLD_DEFAULT_DEF_PATH, - HLD_DEFAULT_GIT_URL, HLD_REPO, MANIFEST_REPO, RequestContext, VARIABLE_GROUP } from "./constants"; import { createDirectory, moveToAbsPath, moveToRelativePath } from "./fsUtil"; -import { commitAndPushToRemote, createRepoInAzureOrg } from "./gitService"; +import { + commitAndPushToRemote, + createRepoInAzureOrg, + getAzureRepoUrl +} from "./gitService"; import { chartTemplate, mainTemplate, valuesTemplate } from "./helmTemplates"; export const createRepo = async ( @@ -105,7 +108,7 @@ export const hldRepo = async ( await hldInitialize( process.cwd(), false, - HLD_DEFAULT_GIT_URL, + "https://github.com/microsoft/fabrikate-definitions.git", HLD_DEFAULT_COMPONENT_NAME, HLD_DEFAULT_DEF_PATH ); @@ -189,7 +192,7 @@ export const setupVariableGroup = async (rc: RequestContext): Promise => { VARIABLE_GROUP, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion rc.acrName!, - HLD_DEFAULT_GIT_URL, + getAzureRepoUrl(rc.orgName, rc.projectName, HLD_REPO), rc.servicePrincipalId, rc.servicePrincipalPassword, rc.servicePrincipalTenantId, @@ -201,19 +204,22 @@ export const setupVariableGroup = async (rc: RequestContext): Promise => { updateLifeCyclePipeline("."); }; -export const initService = async (repoName: string): Promise => { - await createService(".", repoName, { +export const initService = async ( + rc: RequestContext, + repoName: string +): Promise => { + await createService(".", ".", { displayName: repoName, gitPush: false, helmChartChart: "", helmChartRepository: "", helmConfigAccessTokenVariable: "ACCESS_TOKEN_SECRET", helmConfigBranch: "master", - helmConfigGit: HLD_DEFAULT_GIT_URL, + helmConfigGit: getAzureRepoUrl(rc.orgName, rc.projectName, HELM_REPO), helmConfigPath: `${repoName}/chart`, - k8sBackend: "", + k8sBackend: `${repoName}-svc`, k8sBackendPort: "80", - k8sPort: 0, + k8sPort: 80, maintainerEmail: "", maintainerName: "", middlewares: "", @@ -243,12 +249,12 @@ export const appRepo = async ( rc.workspace ); - await projectInitialize("."); + await projectInitialize(".", { defaultRing: "master" }); //How is master set normally? + await setupVariableGroup(rc); + await initService(rc, repoName); await git.add("./*"); - await commitAndPushToRemote(git, rc, repoName); - await setupVariableGroup(rc); - await initService(repoName); + await commitAndPushToRemote(git, rc, repoName); rc.scaffoldAppService = true; logger.info("Completed scaffold app Repo"); diff --git a/src/lib/setup/setupLog.test.ts b/src/lib/setup/setupLog.test.ts index 2c98a7220..ca30c9d69 100644 --- a/src/lib/setup/setupLog.test.ts +++ b/src/lib/setup/setupLog.test.ts @@ -38,6 +38,8 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { rc.servicePrincipalTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; rc.createdResourceGroup = true; rc.createdACR = true; + rc.createdLifecyclePipeline = true; + rc.createdBuildPipeline = true; } create(rc, file); @@ -64,6 +66,8 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "HLD to Manifest Pipeline Created: yes", "Service Principal Created: no", "Resource Group Created: yes", + "Lifecycle Pipeline Created: yes", + "Build Pipeline Created: yes", "ACR Created: yes", "Status: Completed" ]); @@ -88,6 +92,8 @@ const positiveTest = (logExist?: boolean, withAppCreation = false): void => { "HLD to Manifest Pipeline Created: yes", "Service Principal Created: no", "Resource Group Created: no", + "Lifecycle Pipeline Created: no", + "Build Pipeline Created: no", "ACR Created: no", "Status: Completed" ]); @@ -152,6 +158,8 @@ describe("test create function", () => { "HLD to Manifest Pipeline Created: yes", "Service Principal Created: no", "Resource Group Created: no", + "Lifecycle Pipeline Created: no", + "Build Pipeline Created: no", "ACR Created: no", "Error: things broke", "Status: Incomplete" diff --git a/src/lib/setup/setupLog.ts b/src/lib/setup/setupLog.ts index d685c18c5..61d93e610 100644 --- a/src/lib/setup/setupLog.ts +++ b/src/lib/setup/setupLog.ts @@ -33,6 +33,10 @@ export const create = (rc: RequestContext | undefined, file?: string): void => { )}`, `Service Principal Created: ${getBooleanVal(rc.createServicePrincipal)}`, `Resource Group Created: ${getBooleanVal(rc.createdResourceGroup)}`, + `Lifecycle Pipeline Created: ${getBooleanVal( + rc.createdLifecyclePipeline + )}`, + `Build Pipeline Created: ${getBooleanVal(rc.createdBuildPipeline)}`, `ACR Created: ${getBooleanVal(rc.createdACR)}` ]; if (rc.error) {