Skip to content

Commit 2c5eddb

Browse files
committedNov 18, 2020
feat: Implement --detection-depth flag in iac module
Currently we look for IaC files within all subdirectories. This commit refactors the detection code to use direct directory traversal function that takes a `maxDepth` argument which will bail early once the max depth has been reached. This removes the dependency on the glob module, brings the file lookup inline with how we do the lookup for manifest files and offers a slight performance gain over glob. To maintain backwards compatibility with the existing functionality this implementation will traverse all directories by default and ignore dotfiles (files starting with a period).
1 parent c79b7e5 commit 2c5eddb

File tree

2 files changed

+86
-25
lines changed

2 files changed

+86
-25
lines changed
 

‎src/lib/iac/detect-iac.ts

+32-25
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as fs from 'fs';
22
import * as pathLib from 'path';
33
import * as debugLib from 'debug';
4-
import * as glob from 'glob';
54
import { isLocalFolder, localFileSuppliedButNotFound } from '../detect';
65
import { CustomError } from '../errors';
76
import { validateK8sFile, makeValidateTerraformRequest } from './iac-parser';
@@ -10,6 +9,7 @@ import {
109
IacProjectType,
1110
IacFileTypes,
1211
} from './constants';
12+
import { makeDirectoryIterator } from './makeDirectoryIterator';
1313
import {
1414
SupportLocalFileOnlyIacError,
1515
UnsupportedOptionFileIacError,
@@ -38,7 +38,9 @@ export async function getProjectType(
3838
// scanning the projects - we need save the files we need to scan on the options
3939
// so we could create assembly payloads for the relevant files.
4040
// We are sending it as a `Multi IaC` project - and later assign the relevant type for each project
41-
const directoryFiles = await getDirectoryFiles(root);
41+
const directoryFiles = await getDirectoryFiles(root, {
42+
maxDepth: options.detectionDepth,
43+
});
4244
options.iacDirFiles = directoryFiles;
4345
return IacProjectType.MULTI_IAC;
4446
}
@@ -84,33 +86,38 @@ async function getProjectTypeForIacFile(filePath: string) {
8486
return projectType;
8587
}
8688

87-
async function getDirectoryFiles(root: string) {
89+
async function getDirectoryFiles(
90+
root: string,
91+
options: { maxDepth?: number } = {},
92+
) {
8893
const iacFiles: IacFileInDirectory[] = [];
8994
const dirPath = pathLib.resolve(root, '.');
90-
const files = glob.sync(
91-
pathLib.join(dirPath, '/**/**/*.+(json|yaml|yml|tf)'),
92-
);
95+
const supportedExtensions = new Set(Object.keys(projectTypeByFileType));
96+
97+
const directoryPaths = makeDirectoryIterator(dirPath, {
98+
maxDepth: options.maxDepth,
99+
});
93100

94-
for (const fileName of files) {
95-
const ext = pathLib.extname(fileName).substr(1);
96-
if (Object.keys(projectTypeByFileType).includes(ext)) {
97-
const filePath = pathLib.resolve(root, fileName);
101+
for (const filePath of directoryPaths) {
102+
const fileType = pathLib.extname(filePath).substr(1) as IacFileTypes;
103+
if (!fileType || !supportedExtensions.has(fileType)) {
104+
continue;
105+
}
98106

99-
await getProjectTypeForIacFile(filePath)
100-
.then((projectType) => {
101-
iacFiles.push({
102-
filePath,
103-
projectType,
104-
fileType: ext as IacFileTypes,
105-
});
106-
})
107-
.catch((err: CustomError) => {
108-
iacFiles.push({
109-
filePath,
110-
fileType: ext as IacFileTypes,
111-
failureReason: err.userMessage,
112-
});
113-
});
107+
try {
108+
const projectType = await getProjectTypeForIacFile(filePath);
109+
iacFiles.push({
110+
filePath,
111+
projectType,
112+
fileType,
113+
});
114+
} catch (err) {
115+
iacFiles.push({
116+
filePath,
117+
fileType,
118+
failureReason:
119+
err instanceof CustomError ? err.userMessage : 'Unhandled Error',
120+
});
114121
}
115122
}
116123

‎src/lib/iac/makeDirectoryIterator.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as fs from 'fs';
2+
import { resolve } from 'path';
3+
4+
/**
5+
* Walk the provided directory tree, depth first, yielding the matched filename
6+
* to the caller. An optional `maxDepth` argument can be provided to limit how
7+
* deep in the file tree the search will go.
8+
*/
9+
export function* makeDirectoryIterator(
10+
root: string,
11+
options: { maxDepth?: number; includeDotfiles?: boolean } = {},
12+
) {
13+
if (!isDirectory(root)) {
14+
throw new Error(`Path "${root}" is not a directory`);
15+
}
16+
17+
// Internal function to allow us to track the recursion depth.
18+
function* walk(dirpath: string, currentDepth: number) {
19+
const filenames = fs.readdirSync(dirpath);
20+
for (const filename of filenames) {
21+
// NOTE: We filter filenames that start with a period to maintain
22+
// backwards compatibility with the original implementation that used the
23+
// "glob" package.
24+
if (!options.includeDotfiles && filename.startsWith('.')) {
25+
continue;
26+
}
27+
28+
const resolved = resolve(dirpath, filename);
29+
if (isDirectory(resolved)) {
30+
// Skip this directory if max depth has been reached.
31+
if (options.maxDepth === currentDepth) {
32+
continue;
33+
}
34+
35+
yield* walk(resolved, currentDepth + 1);
36+
} else {
37+
yield resolved;
38+
}
39+
}
40+
}
41+
42+
yield* walk(root, 1);
43+
}
44+
45+
// NOTE: We use isDirectory here instead of isLocalFolder() in order to
46+
// follow symlinks and match the original glob() implementation.
47+
function isDirectory(path: string): boolean {
48+
try {
49+
// statSync will resolve symlinks, lstatSync will not.
50+
return fs.statSync(path).isDirectory();
51+
} catch (e) {
52+
return false;
53+
}
54+
}

0 commit comments

Comments
 (0)
Please sign in to comment.