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

Commit 2d28e0e

Browse files
dennisseahandrebriggsyradsmikham
authored
[FEATURE] spk setup command: added scaffold HLD and Manifest repos (#358)
* [HOUSEKEEPING] refactor spk setup command * remove unused imports * reset default project name * added code to create HLD and manifest repo 1. added a request context object to capture all valid input values and status of execution (.e.g is project created, what are the repo created). 2. added `spk setup` as one of the commands. 3. added file operation supports for creating folder, change directory, etc. 4. added git operation supports 5. modified project service to have the right value for `templateTypeId` and `sourceControlType` 6. added scaffold operation supports 7. added code to create setup.log file main feature added: scaffold HLD and Manifest repos * typo * fix gramatical error * added more test code for gitService * improve setupLog test * added code to poll project existence after creation * remove input file * incorporate feedback from review Co-authored-by: Andre Briggs <andrebriggs@users.noreply.github.com> Co-authored-by: Yvonne Radsmikham <yvonne.radsmikham@gmail.com>
1 parent b7ab8fc commit 2d28e0e

19 files changed

+1190
-104
lines changed

src/commands/setup.decorator.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"command": "setup",
33
"alias": "s",
44
"description": "An interactive command to setup resources in azure and azure dev-ops",
5-
"disabled": true,
65
"options": [
76
{
87
"arg": "-f, --file <config-file-path>",

src/commands/setup.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ for a few questions
1212
2. Azure DevOps Project Name, the project to be created.
1313
3. Azure DevOps Personal Access Token. The token needs to have these permissions
1414
1. Read and write projects.
15+
2. Read and write codes.
1516

1617
It can also run in a non interactive mode by providing a file that contains
1718
answers to the above questions.
@@ -28,4 +29,21 @@ azdo_project_name=<Azure DevOps Project Name>
2829
azdo_pat=<Azure DevOps Personal Access Token>
2930
```
3031

31-
azdo_project_name is optional and default value is `BedrockRocks`
32+
`azdo_project_name` is optional and default value is `BedrockRocks`.
33+
34+
The followings shall be created
35+
36+
1. A working directory, `quick-start-env`
37+
2. Project shall not be created if it already exists.
38+
3. A Git Repo, `quick-start-hld`, it shall be deleted and recreated if it
39+
already exists.
40+
1. And initial commit shall be made to this repo
41+
4. A Git Repo, `quick-start-manifest`, it shall be deleted and recreated if it
42+
already exists.
43+
1. And initial commit shall be made to this repo
44+
45+
## Setup log
46+
47+
A `setup.log` file is created after running this command. This file contains
48+
information about what are created and the execution status (completed or
49+
incomplete). This file will not be created if input validation failed.

src/commands/setup.test.ts

Lines changed: 91 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,29 @@ 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";
7+
import * as fsUtil from "../lib/setup/fsUtil";
8+
import * as gitService from "../lib/setup/gitService";
69
import * as projectService from "../lib/setup/projectService";
710
import * as promptInstance from "../lib/setup/prompt";
11+
import * as scaffold from "../lib/setup/scaffold";
12+
import * as setupLog from "../lib/setup/setupLog";
813
import { IConfigYaml } from "../types";
9-
import { createSPKConfig, execute } from "./setup";
14+
import { createSPKConfig, execute, getErrorMessage } from "./setup";
1015
import * as setup from "./setup";
1116

17+
const mockRequestContext = {
18+
accessToken: "pat",
19+
orgName: "orgname",
20+
projectName: "project",
21+
workspace: WORKSPACE
22+
};
23+
1224
describe("test createSPKConfig function", () => {
1325
it("positive test", () => {
1426
const tmpFile = path.join(createTempDir(), "config.yaml");
1527
jest.spyOn(config, "defaultConfigFile").mockReturnValueOnce(tmpFile);
16-
const input = {
17-
azdo_org_name: "orgname",
18-
azdo_pat: "pat",
19-
azdo_project_name: "project"
20-
};
21-
createSPKConfig(input);
28+
createSPKConfig(mockRequestContext);
2229
const data = readYaml<IConfigYaml>(tmpFile);
2330
expect(data.azure_devops).toStrictEqual({
2431
access_token: "pat",
@@ -29,22 +36,24 @@ describe("test createSPKConfig function", () => {
2936
});
3037

3138
const testExecuteFunc = async (usePrompt = true, hasProject = true) => {
39+
jest
40+
.spyOn(gitService, "getGitApi")
41+
.mockReturnValueOnce(Promise.resolve({} as any));
42+
jest.spyOn(fsUtil, "createDirectory").mockReturnValueOnce();
43+
jest.spyOn(scaffold, "hldRepo").mockReturnValueOnce(Promise.resolve());
44+
jest.spyOn(scaffold, "manifestRepo").mockReturnValueOnce(Promise.resolve());
45+
jest.spyOn(setupLog, "create").mockReturnValueOnce();
46+
3247
const exitFn = jest.fn();
3348

3449
if (usePrompt) {
35-
jest.spyOn(promptInstance, "prompt").mockReturnValueOnce(
36-
Promise.resolve({
37-
azdo_org_name: "orgname",
38-
azdo_pat: "pat",
39-
azdo_project_name: "project"
40-
})
41-
);
50+
jest
51+
.spyOn(promptInstance, "prompt")
52+
.mockReturnValueOnce(Promise.resolve(mockRequestContext));
4253
} else {
43-
jest.spyOn(promptInstance, "getAnswerFromFile").mockReturnValueOnce({
44-
azdo_org_name: "orgname",
45-
azdo_pat: "pat",
46-
azdo_project_name: "project"
47-
});
54+
jest
55+
.spyOn(promptInstance, "getAnswerFromFile")
56+
.mockReturnValueOnce(mockRequestContext);
4857
}
4958
jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce();
5059
jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce(
@@ -109,13 +118,9 @@ describe("test execute function", () => {
109118
it("negative test: 401 status code", async () => {
110119
const exitFn = jest.fn();
111120

112-
jest.spyOn(promptInstance, "prompt").mockReturnValueOnce(
113-
Promise.resolve({
114-
azdo_org_name: "orgname",
115-
azdo_pat: "pat",
116-
azdo_project_name: "project"
117-
})
118-
);
121+
jest
122+
.spyOn(promptInstance, "prompt")
123+
.mockReturnValueOnce(Promise.resolve(mockRequestContext));
119124
jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce();
120125
jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce(
121126
Promise.resolve({
@@ -127,6 +132,8 @@ describe("test execute function", () => {
127132
}
128133
} as any)
129134
);
135+
jest.spyOn(setupLog, "create").mockReturnValueOnce();
136+
130137
await execute(
131138
{
132139
file: undefined
@@ -140,13 +147,9 @@ describe("test execute function", () => {
140147
it("negative test: VS402392 error", async () => {
141148
const exitFn = jest.fn();
142149

143-
jest.spyOn(promptInstance, "prompt").mockReturnValueOnce(
144-
Promise.resolve({
145-
azdo_org_name: "orgname",
146-
azdo_pat: "pat",
147-
azdo_project_name: "project"
148-
})
149-
);
150+
jest
151+
.spyOn(promptInstance, "prompt")
152+
.mockReturnValueOnce(Promise.resolve(mockRequestContext));
150153
jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce();
151154
jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce(
152155
Promise.resolve({
@@ -157,6 +160,8 @@ describe("test execute function", () => {
157160
}
158161
} as any)
159162
);
163+
jest.spyOn(setupLog, "create").mockReturnValueOnce();
164+
160165
await execute(
161166
{
162167
file: undefined
@@ -170,13 +175,37 @@ describe("test execute function", () => {
170175
it("negative test: other error", async () => {
171176
const exitFn = jest.fn();
172177

173-
jest.spyOn(promptInstance, "prompt").mockReturnValueOnce(
178+
jest
179+
.spyOn(promptInstance, "prompt")
180+
.mockReturnValueOnce(Promise.resolve(mockRequestContext));
181+
jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce();
182+
jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce(
174183
Promise.resolve({
175-
azdo_org_name: "orgname",
176-
azdo_pat: "pat",
177-
azdo_project_name: "project"
178-
})
184+
getCoreApi: () => {
185+
throw {
186+
message: "other error"
187+
};
188+
}
189+
} as any)
179190
);
191+
jest.spyOn(setupLog, "create").mockReturnValueOnce();
192+
193+
await execute(
194+
{
195+
file: undefined
196+
},
197+
exitFn
198+
);
199+
200+
expect(exitFn).toBeCalledTimes(1);
201+
expect(exitFn.mock.calls).toEqual([[1]]);
202+
});
203+
it("negative test: other error", async () => {
204+
const exitFn = jest.fn();
205+
206+
jest
207+
.spyOn(promptInstance, "prompt")
208+
.mockReturnValueOnce(Promise.resolve(mockRequestContext));
180209
jest.spyOn(setup, "createSPKConfig").mockReturnValueOnce();
181210
jest.spyOn(azdoClient, "getWebApi").mockReturnValueOnce(
182211
Promise.resolve({
@@ -198,3 +227,27 @@ describe("test execute function", () => {
198227
expect(exitFn.mock.calls).toEqual([[1]]);
199228
});
200229
});
230+
231+
describe("test getErrorMessage function", () => {
232+
it("without request context", () => {
233+
const res = getErrorMessage(undefined, new Error("test"));
234+
expect(res).toBe("Error: test");
235+
});
236+
it("with VS402392 error", () => {
237+
const res = getErrorMessage(
238+
{
239+
accessToken: "pat",
240+
orgName: "orgName",
241+
projectName: "projectName",
242+
workspace: WORKSPACE
243+
},
244+
{
245+
message: "VS402392: ",
246+
statusCode: 400
247+
}
248+
);
249+
expect(res).toBe(
250+
"Project, projectName might have been deleted less than 28 days ago. Choose a different project name."
251+
);
252+
});
253+
});

src/commands/setup.ts

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,58 @@ import yaml from "js-yaml";
44
import { defaultConfigFile } from "../config";
55
import { getWebApi } from "../lib/azdoClient";
66
import { build as buildCmd, exit as exitCmd } from "../lib/commandBuilder";
7+
import { IRequestContext, WORKSPACE } from "../lib/setup/constants";
8+
import { createDirectory } from "../lib/setup/fsUtil";
9+
import { getGitApi } from "../lib/setup/gitService";
710
import { createProjectIfNotExist } from "../lib/setup/projectService";
8-
import {
9-
DEFAULT_PROJECT_NAME,
10-
getAnswerFromFile,
11-
IAnswer,
12-
prompt
13-
} from "../lib/setup/prompt";
11+
import { getAnswerFromFile, prompt } from "../lib/setup/prompt";
12+
import { hldRepo, manifestRepo } from "../lib/setup/scaffold";
13+
import { create as createSetupLog } from "../lib/setup/setupLog";
1414
import { logger } from "../logger";
1515
import decorator from "./setup.decorator.json";
1616

1717
interface ICommandOptions {
1818
file: string | undefined;
1919
}
2020

21+
interface IAPIError {
22+
message: string;
23+
statusCode: number;
24+
}
25+
2126
/**
2227
* Creates SPK config file under `user-home/.spk` folder
2328
*
2429
* @param answers Answers provided to the commander
2530
*/
26-
export const createSPKConfig = (answers: IAnswer) => {
31+
export const createSPKConfig = (rc: IRequestContext) => {
2732
const data = yaml.safeDump({
2833
azure_devops: {
29-
access_token: answers.azdo_pat,
30-
org: answers.azdo_org_name,
31-
project: answers.azdo_project_name
34+
access_token: rc.accessToken,
35+
org: rc.orgName,
36+
project: rc.projectName
3237
}
3338
});
3439
fs.writeFileSync(defaultConfigFile(), data);
3540
};
3641

42+
export const getErrorMessage = (
43+
rc: IRequestContext | undefined,
44+
err: Error | IAPIError
45+
) => {
46+
if (rc) {
47+
if (err.message && err.message.indexOf("VS402392") !== -1) {
48+
return `Project, ${
49+
rc!.projectName
50+
} might have been deleted less than 28 days ago. Choose a different project name.`;
51+
}
52+
if (!(err instanceof Error) && err.statusCode && err.statusCode === 401) {
53+
return `Authentication Failed. Make sure that the organization name and access token are correct; or your access token may have expired.`;
54+
}
55+
}
56+
return err.toString();
57+
};
58+
3759
/**
3860
* Executes the command, can all exit function with 0 or 1
3961
* when command completed successfully or failed respectively.
@@ -45,27 +67,34 @@ export const execute = async (
4567
opts: ICommandOptions,
4668
exitFn: (status: number) => Promise<void>
4769
) => {
70+
// tslint:disable-next-line: no-unnecessary-initializer
71+
let requestContext: IRequestContext | undefined = undefined;
72+
4873
try {
49-
const answers = opts.file ? getAnswerFromFile(opts.file) : await prompt();
74+
requestContext = opts.file ? getAnswerFromFile(opts.file) : await prompt();
75+
createDirectory(WORKSPACE, true);
5076

51-
createSPKConfig(answers!);
77+
createSPKConfig(requestContext!);
5278
const webAPI = await getWebApi();
5379
const coreAPI = await webAPI.getCoreApi();
80+
const gitAPI = await getGitApi(webAPI);
5481

55-
await createProjectIfNotExist(coreAPI, answers);
82+
await createProjectIfNotExist(coreAPI, requestContext);
83+
await hldRepo(gitAPI, requestContext);
84+
await manifestRepo(gitAPI, requestContext);
85+
86+
createSetupLog(requestContext);
5687
await exitFn(0);
5788
} catch (err) {
58-
if (err.statusCode === 401) {
59-
logger.error(
60-
`Authentication Failed. Make sure that the organization name and access token are correct; or your access token may have expired.`
61-
);
62-
} else if (err.message && err.message.indexOf("VS402392") !== -1) {
63-
logger.error(
64-
`Project, ${DEFAULT_PROJECT_NAME} might be deleted less than 28 days ago. Choose a different project name.`
65-
);
66-
} else {
67-
logger.error(err);
89+
const msg = getErrorMessage(requestContext, err);
90+
91+
// requestContext will not be created if input validation failed
92+
if (requestContext) {
93+
requestContext.error = msg;
6894
}
95+
createSetupLog(requestContext!);
96+
97+
logger.error(msg);
6998
await exitFn(1);
7099
}
71100
};

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ const commandModules = [
4545
c => {
4646
c.version(require("../package.json").version);
4747
},
48-
initCommandDecorator
49-
// setupCommandDecorator
48+
initCommandDecorator,
49+
setupCommandDecorator
5050
],
5151
cmds
5252
);

src/lib/setup/constants.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export interface IRequestContext {
2+
orgName: string;
3+
projectName: string;
4+
accessToken: string;
5+
workspace: string;
6+
createdProject?: boolean;
7+
scaffoldHLD?: boolean;
8+
scaffoldManifest?: boolean;
9+
error?: string;
10+
}
11+
12+
export const MANIFEST_REPO = "quick-start-manifest";
13+
export const HLD_REPO = "quick-start-hld";
14+
export const APP_REPO = "quick-start-app";
15+
export const DEFAULT_PROJECT_NAME = "BedrockRocks";
16+
export const APP_REPO_LIFECYCLE = "quick-start-lifecycle";
17+
export const WORKSPACE = "quick-start-env";
18+
export const SP_USER_NAME = "service_account";
19+
export const SETUP_LOG = "setup.log";
20+
21+
export const HLD_DEFAULT_GIT_URL =
22+
"https://github.com/microsoft/fabrikate-definitions.git";
23+
export const HLD_DEFAULT_COMPONENT_NAME = "traefik2";
24+
export const HLD_DEFAULT_DEF_PATH = "definitions/traefik2";

0 commit comments

Comments
 (0)