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

Commit 9b39c46

Browse files
mtarngbnookala
andauthored
hld lifecycle pipeline to blow away repository directory in hld before regenerating assets (#505)
* Adding purgeRepositoryComponent to reconcile step to remove existing files on hld with some exceptions * util for getting all files in directory and subdirectories * Adding purging of project directories for reconcile in hld repository * Tested logic locally, looks to be working. Need to add unit tests and possibly ITs * tests * feedback Co-authored-by: Bhargav Nookala <[email protected]>
1 parent e3d5f87 commit 9b39c46

File tree

5 files changed

+264
-0
lines changed

5 files changed

+264
-0
lines changed

src/commands/hld/reconcile.test.ts

+108
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ import {
1616
execute,
1717
getFullPathPrefix,
1818
normalizedName,
19+
purgeRepositoryComponents,
1920
ReconcileDependencies,
2021
reconcileHld,
2122
testAndGetAbsPath,
2223
validateInputs,
2324
} from "./reconcile";
25+
import mockFs from "mock-fs";
26+
import fs from "fs";
2427

2528
beforeAll(() => {
2629
enableVerboseLogging();
@@ -169,6 +172,109 @@ describe("createAccessYaml", () => {
169172
});
170173
});
171174

175+
describe("purgeRepositoryComponents", () => {
176+
const fsSpy = jest.spyOn(fs, "unlink");
177+
178+
beforeEach(() => {
179+
mockFs({
180+
"hld-repo": {
181+
config: {
182+
"common.yaml": "someconfigfile",
183+
},
184+
"bedrock-project-repo": {
185+
"access.yaml": "someaccessfile",
186+
config: {
187+
"common.yaml": "someconfigfile",
188+
},
189+
serviceA: {
190+
config: {
191+
"common.yaml": "someconfigfile",
192+
},
193+
master: {
194+
config: {
195+
"common.yaml": "someconfigfile",
196+
"prod.yaml": "someconfigfile",
197+
"stage.yaml": "someconfigfile",
198+
},
199+
static: {
200+
"ingressroute.yaml": "ingressroutefile",
201+
"middlewares.yaml": "middlewaresfile",
202+
},
203+
"component.yaml": "somecomponentfile",
204+
},
205+
"component.yaml": "somecomponentfile",
206+
},
207+
"component.yaml": "somecomponentfile",
208+
},
209+
"component.yaml": "somecomponentfile",
210+
},
211+
});
212+
});
213+
214+
afterEach(() => {
215+
mockFs.restore();
216+
jest.clearAllMocks();
217+
fsSpy.mockClear();
218+
});
219+
220+
const hldPath = "hld-repo";
221+
const repositoryName = "bedrock-project-repo";
222+
223+
it("should invoke fs.unlink for each file in project repository except config files and access.yaml", async () => {
224+
purgeRepositoryComponents(hldPath, repositoryName);
225+
226+
expect(fs.unlink).toHaveBeenCalledTimes(5);
227+
expect(fs.unlink).toHaveBeenCalledWith(
228+
path.join(hldPath, repositoryName, "component.yaml"),
229+
expect.any(Function)
230+
);
231+
expect(fs.unlink).toHaveBeenCalledWith(
232+
path.join(hldPath, repositoryName, "serviceA", "component.yaml"),
233+
expect.any(Function)
234+
);
235+
expect(fs.unlink).toHaveBeenCalledWith(
236+
path.join(
237+
hldPath,
238+
repositoryName,
239+
"serviceA",
240+
"master",
241+
"component.yaml"
242+
),
243+
expect.any(Function)
244+
);
245+
expect(fs.unlink).toHaveBeenCalledWith(
246+
path.join(
247+
hldPath,
248+
repositoryName,
249+
"serviceA",
250+
"master",
251+
"static",
252+
"middlewares.yaml"
253+
),
254+
expect.any(Function)
255+
);
256+
expect(fs.unlink).toHaveBeenCalledWith(
257+
path.join(
258+
hldPath,
259+
repositoryName,
260+
"serviceA",
261+
"master",
262+
"static",
263+
"ingressroute.yaml"
264+
),
265+
expect.any(Function)
266+
);
267+
});
268+
269+
it("should throw an error if fs fails", async () => {
270+
fsSpy.mockImplementationOnce(() => {
271+
throw Error("some error");
272+
});
273+
274+
expect(() => purgeRepositoryComponents(hldPath, repositoryName)).toThrow();
275+
});
276+
});
277+
172278
describe("createRepositoryComponent", () => {
173279
let exec = jest.fn().mockReturnValue(Promise.resolve({}));
174280
const hldPath = `myMonoRepo`;
@@ -446,6 +552,7 @@ describe("reconcile tests", () => {
446552
exec: jest.fn().mockReturnValue(Promise.resolve({})),
447553
generateAccessYaml: jest.fn(),
448554
getGitOrigin: jest.fn(),
555+
purgeRepositoryComponents: jest.fn(),
449556
writeFile: jest.fn(),
450557
};
451558

@@ -494,6 +601,7 @@ describe("reconcile tests", () => {
494601
expect(dependencies.createMiddlewareForRing).toHaveBeenCalledTimes(2);
495602
expect(dependencies.createIngressRouteForRing).toHaveBeenCalledTimes(2);
496603
expect(dependencies.generateAccessYaml).toHaveBeenCalledTimes(1);
604+
expect(dependencies.purgeRepositoryComponents).toHaveBeenCalledTimes(1);
497605
expect(dependencies.generateAccessYaml).toBeCalledWith(
498606
"path/to/hld/service",
499607
git,

src/commands/hld/reconcile.ts

+49
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { BedrockFile, BedrockServiceConfig } from "../../types";
1919
import decorator from "./reconcile.decorator.json";
2020
import { build as buildError, log as logError } from "../../lib/errorBuilder";
2121
import { errorStatusCode } from "../../lib/errorStatusCode";
22+
import { getAllFilesInDirectory } from "../../lib/ioUtil";
2223

2324
/**
2425
* IExecResult represents the possible return value of a Promise based wrapper
@@ -101,6 +102,46 @@ type MiddlewareMap<T = Partial<ReturnType<typeof middleware.create>>> = {
101102
default?: T;
102103
};
103104

105+
/**
106+
* In spk hld reconcile, the results should always result in the same artifacts being created based on the state of bedrock.yaml.
107+
* The only exception is for files under the /config directories and any access.yaml files.
108+
* @param absHldPath Absolute path to the local HLD repository directory
109+
* @param repositoryName Name of the bedrock project repository/directory inside of the HLD repository
110+
*/
111+
export const purgeRepositoryComponents = (
112+
absHldPath: string,
113+
repositoryName: string
114+
): void => {
115+
assertIsStringWithContent(absHldPath, "hld-path");
116+
assertIsStringWithContent(repositoryName, "repository-name");
117+
118+
const filesToDelete = getAllFilesInDirectory(
119+
path.join(absHldPath, repositoryName)
120+
).filter(
121+
(filePath) =>
122+
!filePath.endsWith("access.yaml") && !filePath.match(/config\/.*\.yaml$/)
123+
);
124+
125+
try {
126+
filesToDelete.forEach((file) => {
127+
fs.unlink(file, function (err) {
128+
if (err) throw err;
129+
console.log(`${file} deleted!`);
130+
});
131+
});
132+
} catch (err) {
133+
throw buildError(
134+
errorStatusCode.FILE_IO_ERR,
135+
{
136+
errorKey: "hld-reconcile-err-purge-repo-comps",
137+
values: [repositoryName, absHldPath],
138+
},
139+
err
140+
);
141+
}
142+
return;
143+
};
144+
104145
export const createRepositoryComponent = async (
105146
execCmd: typeof execAndLog,
106147
absHldPath: string,
@@ -381,6 +422,7 @@ export interface ReconcileDependencies {
381422
getGitOrigin: typeof tryGetGitOrigin;
382423
generateAccessYaml: typeof generateAccessYaml;
383424
createAccessYaml: typeof createAccessYaml;
425+
purgeRepositoryComponents: typeof purgeRepositoryComponents;
384426
createRepositoryComponent: typeof createRepositoryComponent;
385427
configureChartForRing: typeof configureChartForRing;
386428
createServiceComponent: typeof createServiceComponent;
@@ -454,6 +496,12 @@ export const reconcileHld = async (
454496
): Promise<void> => {
455497
const { services: managedServices, rings: managedRings } = bedrockYaml;
456498

499+
// To support removing services and rings, first remove all files under an application repository directory except anything in a /config directory and any access.yaml files, then we generate all values again.
500+
dependencies.purgeRepositoryComponents(
501+
absHldPath,
502+
normalizedName(repositoryName)
503+
);
504+
457505
// Create Repository Component if it doesn't exist.
458506
// In a pipeline, the repository component is the name of the application repository.
459507
await dependencies.createRepositoryComponent(
@@ -649,6 +697,7 @@ export const execute = async (
649697
exec: execAndLog,
650698
generateAccessYaml,
651699
getGitOrigin: tryGetGitOrigin,
700+
purgeRepositoryComponents,
652701
writeFile: fs.writeFileSync,
653702
};
654703

src/lib/i18n.json

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"hld-reconcile-err-path": "Could not validate {0} path.",
3232
"hld-reconcile-err-cmd-failed": "An error occurred executing command: {0}",
3333
"hld-reconcile-err-cmd-exe": "An error occurred while reconciling HLD.",
34+
"hld-reconcile-err-purge-repo-comps": "Could not purge hld repository component {0} in path {1}.",
3435

3536
"infra-scaffold-cmd-failed": "Infra scaffold command was not successfully executed.",
3637
"infra-scaffold-cmd-src-missing": "Value for source is required because it cannot be constructed with properties in spk-config.yaml. Provide value for source.",

src/lib/ioUtil.test.ts

+81
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,25 @@ import path from "path";
33
import uuid from "uuid/v4";
44
import {
55
createTempDir,
6+
getAllFilesInDirectory,
67
getMissingFilenames,
78
isDirEmpty,
89
removeDir,
910
} from "./ioUtil";
11+
import mockFs from "mock-fs";
12+
import { disableVerboseLogging, enableVerboseLogging, logger } from "../logger";
13+
14+
beforeAll(() => {
15+
enableVerboseLogging();
16+
});
17+
18+
afterAll(() => {
19+
disableVerboseLogging();
20+
});
21+
22+
beforeEach(() => {
23+
jest.clearAllMocks();
24+
});
1025

1126
describe("test createTempDir function", () => {
1227
it("create and existence check", () => {
@@ -110,3 +125,69 @@ describe("test doFilesExist function", () => {
110125
expect(missing.length).toBe(1);
111126
});
112127
});
128+
129+
describe("test getAllFilesInDirectory function", () => {
130+
beforeEach(() => {
131+
mockFs({
132+
"hld-repo": {
133+
config: {
134+
"common.yaml": "someconfigfile",
135+
},
136+
"bedrock-project-repo": {
137+
"access.yaml": "someaccessfile",
138+
config: {
139+
"common.yaml": "someconfigfile",
140+
},
141+
serviceA: {
142+
config: {
143+
"common.yaml": "someconfigfile",
144+
},
145+
master: {
146+
config: {
147+
"common.yaml": "someconfigfile",
148+
"prod.yaml": "someconfigfile",
149+
"stage.yaml": "someconfigfile",
150+
},
151+
static: {
152+
"ingressroute.yaml": "ingressroutefile",
153+
"middlewares.yaml": "middlewaresfile",
154+
},
155+
"component.yaml": "somecomponentfile",
156+
},
157+
"component.yaml": "somecomponentfile",
158+
},
159+
"component.yaml": "somecomponentfile",
160+
},
161+
"component.yaml": "somecomponentfile",
162+
},
163+
});
164+
});
165+
166+
afterEach(() => {
167+
mockFs.restore();
168+
});
169+
170+
it("gets all files in a populated directory", () => {
171+
const fileList = getAllFilesInDirectory("hld-repo");
172+
expect(fileList).toHaveLength(13);
173+
expect(fileList).toContain(
174+
"hld-repo/bedrock-project-repo/serviceA/master/static/middlewares.yaml"
175+
);
176+
177+
const filesToDelete = fileList.filter(
178+
(filePath) =>
179+
!filePath.match(/access\.yaml$/) && !filePath.match(/config\/.*\.yaml$/)
180+
);
181+
logger.info("filestoDelete.length: " + filesToDelete.length);
182+
logger.info(filesToDelete);
183+
});
184+
185+
it("returns an empty list when there's no files directory", () => {
186+
mockFs({
187+
emptyDirectory: {},
188+
});
189+
190+
const fileList = getAllFilesInDirectory("emptyDirectory");
191+
expect(fileList).toHaveLength(0);
192+
});
193+
});

src/lib/ioUtil.ts

+25
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from "fs";
22
import os from "os";
33
import path from "path";
44
import uuid from "uuid/v4";
5+
import { logger } from "../logger";
56

67
/**
78
* Creates a random directory in tmp directory.
@@ -68,3 +69,27 @@ export const getMissingFilenames = (
6869
)
6970
.map((f) => path.basename(f));
7071
};
72+
73+
/**
74+
* Returns all files/filepaths in this directory and subdirectories.
75+
*
76+
* @param dir directory to search
77+
* @param files list of files
78+
* @returns list of files in given directory and subdirectories.
79+
*/
80+
export const getAllFilesInDirectory = (
81+
dir: string,
82+
files: string[] = []
83+
): string[] => {
84+
const filesInDir = fs.readdirSync(dir);
85+
86+
filesInDir.forEach(function (file) {
87+
if (fs.statSync(path.join(dir, file)).isDirectory()) {
88+
files = getAllFilesInDirectory(path.join(dir, file), files);
89+
} else {
90+
files.push(path.join(dir, file));
91+
}
92+
});
93+
94+
return files;
95+
};

0 commit comments

Comments
 (0)