Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

Commit

Permalink
Merge branch 'master' into edaena-int-ring
Browse files Browse the repository at this point in the history
  • Loading branch information
samiyaakhtar authored Apr 16, 2020
2 parents 812016d + c5ecda6 commit 12ba317
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 80 deletions.
2 changes: 1 addition & 1 deletion docs/commands/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\nAfter this command is successfully executed, you can launch the introspection\ndashboard to view the status of pipelines.\n\n```\nspk setup --file <file-name>\n```\n\nContent of this file is as follow\n\n```\nazdo_org_name=<Azure DevOps Organization Name>\nazdo_project_name=<Azure DevOps Project Name>\nazdo_pat=<Azure DevOps Personal Access Token>\naz_create_app=<true to create sample service app>\naz_create_sp=<true to have command line to create service principal>\naz_sp_id=<sevice principal Id need if az_create_app=true and az_create_sp=false>\naz_sp_password=<sevice principal password need if az_create_app=true and az_create_sp=false>\naz_sp_tenant=<sevice principal tenant Id need if az_create_app=true and az_create_sp=false>\naz_subscription_id=<subscription id>\naz_acr_name=<name of azure container registry>\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 <the sp 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 <file-name>\n```\n\nContent of this file is as follow\n\n```\nazdo_org_name=<Azure DevOps Organization Name>\nazdo_project_name=<Azure DevOps Project Name>\nazdo_pat=<Azure DevOps Personal Access Token>\naz_create_app=<true to create sample service app>\naz_create_sp=<true to have command line to create service principal>\naz_sp_id=<sevice principal Id need if az_create_app=true and az_create_sp=false>\naz_sp_password=<sevice principal password need if az_create_app=true and az_create_sp=false>\naz_sp_tenant=<sevice principal tenant Id need if az_create_app=true and az_create_sp=false>\naz_subscription_id=<subscription id>\naz_acr_name=<name of azure container registry>\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## Pre-requisite\n\n1. azure cli needs to be installed so that pull request can be automatically\napproved. type `az version` to check if you have version 2.0.x installed.\n2. install `azure-devops` extension. To check if you have the extension, type `az extension list`\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 <the sp id>`\n"
},
"deployment create": {
"command": "create",
Expand Down
6 changes: 6 additions & 0 deletions src/commands/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ The followings shall be created
1. A Lifecycle pipeline.
1. A Build pipeline.

## Pre-requisite

1. azure cli needs to be installed so that pull request can be automatically
approved. type `az version` to check if you have version 2.0.x installed.
2. install `azure-devops` extension. To check if you have the extension, type `az extension list`

## Setup log

A `setup.log` file is created after running this command. This file contains
Expand Down
29 changes: 8 additions & 21 deletions src/commands/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const testExecuteFunc = async (
usePrompt = true,
hasProject = true
): Promise<void> => {
jest.spyOn(setup, "isAzCLIInstall").mockResolvedValueOnce();
jest
.spyOn(gitService, "getGitApi")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -165,10 +166,8 @@ const testExecuteFunc = async (
.spyOn(projectService, "getProject")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.mockResolvedValueOnce(undefined as any);
jest.spyOn(projectService, "createProject").mockResolvedValueOnce();
}
const fncreateProject = jest
.spyOn(projectService, "createProject")
.mockResolvedValueOnce();

if (usePrompt) {
await execute(
Expand All @@ -186,12 +185,6 @@ const testExecuteFunc = async (
);
}

if (hasProject) {
expect(fncreateProject).toBeCalledTimes(0);
} else {
expect(fncreateProject).toBeCalledTimes(1);
}
fncreateProject.mockReset();
expect(exitFn).toBeCalledTimes(1);
expect(exitFn.mock.calls).toEqual([[0]]);
};
Expand Down Expand Up @@ -341,7 +334,7 @@ describe("test getErrorMessage function", () => {
});
});

const testCreateAppRepoTasks = async (prApproved = true): Promise<void> => {
const testCreateAppRepoTasks = async (): Promise<void> => {
const mockRc: RequestContext = {
orgName: "org",
projectName: "project",
Expand All @@ -366,15 +359,10 @@ const testCreateAppRepoTasks = async (prApproved = true): Promise<void> => {
jest
.spyOn(pipelineService, "createLifecyclePipeline")
.mockResolvedValueOnce();
jest
.spyOn(promptInstance, "promptForApprovingHLDPullRequest")
.mockResolvedValueOnce(prApproved);
if (prApproved) {
jest.spyOn(pipelineService, "createBuildPipeline").mockResolvedValueOnce();
jest
.spyOn(promptInstance, "promptForApprovingHLDPullRequest")
.mockResolvedValueOnce(prApproved);
}
jest.spyOn(gitService, "completePullRequest").mockResolvedValueOnce();

jest.spyOn(pipelineService, "createBuildPipeline").mockResolvedValueOnce();
jest.spyOn(gitService, "completePullRequest").mockResolvedValueOnce();

const res = await createAppRepoTasks(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -383,13 +371,12 @@ const testCreateAppRepoTasks = async (prApproved = true): Promise<void> => {
{} as any, // buildAPI
mockRc
);
expect(res).toBe(prApproved);
expect(res).toBe(true);
};

describe("test createAppRepoTasks function", () => {
it("positive test", async () => {
await testCreateAppRepoTasks();
await testCreateAppRepoTasks(false);
});
});

Expand Down
51 changes: 33 additions & 18 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ import {
WORKSPACE,
} from "../lib/setup/constants";
import { createDirectory } from "../lib/setup/fsUtil";
import { getAzureRepoUrl, getGitApi } from "../lib/setup/gitService";
import {
completePullRequest,
getAzureRepoUrl,
getGitApi,
} from "../lib/setup/gitService";
import {
createBuildPipeline,
createHLDtoManifestPipeline,
createLifecyclePipeline,
} from "../lib/setup/pipelineService";
import { createProjectIfNotExist } from "../lib/setup/projectService";
import {
getAnswerFromFile,
prompt,
promptForApprovingHLDPullRequest,
} from "../lib/setup/prompt";
import { getAnswerFromFile, prompt } from "../lib/setup/prompt";
import {
appRepo,
helmRepo,
Expand All @@ -43,6 +43,7 @@ import decorator from "./setup.decorator.json";
import { createStorage } from "../lib/setup/azureStorage";
import { build as buildError, log as logError } from "../lib/errorBuilder";
import { errorStatusCode } from "../lib/errorStatusCode";
import { exec } from "../lib/shell";
import { ConfigYaml } from "../types";

interface CommandOptions {
Expand All @@ -60,6 +61,27 @@ interface APIClients {
buildAPI: IBuildApi;
}

export const isAzCLIInstall = async (): Promise<void> => {
try {
const result = await exec("az", ["version"]);
try {
logger.info(`az cli vesion ${JSON.parse(result)["azure-cli"]}`);
} catch (e) {
throw buildError(
errorStatusCode.ENV_SETTING_ERR,
"setup-cmd-az-cli-get-version-err",
e
);
}
} catch (err) {
throw buildError(
errorStatusCode.ENV_SETTING_ERR,
"setup-cmd-az-cli-err",
err
);
}
};

/**
* Creates SPK config file under `user-home/.spk` folder
*
Expand Down Expand Up @@ -169,18 +191,10 @@ 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);

if (await promptForApprovingHLDPullRequest(rc)) {
return true;
}
}

logger.warn("HLD Pull Request is not approved.");
return false;
await completePullRequest(gitAPI, rc, HLD_REPO);
await createBuildPipeline(buildAPI, rc);
await completePullRequest(gitAPI, rc, HLD_REPO);
return true;
} else {
return false;
}
Expand Down Expand Up @@ -243,6 +257,7 @@ export const execute = async (
let requestContext: RequestContext | undefined = undefined;

try {
await isAzCLIInstall();
requestContext = opts.file ? getAnswerFromFile(opts.file) : await prompt();
const rc = requestContext;
createDirectory(WORKSPACE, true);
Expand Down
49 changes: 49 additions & 0 deletions src/lib/git/azure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import { disableVerboseLogging, enableVerboseLogging } from "../../logger";
import { getErrorMessage } from "../errorBuilder";
import { AzureDevOpsOpts } from "../git";
import * as gitutils from "../gitutils";
import * as shell from "../shell";
import {
completePullRequest,
createPullRequest,
generatePRUrl,
getAzureOrganizationUrl,
getGitOrigin,
GitAPI,
repositoryHasFile,
validateRepository,
} from "./azure";
import * as azure from "./azure";
import { RequestContext } from "../setup/constants";
jest.mock("azure-devops-node-api");
jest.mock("../../config");

Expand Down Expand Up @@ -283,3 +287,48 @@ describe("repositoryHasFile", () => {
).rejects.toThrow();
});
});

describe("test getAzureOrganizationUrl function", () => {
it("positive test", () => {
expect(getAzureOrganizationUrl("hello")).toBe(
"https://dev.azure.com/hello"
);
});
});

const mockRequestContext: RequestContext = {
servicePrincipalId: "servicePrincipalId",
servicePrincipalPassword: "servicePrincipalPassword",
servicePrincipalTenantId: "servicePrincipalTenantId",
orgName: "unittest",
projectName: "project",
accessToken: "token",
workspace: "test",
};

describe("test completePullRequest function", () => {
it("negative test: cannot login", async () => {
jest.spyOn(shell, "exec").mockRejectedValueOnce(Error());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await expect(
completePullRequest({ pullRequestId: "test" } as any, mockRequestContext)
).rejects.toThrow(getErrorMessage("git-azure-approve-pull-request-err"));
});
it("negative test: cannot approve pull request", async () => {
jest.spyOn(shell, "exec").mockResolvedValueOnce("ok");
jest.spyOn(shell, "exec").mockRejectedValueOnce(Error());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await expect(
completePullRequest({ pullRequestId: "test" } as any, mockRequestContext)
).rejects.toThrow(getErrorMessage("git-azure-approve-pull-request-err"));
});
it("positive test", async () => {
jest.spyOn(shell, "exec").mockResolvedValueOnce("ok");
jest.spyOn(shell, "exec").mockResolvedValueOnce("ok");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await completePullRequest(
{ pullRequestId: "test" } as any,
mockRequestContext
);
});
});
89 changes: 89 additions & 0 deletions src/lib/git/azure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { IGitApi } from "azure-devops-node-api/GitApi";
import AZGitInterfaces, {
GitPullRequestSearchCriteria,
GitRepository,
PullRequestStatus,
GitPullRequest,
} from "azure-devops-node-api/interfaces/GitInterfaces";
import { AzureDevOpsOpts } from ".";
import { Config } from "../../config";
Expand All @@ -11,6 +13,8 @@ import { azdoUrl } from "../azdoClient";
import { build as buildError } from "../errorBuilder";
import { errorStatusCode } from "../errorStatusCode";
import { getOriginUrl, safeGitUrlForLogging } from "../gitutils";
import { RequestContext } from "../setup/constants";
import { exec } from "../shell";

////////////////////////////////////////////////////////////////////////////////
// State
Expand All @@ -22,6 +26,16 @@ let gitApi: IGitApi | undefined; // keep track of the gitApi so it can be reused
// Helpers
////////////////////////////////////////////////////////////////////////////////

/**
* Returns azure organization URL.
*
* @param orgName Organization name
*/
export const getAzureOrganizationUrl = (orgName: string): string => {
// TODO: Do we need to consider visualstudio.com domain?
return `https://dev.azure.com/${orgName}`;
};

/**
* Authenticates using config and credentials from global config and returns
* an Azure DevOps Git API
Expand Down Expand Up @@ -400,3 +414,78 @@ export const validateRepository = async (

await repositoryHasFile(fileName, branch, repoName, accessOpts);
};

/**
* Returns active pull requests.
*
* @param gitAPI Git Api client.
* @param repoName Name of repository
* @param projectName Project name
* @param targetRef target git reference (default is master)
*/
export const getActivePullRequests = async (
gitAPI: IGitApi,
repoName: string,
projectName: string,
targetRef = "master"
): Promise<GitPullRequest[]> => {
return await gitAPI.getPullRequests(
repoName,
{
targetRefName: `refs/heads/${targetRef}`,
status: PullRequestStatus.Active,
},
projectName
);
};

/**
* Completes a pull request.
*
* @param pullRequest pull request
* @param rc Request Context
*/
export const completePullRequest = async (
pullRequest: GitPullRequest,
rc: RequestContext
): Promise<void> => {
logger.info(`Approving pull request ${pullRequest.pullRequestId}`);
try {
const login = [
"login",
"--service-principal",
"--username",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
rc.servicePrincipalId!,
"--password",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
rc.servicePrincipalPassword!,
"--tenant",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
rc.servicePrincipalTenantId!,
];
const autoComplete = [
"repos",
"pr",
"update",
"--id",
(pullRequest.pullRequestId || "").toString(),
"--auto-complete",
"true",
"--organization",
getAzureOrganizationUrl(rc.orgName),
"--output",
"json",
];

await exec("az", login);
await exec("az", autoComplete);
logger.info(`Approved pull request ${pullRequest.pullRequestId}`);
} catch (err) {
throw buildError(
errorStatusCode.GIT_OPS_ERR,
"git-azure-approve-pull-request-err",
err
);
}
};
Loading

0 comments on commit 12ba317

Please sign in to comment.