Skip to content
This repository has been archived by the owner on Apr 13, 2020. It is now read-only.

Commit

Permalink
[FEATURE] create service principal in spk setup (#379)
Browse files Browse the repository at this point in the history
* [FEATURE] create service principal

* update doc and added code to have service principal info in config.yaml

* added information on how to delete sp

* masked SP password and change that new project is in wellFormed state before continuing

* fix project service test

* break large functions to smaller ones and add jsDoc
  • Loading branch information
dennisseah authored Mar 10, 2020
1 parent 3ec06b1 commit 681cd29
Show file tree
Hide file tree
Showing 14 changed files with 596 additions and 53 deletions.
20 changes: 20 additions & 0 deletions src/commands/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ for a few questions
3. Azure DevOps Personal Access Token. The token needs to have these permissions
1. Read and write projects.
2. Read and write codes.
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.

It can also run in a non interactive mode by providing a file that contains
answers to the above questions.
Expand All @@ -27,6 +32,11 @@ Content of this file is as follow
azdo_org_name=<Azure DevOps Organization Name>
azdo_project_name=<Azure DevOps Project Name>
azdo_pat=<Azure DevOps Personal Access Token>
az_create_app=<true to create sample service app>
az_create_sp=<true to have command line to create service principal>
az_sp_id=<sevice principal Id need if az_create_app=true and az_create_sp=false>
az_sp_password=<sevice principal password need if az_create_app=true and az_create_sp=false>
az_sp_tenant=<sevice principal tenant Id need if az_create_app=true and az_create_sp=false>
```

`azdo_project_name` is optional and default value is `BedrockRocks`.
Expand All @@ -41,9 +51,19 @@ The followings shall be created
4. A Git Repo, `quick-start-manifest`, it shall be deleted and recreated if it
already exists.
1. And initial commit shall be made to this repo
5. A High Level Definition (HLD) to Manifest pipeline.
6. A Service Principal (if requested)

## Setup log

A `setup.log` file is created after running this command. This file contains
information about what are created and the execution status (completed or
incomplete). This file will not be created if input validation failed.

## Note

To remove the service principal that it is created by the tool, you can do the
followings:

1. Get the identifier from `setup.log` (look for `az_sp_id`)
2. run on terminal `az ad sp delete --id <the sp id>`
28 changes: 27 additions & 1 deletion src/commands/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { readYaml } from "../config";
import * as config from "../config";
import * as azdoClient from "../lib/azdoClient";
import { createTempDir } from "../lib/ioUtil";
import { WORKSPACE } from "../lib/setup/constants";
import { IRequestContext, WORKSPACE } from "../lib/setup/constants";
import * as fsUtil from "../lib/setup/fsUtil";
import * as gitService from "../lib/setup/gitService";
import * as pipelineService from "../lib/setup/pipelineService";
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 { deepClone } from "../lib/util";
import { IConfigYaml } from "../types";
import { createSPKConfig, execute, getErrorMessage } from "./setup";
import * as setup from "./setup";
Expand All @@ -34,6 +35,31 @@ describe("test createSPKConfig function", () => {
project: "project"
});
});
it("positive test: with service principal", () => {
const tmpFile = path.join(createTempDir(), "config.yaml");
jest.spyOn(config, "defaultConfigFile").mockReturnValueOnce(tmpFile);
const rc: IRequestContext = deepClone(mockRequestContext);
rc.toCreateAppRepo = true;
rc.toCreateSP = true;
rc.servicePrincipalId = "1eba2d04-1506-4278-8f8c-b1eb2fc462a8";
rc.servicePrincipalPassword = "e4c19d72-96d6-4172-b195-66b3b1c36db1";
rc.servicePrincipalTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
createSPKConfig(rc);

const data = readYaml<IConfigYaml>(tmpFile);
expect(data.azure_devops).toStrictEqual({
access_token: "pat",
org: "orgname",
project: "project"
});
expect(data.introspection).toStrictEqual({
azure: {
service_principal_id: rc.servicePrincipalId,
service_principal_secret: rc.servicePrincipalPassword,
tenant_id: rc.servicePrincipalTenantId
}
});
});
});

const testExecuteFunc = async (usePrompt = true, hasProject = true) => {
Expand Down
34 changes: 25 additions & 9 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,30 @@ interface IAPIError {
* @param answers Answers provided to the commander
*/
export const createSPKConfig = (rc: IRequestContext) => {
const data = yaml.safeDump({
azure_devops: {
access_token: rc.accessToken,
org: rc.orgName,
project: rc.projectName
}
});
fs.writeFileSync(defaultConfigFile(), data);
const data = rc.toCreateAppRepo
? {
azure_devops: {
access_token: rc.accessToken,
org: rc.orgName,
project: rc.projectName
},
introspection: {
azure: {
service_principal_id: rc.servicePrincipalId,
service_principal_secret: rc.servicePrincipalPassword,
tenant_id: rc.servicePrincipalTenantId
}
}
}
: {
azure_devops: {
access_token: rc.accessToken,
org: rc.orgName,
project: rc.projectName
},
introspection: {}
};
fs.writeFileSync(defaultConfigFile(), yaml.safeDump(data));
};

export const getErrorMessage = (
Expand Down Expand Up @@ -74,8 +90,8 @@ export const execute = async (
try {
requestContext = opts.file ? getAnswerFromFile(opts.file) : await prompt();
createDirectory(WORKSPACE, true);
createSPKConfig(requestContext);

createSPKConfig(requestContext!);
const webAPI = await getWebApi();
const coreAPI = await webAPI.getCoreApi();
const gitAPI = await getGitApi(webAPI);
Expand Down
6 changes: 6 additions & 0 deletions src/lib/setup/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ export interface IRequestContext {
projectName: string;
accessToken: string;
workspace: string;
toCreateAppRepo?: boolean;
toCreateSP?: boolean;
createdProject?: boolean;
scaffoldHLD?: boolean;
scaffoldManifest?: boolean;
createdHLDtoManifestPipeline?: boolean;
createServicePrincipal?: boolean;
servicePrincipalId?: string;
servicePrincipalPassword?: string;
servicePrincipalTenantId?: string;
error?: string;
}

Expand Down
4 changes: 3 additions & 1 deletion src/lib/setup/projectService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ describe("test createProject function", () => {
await createProject(
{
getProject: () => {
return {};
return {
state: "wellFormed"
};
},
queueCreateProject: async () => {
return;
Expand Down
13 changes: 10 additions & 3 deletions src/lib/setup/projectService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ICoreApi } from "azure-devops-node-api/CoreApi";
import { ProjectVisibility } from "azure-devops-node-api/interfaces/CoreInterfaces";
import {
ProjectVisibility,
TeamProject
} from "azure-devops-node-api/interfaces/CoreInterfaces";
import { sleep } from "../../lib/util";
import { logger } from "../../logger";
import { IRequestContext } from "./constants";
Expand All @@ -10,7 +13,10 @@ import { IRequestContext } from "./constants";
* @param coreAPI Core API service
* @param name Name of Project
*/
export const getProject = async (coreAPI: ICoreApi, name: string) => {
export const getProject = async (
coreAPI: ICoreApi,
name: string
): Promise<TeamProject> => {
try {
return await coreAPI.getProject(name);
} catch (err) {
Expand Down Expand Up @@ -55,7 +61,8 @@ export const createProject = async (
// poll to check if project is checked.
let created = false;
while (tries > 0 && !created) {
created = !!(await getProject(coreAPI, name));
const p = await getProject(coreAPI, name);
created = p && p.state === "wellFormed";
if (!created) {
await sleep(sleepDuration);
tries--;
Expand Down
116 changes: 114 additions & 2 deletions src/lib/setup/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,76 @@ import uuid from "uuid/v4";
import { createTempDir } from "../../lib/ioUtil";
import { DEFAULT_PROJECT_NAME, WORKSPACE } from "./constants";
import { getAnswerFromFile, prompt } from "./prompt";
import * as servicePrincipalService from "./servicePrincipalService";

describe("test prompt function", () => {
it("positive test", async () => {
it("positive test: No App Creation", async () => {
const answers = {
azdo_org_name: "org",
azdo_pat: "pat",
azdo_project_name: "project"
azdo_project_name: "project",
create_app_repo: false
};
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers);
const ans = await prompt();
expect(ans).toStrictEqual({
accessToken: "pat",
orgName: "org",
projectName: "project",
toCreateAppRepo: false,
workspace: WORKSPACE
});
});
it("positive test: create SP", async () => {
const answers = {
azdo_org_name: "org",
azdo_pat: "pat",
azdo_project_name: "project",
create_app_repo: true
};
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers);
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({
create_service_principal: true
});
jest
.spyOn(servicePrincipalService, "createWithAzCLI")
.mockReturnValueOnce(Promise.resolve());
const ans = await prompt();
expect(ans).toStrictEqual({
accessToken: "pat",
orgName: "org",
projectName: "project",
toCreateAppRepo: true,
toCreateSP: true,
workspace: WORKSPACE
});
});
it("positive test: no create SP", async () => {
const answers = {
azdo_org_name: "org",
azdo_pat: "pat",
azdo_project_name: "project",
create_app_repo: true
};
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers);
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({
create_service_principal: false
});
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({
az_sp_id: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
az_sp_password: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
az_sp_tenant: "72f988bf-86f1-41af-91ab-2d7cd011db47"
});
const ans = await prompt();
expect(ans).toStrictEqual({
accessToken: "pat",
orgName: "org",
projectName: "project",
servicePrincipalId: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
servicePrincipalPassword: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
servicePrincipalTenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47",
toCreateAppRepo: true,
toCreateSP: false,
workspace: WORKSPACE
});
});
Expand Down Expand Up @@ -87,4 +143,60 @@ describe("test getAnswerFromFile function", () => {
getAnswerFromFile(file);
}).toThrow();
});
it("positive test: with app creation, without SP creation", () => {
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_sp_id=b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
"az_sp_password=a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
"az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47"
];
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.toCreateAppRepo).toBeTruthy();
expect(requestContext.toCreateSP).toBeFalsy();
expect(requestContext.servicePrincipalId).toBe(
"b510c1ff-358c-4ed4-96c8-eb23f42bb65b"
);
expect(requestContext.servicePrincipalPassword).toBe(
"a510c1ff-358c-4ed4-96c8-eb23f42bbc5b"
);
expect(requestContext.servicePrincipalTenantId).toBe(
"72f988bf-86f1-41af-91ab-2d7cd011db47"
);
});
it("negative test: with app creation, incorrect SP values", () => {
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"
];
[".", ".##", ".abc"].forEach((v, i) => {
if (i === 0) {
data.push(`az_sp_id=${v}`);
} else if (i === 1) {
data.pop();
data.push("az_sp_id=b510c1ff-358c-4ed4-96c8-eb23f42bb65b");
data.push(`az_sp_password=${v}`);
} else {
data.pop();
data.push("az_sp_password=a510c1ff-358c-4ed4-96c8-eb23f42bbc5b");
data.push(`az_sp_tenant=${v}`);
}
fs.writeFileSync(file, data.join("\n"));
expect(() => {
getAnswerFromFile(file);
}).toThrow();
});
});
});
Loading

0 comments on commit 681cd29

Please sign in to comment.