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

Commit 681cd29

Browse files
authored
[FEATURE] create service principal in spk setup (#379)
* [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
1 parent 3ec06b1 commit 681cd29

14 files changed

+596
-53
lines changed

src/commands/setup.md

+20
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ for a few questions
1313
3. Azure DevOps Personal Access Token. The token needs to have these permissions
1414
1. Read and write projects.
1515
2. Read and write codes.
16+
4. To create a sample application Repo
17+
1. If Yes, a Azure Service Principal is needed. You have 2 options
18+
1. have the command line tool to create it. Azure command line tool shall
19+
be used
20+
2. provide the Service Principal Id, Password and Tenant Id.
1621

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

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

4557
## Setup log
4658

4759
A `setup.log` file is created after running this command. This file contains
4860
information about what are created and the execution status (completed or
4961
incomplete). This file will not be created if input validation failed.
62+
63+
## Note
64+
65+
To remove the service principal that it is created by the tool, you can do the
66+
followings:
67+
68+
1. Get the identifier from `setup.log` (look for `az_sp_id`)
69+
2. run on terminal `az ad sp delete --id <the sp id>`

src/commands/setup.test.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { readYaml } from "../config";
33
import * as config from "../config";
44
import * as azdoClient from "../lib/azdoClient";
55
import { createTempDir } from "../lib/ioUtil";
6-
import { WORKSPACE } from "../lib/setup/constants";
6+
import { IRequestContext, WORKSPACE } from "../lib/setup/constants";
77
import * as fsUtil from "../lib/setup/fsUtil";
88
import * as gitService from "../lib/setup/gitService";
99
import * as pipelineService from "../lib/setup/pipelineService";
1010
import * as projectService from "../lib/setup/projectService";
1111
import * as promptInstance from "../lib/setup/prompt";
1212
import * as scaffold from "../lib/setup/scaffold";
1313
import * as setupLog from "../lib/setup/setupLog";
14+
import { deepClone } from "../lib/util";
1415
import { IConfigYaml } from "../types";
1516
import { createSPKConfig, execute, getErrorMessage } from "./setup";
1617
import * as setup from "./setup";
@@ -34,6 +35,31 @@ describe("test createSPKConfig function", () => {
3435
project: "project"
3536
});
3637
});
38+
it("positive test: with service principal", () => {
39+
const tmpFile = path.join(createTempDir(), "config.yaml");
40+
jest.spyOn(config, "defaultConfigFile").mockReturnValueOnce(tmpFile);
41+
const rc: IRequestContext = deepClone(mockRequestContext);
42+
rc.toCreateAppRepo = true;
43+
rc.toCreateSP = true;
44+
rc.servicePrincipalId = "1eba2d04-1506-4278-8f8c-b1eb2fc462a8";
45+
rc.servicePrincipalPassword = "e4c19d72-96d6-4172-b195-66b3b1c36db1";
46+
rc.servicePrincipalTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
47+
createSPKConfig(rc);
48+
49+
const data = readYaml<IConfigYaml>(tmpFile);
50+
expect(data.azure_devops).toStrictEqual({
51+
access_token: "pat",
52+
org: "orgname",
53+
project: "project"
54+
});
55+
expect(data.introspection).toStrictEqual({
56+
azure: {
57+
service_principal_id: rc.servicePrincipalId,
58+
service_principal_secret: rc.servicePrincipalPassword,
59+
tenant_id: rc.servicePrincipalTenantId
60+
}
61+
});
62+
});
3763
});
3864

3965
const testExecuteFunc = async (usePrompt = true, hasProject = true) => {

src/commands/setup.ts

+25-9
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,30 @@ interface IAPIError {
3030
* @param answers Answers provided to the commander
3131
*/
3232
export const createSPKConfig = (rc: IRequestContext) => {
33-
const data = yaml.safeDump({
34-
azure_devops: {
35-
access_token: rc.accessToken,
36-
org: rc.orgName,
37-
project: rc.projectName
38-
}
39-
});
40-
fs.writeFileSync(defaultConfigFile(), data);
33+
const data = rc.toCreateAppRepo
34+
? {
35+
azure_devops: {
36+
access_token: rc.accessToken,
37+
org: rc.orgName,
38+
project: rc.projectName
39+
},
40+
introspection: {
41+
azure: {
42+
service_principal_id: rc.servicePrincipalId,
43+
service_principal_secret: rc.servicePrincipalPassword,
44+
tenant_id: rc.servicePrincipalTenantId
45+
}
46+
}
47+
}
48+
: {
49+
azure_devops: {
50+
access_token: rc.accessToken,
51+
org: rc.orgName,
52+
project: rc.projectName
53+
},
54+
introspection: {}
55+
};
56+
fs.writeFileSync(defaultConfigFile(), yaml.safeDump(data));
4157
};
4258

4359
export const getErrorMessage = (
@@ -74,8 +90,8 @@ export const execute = async (
7490
try {
7591
requestContext = opts.file ? getAnswerFromFile(opts.file) : await prompt();
7692
createDirectory(WORKSPACE, true);
93+
createSPKConfig(requestContext);
7794

78-
createSPKConfig(requestContext!);
7995
const webAPI = await getWebApi();
8096
const coreAPI = await webAPI.getCoreApi();
8197
const gitAPI = await getGitApi(webAPI);

src/lib/setup/constants.ts

+6
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ export interface IRequestContext {
33
projectName: string;
44
accessToken: string;
55
workspace: string;
6+
toCreateAppRepo?: boolean;
7+
toCreateSP?: boolean;
68
createdProject?: boolean;
79
scaffoldHLD?: boolean;
810
scaffoldManifest?: boolean;
911
createdHLDtoManifestPipeline?: boolean;
12+
createServicePrincipal?: boolean;
13+
servicePrincipalId?: string;
14+
servicePrincipalPassword?: string;
15+
servicePrincipalTenantId?: string;
1016
error?: string;
1117
}
1218

src/lib/setup/projectService.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ describe("test createProject function", () => {
5959
await createProject(
6060
{
6161
getProject: () => {
62-
return {};
62+
return {
63+
state: "wellFormed"
64+
};
6365
},
6466
queueCreateProject: async () => {
6567
return;

src/lib/setup/projectService.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { ICoreApi } from "azure-devops-node-api/CoreApi";
2-
import { ProjectVisibility } from "azure-devops-node-api/interfaces/CoreInterfaces";
2+
import {
3+
ProjectVisibility,
4+
TeamProject
5+
} from "azure-devops-node-api/interfaces/CoreInterfaces";
36
import { sleep } from "../../lib/util";
47
import { logger } from "../../logger";
58
import { IRequestContext } from "./constants";
@@ -10,7 +13,10 @@ import { IRequestContext } from "./constants";
1013
* @param coreAPI Core API service
1114
* @param name Name of Project
1215
*/
13-
export const getProject = async (coreAPI: ICoreApi, name: string) => {
16+
export const getProject = async (
17+
coreAPI: ICoreApi,
18+
name: string
19+
): Promise<TeamProject> => {
1420
try {
1521
return await coreAPI.getProject(name);
1622
} catch (err) {
@@ -55,7 +61,8 @@ export const createProject = async (
5561
// poll to check if project is checked.
5662
let created = false;
5763
while (tries > 0 && !created) {
58-
created = !!(await getProject(coreAPI, name));
64+
const p = await getProject(coreAPI, name);
65+
created = p && p.state === "wellFormed";
5966
if (!created) {
6067
await sleep(sleepDuration);
6168
tries--;

src/lib/setup/prompt.test.ts

+114-2
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,76 @@ import uuid from "uuid/v4";
66
import { createTempDir } from "../../lib/ioUtil";
77
import { DEFAULT_PROJECT_NAME, WORKSPACE } from "./constants";
88
import { getAnswerFromFile, prompt } from "./prompt";
9+
import * as servicePrincipalService from "./servicePrincipalService";
910

1011
describe("test prompt function", () => {
11-
it("positive test", async () => {
12+
it("positive test: No App Creation", async () => {
1213
const answers = {
1314
azdo_org_name: "org",
1415
azdo_pat: "pat",
15-
azdo_project_name: "project"
16+
azdo_project_name: "project",
17+
create_app_repo: false
1618
};
1719
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers);
1820
const ans = await prompt();
1921
expect(ans).toStrictEqual({
2022
accessToken: "pat",
2123
orgName: "org",
2224
projectName: "project",
25+
toCreateAppRepo: false,
26+
workspace: WORKSPACE
27+
});
28+
});
29+
it("positive test: create SP", async () => {
30+
const answers = {
31+
azdo_org_name: "org",
32+
azdo_pat: "pat",
33+
azdo_project_name: "project",
34+
create_app_repo: true
35+
};
36+
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers);
37+
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({
38+
create_service_principal: true
39+
});
40+
jest
41+
.spyOn(servicePrincipalService, "createWithAzCLI")
42+
.mockReturnValueOnce(Promise.resolve());
43+
const ans = await prompt();
44+
expect(ans).toStrictEqual({
45+
accessToken: "pat",
46+
orgName: "org",
47+
projectName: "project",
48+
toCreateAppRepo: true,
49+
toCreateSP: true,
50+
workspace: WORKSPACE
51+
});
52+
});
53+
it("positive test: no create SP", async () => {
54+
const answers = {
55+
azdo_org_name: "org",
56+
azdo_pat: "pat",
57+
azdo_project_name: "project",
58+
create_app_repo: true
59+
};
60+
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers);
61+
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({
62+
create_service_principal: false
63+
});
64+
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({
65+
az_sp_id: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
66+
az_sp_password: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
67+
az_sp_tenant: "72f988bf-86f1-41af-91ab-2d7cd011db47"
68+
});
69+
const ans = await prompt();
70+
expect(ans).toStrictEqual({
71+
accessToken: "pat",
72+
orgName: "org",
73+
projectName: "project",
74+
servicePrincipalId: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
75+
servicePrincipalPassword: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
76+
servicePrincipalTenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47",
77+
toCreateAppRepo: true,
78+
toCreateSP: false,
2379
workspace: WORKSPACE
2480
});
2581
});
@@ -87,4 +143,60 @@ describe("test getAnswerFromFile function", () => {
87143
getAnswerFromFile(file);
88144
}).toThrow();
89145
});
146+
it("positive test: with app creation, without SP creation", () => {
147+
const dir = createTempDir();
148+
const file = path.join(dir, "testfile");
149+
const data = [
150+
"azdo_org_name=orgname",
151+
"azdo_pat=pat",
152+
"azdo_project_name=project",
153+
"az_create_app=true",
154+
"az_sp_id=b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
155+
"az_sp_password=a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
156+
"az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47"
157+
];
158+
fs.writeFileSync(file, data.join("\n"));
159+
const requestContext = getAnswerFromFile(file);
160+
expect(requestContext.orgName).toBe("orgname");
161+
expect(requestContext.accessToken).toBe("pat");
162+
expect(requestContext.projectName).toBe("project");
163+
expect(requestContext.toCreateAppRepo).toBeTruthy();
164+
expect(requestContext.toCreateSP).toBeFalsy();
165+
expect(requestContext.servicePrincipalId).toBe(
166+
"b510c1ff-358c-4ed4-96c8-eb23f42bb65b"
167+
);
168+
expect(requestContext.servicePrincipalPassword).toBe(
169+
"a510c1ff-358c-4ed4-96c8-eb23f42bbc5b"
170+
);
171+
expect(requestContext.servicePrincipalTenantId).toBe(
172+
"72f988bf-86f1-41af-91ab-2d7cd011db47"
173+
);
174+
});
175+
it("negative test: with app creation, incorrect SP values", () => {
176+
const dir = createTempDir();
177+
const file = path.join(dir, "testfile");
178+
const data = [
179+
"azdo_org_name=orgname",
180+
"azdo_pat=pat",
181+
"azdo_project_name=project",
182+
"az_create_app=true"
183+
];
184+
[".", ".##", ".abc"].forEach((v, i) => {
185+
if (i === 0) {
186+
data.push(`az_sp_id=${v}`);
187+
} else if (i === 1) {
188+
data.pop();
189+
data.push("az_sp_id=b510c1ff-358c-4ed4-96c8-eb23f42bb65b");
190+
data.push(`az_sp_password=${v}`);
191+
} else {
192+
data.pop();
193+
data.push("az_sp_password=a510c1ff-358c-4ed4-96c8-eb23f42bbc5b");
194+
data.push(`az_sp_tenant=${v}`);
195+
}
196+
fs.writeFileSync(file, data.join("\n"));
197+
expect(() => {
198+
getAnswerFromFile(file);
199+
}).toThrow();
200+
});
201+
});
90202
});

0 commit comments

Comments
 (0)