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

Commit 725d409

Browse files
authored
[FEATURE] Get subscription id in spk setup command (#386)
* [FEATURE] Get subscription id in spk setup command * fix typo and test * fix typo
1 parent 5c37052 commit 725d409

14 files changed

+280
-8
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
},
6666
"dependencies": {
6767
"@azure/arm-storage": "^10.1.0",
68+
"@azure/arm-subscriptions": "^2.0.0",
6869
"@azure/identity": "^1.0.0",
6970
"@azure/keyvault-secrets": "^4.0.0",
7071
"@azure/ms-rest-nodeauth": "^3.0.0",

src/commands/setup.md

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ for a few questions
1818
1. have the command line tool to create it. Azure command line tool shall
1919
be used
2020
2. provide the Service Principal Id, Password and Tenant Id.
21+
2. Subscription Id is automatically retrieved with the Service Principal
22+
credential. In case, there are two or more subscriptions, you will be
23+
prompt to select one of them.
2124

2225
It can also run in a non interactive mode by providing a file that contains
2326
answers to the above questions.

src/commands/setup.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe("test createSPKConfig function", () => {
4444
rc.servicePrincipalId = "1eba2d04-1506-4278-8f8c-b1eb2fc462a8";
4545
rc.servicePrincipalPassword = "e4c19d72-96d6-4172-b195-66b3b1c36db1";
4646
rc.servicePrincipalTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
47+
rc.subscriptionId = "72f988bf-86f1-41af-91ab-2d7cd011db48";
4748
createSPKConfig(rc);
4849

4950
const data = readYaml<IConfigYaml>(tmpFile);
@@ -56,6 +57,7 @@ describe("test createSPKConfig function", () => {
5657
azure: {
5758
service_principal_id: rc.servicePrincipalId,
5859
service_principal_secret: rc.servicePrincipalPassword,
60+
subscription_id: rc.subscriptionId,
5961
tenant_id: rc.servicePrincipalTenantId
6062
}
6163
});

src/commands/setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const createSPKConfig = (rc: IRequestContext) => {
4141
azure: {
4242
service_principal_id: rc.servicePrincipalId,
4343
service_principal_secret: rc.servicePrincipalPassword,
44+
subscription_id: rc.subscriptionId,
4445
tenant_id: rc.servicePrincipalTenantId
4546
}
4647
}
@@ -50,8 +51,7 @@ export const createSPKConfig = (rc: IRequestContext) => {
5051
access_token: rc.accessToken,
5152
org: rc.orgName,
5253
project: rc.projectName
53-
},
54-
introspection: {}
54+
}
5555
};
5656
fs.writeFileSync(defaultConfigFile(), yaml.safeDump(data));
5757
};

src/lib/setup/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface IRequestContext {
1313
servicePrincipalId?: string;
1414
servicePrincipalPassword?: string;
1515
servicePrincipalTenantId?: string;
16+
subscriptionId?: string;
1617
error?: string;
1718
}
1819

src/lib/setup/prompt.test.ts

+81-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import os from "os";
44
import path from "path";
55
import uuid from "uuid/v4";
66
import { createTempDir } from "../../lib/ioUtil";
7-
import { DEFAULT_PROJECT_NAME, WORKSPACE } from "./constants";
8-
import { getAnswerFromFile, prompt } from "./prompt";
7+
import { DEFAULT_PROJECT_NAME, IRequestContext, WORKSPACE } from "./constants";
8+
import { getAnswerFromFile, prompt, promptForSubscriptionId } from "./prompt";
99
import * as servicePrincipalService from "./servicePrincipalService";
10+
import * as subscriptionService from "./subscriptionService";
1011

1112
describe("test prompt function", () => {
1213
it("positive test: No App Creation", async () => {
@@ -40,11 +41,19 @@ describe("test prompt function", () => {
4041
jest
4142
.spyOn(servicePrincipalService, "createWithAzCLI")
4243
.mockReturnValueOnce(Promise.resolve());
44+
jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([
45+
{
46+
id: "72f988bf-86f1-41af-91ab-2d7cd011db48",
47+
name: "test"
48+
}
49+
]);
50+
4351
const ans = await prompt();
4452
expect(ans).toStrictEqual({
4553
accessToken: "pat",
4654
orgName: "org",
4755
projectName: "project",
56+
subscriptionId: "72f988bf-86f1-41af-91ab-2d7cd011db48",
4857
toCreateAppRepo: true,
4958
toCreateSP: true,
5059
workspace: WORKSPACE
@@ -66,6 +75,12 @@ describe("test prompt function", () => {
6675
az_sp_password: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
6776
az_sp_tenant: "72f988bf-86f1-41af-91ab-2d7cd011db47"
6877
});
78+
jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([
79+
{
80+
id: "72f988bf-86f1-41af-91ab-2d7cd011db48",
81+
name: "test"
82+
}
83+
]);
6984
const ans = await prompt();
7085
expect(ans).toStrictEqual({
7186
accessToken: "pat",
@@ -74,6 +89,7 @@ describe("test prompt function", () => {
7489
servicePrincipalId: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
7590
servicePrincipalPassword: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
7691
servicePrincipalTenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47",
92+
subscriptionId: "72f988bf-86f1-41af-91ab-2d7cd011db48",
7793
toCreateAppRepo: true,
7894
toCreateSP: false,
7995
workspace: WORKSPACE
@@ -153,7 +169,8 @@ describe("test getAnswerFromFile function", () => {
153169
"az_create_app=true",
154170
"az_sp_id=b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
155171
"az_sp_password=a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
156-
"az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47"
172+
"az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47",
173+
"az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48"
157174
];
158175
fs.writeFileSync(file, data.join("\n"));
159176
const requestContext = getAnswerFromFile(file);
@@ -171,6 +188,9 @@ describe("test getAnswerFromFile function", () => {
171188
expect(requestContext.servicePrincipalTenantId).toBe(
172189
"72f988bf-86f1-41af-91ab-2d7cd011db47"
173190
);
191+
expect(requestContext.subscriptionId).toBe(
192+
"72f988bf-86f1-41af-91ab-2d7cd011db48"
193+
);
174194
});
175195
it("negative test: with app creation, incorrect SP values", () => {
176196
const dir = createTempDir();
@@ -179,7 +199,8 @@ describe("test getAnswerFromFile function", () => {
179199
"azdo_org_name=orgname",
180200
"azdo_pat=pat",
181201
"azdo_project_name=project",
182-
"az_create_app=true"
202+
"az_create_app=true",
203+
"az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48"
183204
];
184205
[".", ".##", ".abc"].forEach((v, i) => {
185206
if (i === 0) {
@@ -199,4 +220,60 @@ describe("test getAnswerFromFile function", () => {
199220
}).toThrow();
200221
});
201222
});
223+
it("negative test: with app creation, incorrect subscription id value", () => {
224+
const dir = createTempDir();
225+
const file = path.join(dir, "testfile");
226+
const data = [
227+
"azdo_org_name=orgname",
228+
"azdo_pat=pat",
229+
"azdo_project_name=project",
230+
"az_create_app=true",
231+
"az_sp_id=b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
232+
"az_sp_password=a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
233+
"az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47",
234+
"az_subscription_id=xyz"
235+
];
236+
fs.writeFileSync(file, data.join("\n"));
237+
expect(() => {
238+
getAnswerFromFile(file);
239+
}).toThrow();
240+
});
241+
});
242+
243+
describe("test promptForSubscriptionId function", () => {
244+
it("no subscriptions", async () => {
245+
jest
246+
.spyOn(subscriptionService, "getSubscriptions")
247+
.mockResolvedValueOnce([]);
248+
const mockRc: IRequestContext = {
249+
accessToken: "pat",
250+
orgName: "org",
251+
projectName: "project",
252+
workspace: WORKSPACE
253+
};
254+
await expect(promptForSubscriptionId(mockRc)).rejects.toThrow();
255+
});
256+
it("2 subscriptions", async () => {
257+
jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([
258+
{
259+
id: "123345",
260+
name: "subscription1"
261+
},
262+
{
263+
id: "12334567890",
264+
name: "subscription2"
265+
}
266+
]);
267+
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({
268+
az_subscription: "subscription2"
269+
});
270+
const mockRc: IRequestContext = {
271+
accessToken: "pat",
272+
orgName: "org",
273+
projectName: "project",
274+
workspace: WORKSPACE
275+
};
276+
await promptForSubscriptionId(mockRc);
277+
expect(mockRc.subscriptionId).toBe("12334567890");
278+
});
202279
});

src/lib/setup/prompt.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,41 @@
11
import fs from "fs";
22
import inquirer from "inquirer";
3+
34
import {
45
validateAccessToken,
56
validateOrgName,
67
validateProjectName,
78
validateServicePrincipalId,
89
validateServicePrincipalPassword,
9-
validateServicePrincipalTenantId
10+
validateServicePrincipalTenantId,
11+
validateSubscriptionId
1012
} from "../validator";
1113
import { DEFAULT_PROJECT_NAME, IRequestContext, WORKSPACE } from "./constants";
1214
import { createWithAzCLI } from "./servicePrincipalService";
15+
import { getSubscriptions } from "./subscriptionService";
16+
17+
export const promptForSubscriptionId = async (rc: IRequestContext) => {
18+
const subscriptions = await getSubscriptions(rc);
19+
if (subscriptions.length === 0) {
20+
throw Error("no subscriptions found");
21+
}
22+
if (subscriptions.length === 1) {
23+
rc.subscriptionId = subscriptions[0].id;
24+
} else {
25+
const questions = [
26+
{
27+
choices: subscriptions.map(s => s.name),
28+
message: "Select one of the subscription\n",
29+
name: "az_subscription",
30+
type: "list"
31+
}
32+
];
33+
const ans = await inquirer.prompt(questions);
34+
rc.subscriptionId = subscriptions.find(
35+
s => s.name === ans.az_subscription
36+
)!.id;
37+
}
38+
};
1339

1440
/**
1541
* Prompts for service principal identifer, password and tenant identifer.
@@ -71,6 +97,7 @@ export const promptForServicePrincipalCreation = async (
7197
rc.toCreateSP = false;
7298
await promptForServicePrincipal(rc);
7399
}
100+
await promptForSubscriptionId(rc);
74101
};
75102

76103
/**
@@ -145,6 +172,12 @@ const validationServicePrincipalInfoFromFile = (
145172
throw new Error(vSPTenantId);
146173
}
147174
}
175+
176+
const vSubscriptionId = validateSubscriptionId(map.az_subscription_id);
177+
if (typeof vSubscriptionId === "string") {
178+
throw new Error(vSubscriptionId);
179+
}
180+
rc.subscriptionId = map.az_subscription_id;
148181
}
149182
};
150183

@@ -205,5 +238,6 @@ export const getAnswerFromFile = (file: string): IRequestContext => {
205238

206239
rc.toCreateAppRepo = map.az_create_app === "true";
207240
validationServicePrincipalInfoFromFile(rc, map);
241+
208242
return rc;
209243
};

src/lib/setup/setupLog.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false) => {
2121
projectName: "projectName",
2222
scaffoldHLD: true,
2323
scaffoldManifest: true,
24+
subscriptionId: "72f988bf-86f1-41af-91ab-2d7cd011db48",
2425
workspace: "workspace"
2526
};
2627

@@ -45,6 +46,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false) => {
4546
"az_sp_id=b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
4647
"az_sp_password=********",
4748
"az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47",
49+
"az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48",
4850
"workspace: workspace",
4951
"Project Created: yes",
5052
"High Level Definition Repo Scaffolded: yes",
@@ -63,6 +65,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false) => {
6365
"az_sp_id=",
6466
"az_sp_password=",
6567
"az_sp_tenant=",
68+
"az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48",
6669
"workspace: workspace",
6770
"Project Created: yes",
6871
"High Level Definition Repo Scaffolded: yes",
@@ -119,6 +122,7 @@ describe("test create function", () => {
119122
"az_sp_id=",
120123
"az_sp_password=",
121124
"az_sp_tenant=",
125+
"az_subscription_id=",
122126
"workspace: workspace",
123127
"Project Created: yes",
124128
"High Level Definition Repo Scaffolded: yes",

src/lib/setup/setupLog.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const create = (rc: IRequestContext | undefined, file?: string) => {
2020
`az_sp_id=${rc.servicePrincipalId || ""}`,
2121
`az_sp_password=${rc.servicePrincipalPassword ? "********" : ""}`,
2222
`az_sp_tenant=${rc.servicePrincipalTenantId || ""}`,
23+
`az_subscription_id=${rc.subscriptionId || ""}`,
2324
`workspace: ${rc.workspace}`,
2425
`Project Created: ${getBooleanVal(rc.createdProject)}`,
2526
`High Level Definition Repo Scaffolded: ${getBooleanVal(rc.scaffoldHLD)}`,
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
Subscription,
3+
SubscriptionClientOptions
4+
} from "@azure/arm-subscriptions/src/models";
5+
import { ApplicationTokenCredentials } from "@azure/ms-rest-nodeauth";
6+
import * as restAuth from "@azure/ms-rest-nodeauth";
7+
import { getSubscriptions } from "./subscriptionService";
8+
9+
jest.mock("@azure/arm-subscriptions", () => {
10+
class MockClient {
11+
constructor(
12+
cred: ApplicationTokenCredentials,
13+
options?: SubscriptionClientOptions
14+
) {
15+
return {
16+
subscriptions: {
17+
list: () => {
18+
return [
19+
{
20+
displayName: "test",
21+
subscriptionId: "1234567890-abcdef"
22+
}
23+
];
24+
}
25+
}
26+
};
27+
}
28+
}
29+
return {
30+
SubscriptionClient: MockClient
31+
};
32+
});
33+
34+
describe("test getSubscriptions function", () => {
35+
it("positive test: one value", async () => {
36+
jest
37+
.spyOn(restAuth, "loginWithServicePrincipalSecret")
38+
.mockImplementationOnce(async () => {
39+
return {};
40+
});
41+
const result = await getSubscriptions({
42+
accessToken: "pat",
43+
orgName: "org",
44+
projectName: "project",
45+
workspace: "test"
46+
});
47+
expect(result).toStrictEqual([
48+
{
49+
id: "1234567890-abcdef",
50+
name: "test"
51+
}
52+
]);
53+
});
54+
it("negative test", async () => {
55+
jest
56+
.spyOn(restAuth, "loginWithServicePrincipalSecret")
57+
.mockImplementationOnce(async () => {
58+
throw Error("fake");
59+
});
60+
await expect(
61+
getSubscriptions({
62+
accessToken: "pat",
63+
orgName: "org",
64+
projectName: "project",
65+
workspace: "test"
66+
})
67+
).rejects.toThrow();
68+
});
69+
});

0 commit comments

Comments
 (0)