Skip to content
This repository was archived by the owner on Apr 13, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
},
"dependencies": {
"@azure/arm-storage": "^10.1.0",
"@azure/arm-subscriptions": "^2.0.0",
"@azure/identity": "^1.0.0",
"@azure/keyvault-secrets": "^4.0.0",
"@azure/ms-rest-nodeauth": "^3.0.0",
Expand Down
3 changes: 3 additions & 0 deletions src/commands/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ for a few questions
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.

It can also run in a non interactive mode by providing a file that contains
answers to the above questions.
Expand Down
2 changes: 2 additions & 0 deletions src/commands/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe("test createSPKConfig function", () => {
rc.servicePrincipalId = "1eba2d04-1506-4278-8f8c-b1eb2fc462a8";
rc.servicePrincipalPassword = "e4c19d72-96d6-4172-b195-66b3b1c36db1";
rc.servicePrincipalTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
rc.subscriptionId = "72f988bf-86f1-41af-91ab-2d7cd011db48";
createSPKConfig(rc);

const data = readYaml<IConfigYaml>(tmpFile);
Expand All @@ -56,6 +57,7 @@ describe("test createSPKConfig function", () => {
azure: {
service_principal_id: rc.servicePrincipalId,
service_principal_secret: rc.servicePrincipalPassword,
subscription_id: rc.subscriptionId,
tenant_id: rc.servicePrincipalTenantId
}
});
Expand Down
4 changes: 2 additions & 2 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const createSPKConfig = (rc: IRequestContext) => {
azure: {
service_principal_id: rc.servicePrincipalId,
service_principal_secret: rc.servicePrincipalPassword,
subscription_id: rc.subscriptionId,
tenant_id: rc.servicePrincipalTenantId
}
}
Expand All @@ -50,8 +51,7 @@ export const createSPKConfig = (rc: IRequestContext) => {
access_token: rc.accessToken,
org: rc.orgName,
project: rc.projectName
},
introspection: {}
}
};
fs.writeFileSync(defaultConfigFile(), yaml.safeDump(data));
};
Expand Down
1 change: 1 addition & 0 deletions src/lib/setup/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface IRequestContext {
servicePrincipalId?: string;
servicePrincipalPassword?: string;
servicePrincipalTenantId?: string;
subscriptionId?: string;
error?: string;
}

Expand Down
85 changes: 81 additions & 4 deletions src/lib/setup/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import os from "os";
import path from "path";
import uuid from "uuid/v4";
import { createTempDir } from "../../lib/ioUtil";
import { DEFAULT_PROJECT_NAME, WORKSPACE } from "./constants";
import { getAnswerFromFile, prompt } from "./prompt";
import { DEFAULT_PROJECT_NAME, IRequestContext, WORKSPACE } from "./constants";
import { getAnswerFromFile, prompt, promptForSubscriptionId } from "./prompt";
import * as servicePrincipalService from "./servicePrincipalService";
import * as subscriptionService from "./subscriptionService";

describe("test prompt function", () => {
it("positive test: No App Creation", async () => {
Expand Down Expand Up @@ -40,11 +41,19 @@ describe("test prompt function", () => {
jest
.spyOn(servicePrincipalService, "createWithAzCLI")
.mockReturnValueOnce(Promise.resolve());
jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([
{
id: "72f988bf-86f1-41af-91ab-2d7cd011db48",
name: "test"
}
]);

const ans = await prompt();
expect(ans).toStrictEqual({
accessToken: "pat",
orgName: "org",
projectName: "project",
subscriptionId: "72f988bf-86f1-41af-91ab-2d7cd011db48",
toCreateAppRepo: true,
toCreateSP: true,
workspace: WORKSPACE
Expand All @@ -66,6 +75,12 @@ describe("test prompt function", () => {
az_sp_password: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
az_sp_tenant: "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({
accessToken: "pat",
Expand All @@ -74,6 +89,7 @@ describe("test prompt function", () => {
servicePrincipalId: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
servicePrincipalPassword: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b",
servicePrincipalTenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47",
subscriptionId: "72f988bf-86f1-41af-91ab-2d7cd011db48",
toCreateAppRepo: true,
toCreateSP: false,
workspace: WORKSPACE
Expand Down Expand Up @@ -153,7 +169,8 @@ describe("test getAnswerFromFile function", () => {
"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"
"az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47",
"az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48"
];
fs.writeFileSync(file, data.join("\n"));
const requestContext = getAnswerFromFile(file);
Expand All @@ -171,6 +188,9 @@ describe("test getAnswerFromFile function", () => {
expect(requestContext.servicePrincipalTenantId).toBe(
"72f988bf-86f1-41af-91ab-2d7cd011db47"
);
expect(requestContext.subscriptionId).toBe(
"72f988bf-86f1-41af-91ab-2d7cd011db48"
);
});
it("negative test: with app creation, incorrect SP values", () => {
const dir = createTempDir();
Expand All @@ -179,7 +199,8 @@ describe("test getAnswerFromFile function", () => {
"azdo_org_name=orgname",
"azdo_pat=pat",
"azdo_project_name=project",
"az_create_app=true"
"az_create_app=true",
"az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48"
];
[".", ".##", ".abc"].forEach((v, i) => {
if (i === 0) {
Expand All @@ -199,4 +220,60 @@ describe("test getAnswerFromFile function", () => {
}).toThrow();
});
});
it("negative test: with app creation, incorrect subscription id value", () => {
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",
"az_subscription_id=xyz"
];
fs.writeFileSync(file, data.join("\n"));
expect(() => {
getAnswerFromFile(file);
}).toThrow();
});
});

describe("test promptForSubscriptionId function", () => {
it("no subscriptions", async () => {
jest
.spyOn(subscriptionService, "getSubscriptions")
.mockResolvedValueOnce([]);
const mockRc: IRequestContext = {
accessToken: "pat",
orgName: "org",
projectName: "project",
workspace: WORKSPACE
};
await expect(promptForSubscriptionId(mockRc)).rejects.toThrow();
});
it("2 subscriptions", async () => {
jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([
{
id: "123345",
name: "subscription1"
},
{
id: "12334567890",
name: "subscription2"
}
]);
jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({
az_subscription: "subscription2"
});
const mockRc: IRequestContext = {
accessToken: "pat",
orgName: "org",
projectName: "project",
workspace: WORKSPACE
};
await promptForSubscriptionId(mockRc);
expect(mockRc.subscriptionId).toBe("12334567890");
});
});
36 changes: 35 additions & 1 deletion src/lib/setup/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
import fs from "fs";
import inquirer from "inquirer";

import {
validateAccessToken,
validateOrgName,
validateProjectName,
validateServicePrincipalId,
validateServicePrincipalPassword,
validateServicePrincipalTenantId
validateServicePrincipalTenantId,
validateSubscriptionId
} from "../validator";
import { DEFAULT_PROJECT_NAME, IRequestContext, WORKSPACE } from "./constants";
import { createWithAzCLI } from "./servicePrincipalService";
import { getSubscriptions } from "./subscriptionService";

export const promptForSubscriptionId = async (rc: IRequestContext) => {
const subscriptions = await getSubscriptions(rc);
if (subscriptions.length === 0) {
throw Error("no subscriptions found");
}
if (subscriptions.length === 1) {
rc.subscriptionId = subscriptions[0].id;
} else {
const questions = [
{
choices: subscriptions.map(s => s.name),
message: "Select one of the subscription\n",
name: "az_subscription",
type: "list"
}
];
const ans = await inquirer.prompt(questions);
rc.subscriptionId = subscriptions.find(
s => s.name === ans.az_subscription
)!.id;
}
};

/**
* Prompts for service principal identifer, password and tenant identifer.
Expand Down Expand Up @@ -71,6 +97,7 @@ export const promptForServicePrincipalCreation = async (
rc.toCreateSP = false;
await promptForServicePrincipal(rc);
}
await promptForSubscriptionId(rc);
};

/**
Expand Down Expand Up @@ -145,6 +172,12 @@ const validationServicePrincipalInfoFromFile = (
throw new Error(vSPTenantId);
}
}

const vSubscriptionId = validateSubscriptionId(map.az_subscription_id);
if (typeof vSubscriptionId === "string") {
throw new Error(vSubscriptionId);
}
rc.subscriptionId = map.az_subscription_id;
}
};

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

rc.toCreateAppRepo = map.az_create_app === "true";
validationServicePrincipalInfoFromFile(rc, map);

return rc;
};
4 changes: 4 additions & 0 deletions src/lib/setup/setupLog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false) => {
projectName: "projectName",
scaffoldHLD: true,
scaffoldManifest: true,
subscriptionId: "72f988bf-86f1-41af-91ab-2d7cd011db48",
workspace: "workspace"
};

Expand All @@ -45,6 +46,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false) => {
"az_sp_id=b510c1ff-358c-4ed4-96c8-eb23f42bb65b",
"az_sp_password=********",
"az_sp_tenant=72f988bf-86f1-41af-91ab-2d7cd011db47",
"az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48",
"workspace: workspace",
"Project Created: yes",
"High Level Definition Repo Scaffolded: yes",
Expand All @@ -63,6 +65,7 @@ const positiveTest = (logExist?: boolean, withAppCreation = false) => {
"az_sp_id=",
"az_sp_password=",
"az_sp_tenant=",
"az_subscription_id=72f988bf-86f1-41af-91ab-2d7cd011db48",
"workspace: workspace",
"Project Created: yes",
"High Level Definition Repo Scaffolded: yes",
Expand Down Expand Up @@ -119,6 +122,7 @@ describe("test create function", () => {
"az_sp_id=",
"az_sp_password=",
"az_sp_tenant=",
"az_subscription_id=",
"workspace: workspace",
"Project Created: yes",
"High Level Definition Repo Scaffolded: yes",
Expand Down
1 change: 1 addition & 0 deletions src/lib/setup/setupLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const create = (rc: IRequestContext | undefined, file?: string) => {
`az_sp_id=${rc.servicePrincipalId || ""}`,
`az_sp_password=${rc.servicePrincipalPassword ? "********" : ""}`,
`az_sp_tenant=${rc.servicePrincipalTenantId || ""}`,
`az_subscription_id=${rc.subscriptionId || ""}`,
`workspace: ${rc.workspace}`,
`Project Created: ${getBooleanVal(rc.createdProject)}`,
`High Level Definition Repo Scaffolded: ${getBooleanVal(rc.scaffoldHLD)}`,
Expand Down
69 changes: 69 additions & 0 deletions src/lib/setup/subscriptionService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
Subscription,
SubscriptionClientOptions
} from "@azure/arm-subscriptions/src/models";
import { ApplicationTokenCredentials } from "@azure/ms-rest-nodeauth";
import * as restAuth from "@azure/ms-rest-nodeauth";
import { getSubscriptions } from "./subscriptionService";

jest.mock("@azure/arm-subscriptions", () => {
class MockClient {
constructor(
cred: ApplicationTokenCredentials,
options?: SubscriptionClientOptions
) {
return {
subscriptions: {
list: () => {
return [
{
displayName: "test",
subscriptionId: "1234567890-abcdef"
}
];
}
}
};
}
}
return {
SubscriptionClient: MockClient
};
});

describe("test getSubscriptions function", () => {
it("positive test: one value", async () => {
jest
.spyOn(restAuth, "loginWithServicePrincipalSecret")
.mockImplementationOnce(async () => {
return {};
});
const result = await getSubscriptions({
accessToken: "pat",
orgName: "org",
projectName: "project",
workspace: "test"
});
expect(result).toStrictEqual([
{
id: "1234567890-abcdef",
name: "test"
}
]);
});
it("negative test", async () => {
jest
.spyOn(restAuth, "loginWithServicePrincipalSecret")
.mockImplementationOnce(async () => {
throw Error("fake");
});
await expect(
getSubscriptions({
accessToken: "pat",
orgName: "org",
projectName: "project",
workspace: "test"
})
).rejects.toThrow();
});
});
Loading