terraguardian-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.babelrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "presets": [
3
+ "@babel/preset-env"
4
+ ],
5
+ "plugins": []
6
+ }
7
+
package/.eslintrc.yaml ADDED
@@ -0,0 +1,12 @@
1
+ extends:
2
+ - airbnb-base
3
+ - plugin:import/errors
4
+ - plugin:import/warnings
5
+ rules:
6
+ prefer-destructuring: off
7
+ no-template-curly-in-string: warn
8
+ no-return-await: off
9
+ no-await-in-loop: warn
10
+ no-plusplus: off
11
+ import/extensions: off
12
+ import/no-unresolved: "off"
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,5 @@
1
+ include:
2
+ - project: 'grp-development-utils/cicd-pipeline-utilities/node-cicd'
3
+ ref: v1-latest
4
+ file:
5
+ - 'consumer/templates/node-package.yml'
package/.prettierrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "useTabs": false
6
+ }
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # terraguardian-cli
2
+
3
+ # Working with the
4
+
5
+ ## Step 1: Install the Package
6
+
7
+ Navigate to the root directory of your package in the terminal:
8
+ cd /path/to/your-package-name
9
+ Install the package dependencies:
10
+ npm install
11
+
12
+ ## Step 2: Link the Package Globally
13
+
14
+ Run the following command to create a global symbolic link for your package:
15
+ npm link
16
+ This command will create a symbolic link to your package in the global node_modules directory, making it accessible from anywhere on your system.
17
+
18
+ ## Step 3: Link the Package Locally
19
+
20
+ Navigate to the directory of your project where you want to use the package:
21
+ cd /path/to/your-project
22
+ Link the package to your project using its name:
23
+ npm link your-package-name
24
+ Replace your-package-name with the actual name of your package.
25
+
26
+ ## Step 4: Use the Package in Your Project
27
+
28
+ Now you can use the package in your project as if it were installed from npm. For example, you can import it into your code:
29
+
30
+ const yourPackage = require('your-package-name');
31
+
32
+ ## Step 5: Test the Package
33
+
34
+ You can test the package locally by running the tests located in the test/ directory. Use the following command:
35
+ npm test
36
+ This will execute the test suite for your package and provide feedback on its functionality.
37
+
38
+ ## Step 6: Make Changes and Iterate
39
+
40
+ You can make changes to your package code and repeat the testing process as needed. Since you've linked the package globally, any changes you make to the package code will be reflected immediately in your project without the need to reinstall it.
41
+
42
+ ## Step 7: Cleanup (Optional)
43
+
44
+ Once you're done testing and no longer need the global link, you can remove it using:
45
+ npm unlink
46
+ This will remove the global symbolic link created for your package.
47
+
48
+ That's it! You've successfully installed, tested, and used your package locally using npm link.
package/jest.config.js ADDED
@@ -0,0 +1,21 @@
1
+ module.exports = {
2
+ roots: ['<rootDir>'],
3
+ moduleFileExtensions: ['js', 'json'],
4
+ testEnvironment: 'node',
5
+ testRegex: '\\.test\\.js$',
6
+ transform: {
7
+ '^.+\\.js$': 'babel-jest',
8
+ },
9
+ collectCoverage: true,
10
+ coverageDirectory: 'test/coverage',
11
+ collectCoverageFrom: ['<rootDir>/src/**/*.js'],
12
+ coveragePathIgnorePatterns: ['/node_modules/', '/build/', '/dist/'], // Add paths to ignore here
13
+ coverageThreshold: {
14
+ global: {
15
+ branches: 0,
16
+ functions: 0,
17
+ lines: 0,
18
+ statements: 0,
19
+ },
20
+ },
21
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "terraguardian-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI that helps with projects that are built with terraform and helps with deployments of lambda src code",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "tg-cli": "src/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "jest",
11
+ "lint": "eslint src",
12
+ "format": "prettier --write ."
13
+ },
14
+ "scope": "@urbangemfinder",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://gitlab.com/grp-development-utils/terraguardian-cli.git"
18
+ },
19
+ "author": "Martin Colmenero",
20
+ "license": "ISC",
21
+ "bugs": {
22
+ "url": "https://gitlab.com/grp-development-utils/terraguardian-cli/issues"
23
+ },
24
+ "homepage": "https://gitlab.com/grp-development-utils/terraguardian-cli#readme",
25
+ "dependencies": {
26
+ "archiver": "^7.0.0",
27
+ "chalk": "^4.0.0",
28
+ "inquirer": "^8.0.0",
29
+ "yargs": "^17.7.2"
30
+ },
31
+ "devDependencies": {
32
+ "@babel/core": "^7.24.0",
33
+ "@babel/preset-env": "^7.24.0",
34
+ "babel-jest": "^29.7.0",
35
+ "eslint": "^8.57.0",
36
+ "eslint-config-airbnb-base": "^15.0.0",
37
+ "eslint-plugin-import": "^2.29.1",
38
+ "jest": "^29.7.0",
39
+ "prettier": "^3.2.5"
40
+ }
41
+ }
@@ -0,0 +1,22 @@
1
+ const NODE_ENTRY_POINT = process.env.NODE_ENTRY_POINT || 'index.js';
2
+ const WEBPACK_FILE_PATH = process.env.WEBPACK_FILE_PATH || 'webpack.config.js';
3
+ const NODE_LIBARY_TYPE = process.env.NODE_LIBARY_TYPE || 'commonjs2';
4
+ const DEFAULT_WEBPACK_MODE = process.env.DEFAULT_WEBPACK_MODE || 'production';
5
+ const NODE_BUILD_FILE_NAME = process.env.NODE_BUILD_FILE_NAME || 'index.js';
6
+
7
+ const NODE_RUNTIME_TF_VAR = 'node_runtime';
8
+ const GO_RUNTIME_TF_VAR = 'go_runtime';
9
+ const LAMBDA_BUILD_DIR = 'build';
10
+ const GO_ENTRY_POINT = process.env.GO_ENTRY_POINT || 'main.go';
11
+
12
+ module.exports = {
13
+ DEFAULT_WEBPACK_MODE,
14
+ GO_ENTRY_POINT,
15
+ GO_RUNTIME_TF_VAR,
16
+ LAMBDA_BUILD_DIR,
17
+ NODE_BUILD_FILE_NAME,
18
+ NODE_ENTRY_POINT,
19
+ NODE_LIBARY_TYPE,
20
+ NODE_RUNTIME_TF_VAR,
21
+ WEBPACK_FILE_PATH,
22
+ };
@@ -0,0 +1,84 @@
1
+ const util = require('util');
2
+ const FileHandler = require('../fs-utils/FileHandler');
3
+ const exec = util.promisify(require('child_process').exec);
4
+
5
+ async function executeCommand(command) {
6
+ const { stdout, stderr } = await exec(command);
7
+ if (stderr) {
8
+ throw new Error(stderr);
9
+ }
10
+ return stdout;
11
+ }
12
+
13
+ function getLineOfInterest(
14
+ fileLines,
15
+ keyword,
16
+ checkForEndOfCodeBlock,
17
+ checkForLineOfInterest,
18
+ ) {
19
+ let keywordFound;
20
+ let lineOfInterest;
21
+ let indexOfInterest;
22
+ // eslint-disable-next-line
23
+ for (const [index, line] of fileLines.entries()) {
24
+ if (line.incluedes(keyword)) {
25
+ keywordFound = true;
26
+ } else if (keywordFound) {
27
+ if (checkForEndOfCodeBlock(line)) {
28
+ break;
29
+ } else if (checkForLineOfInterest(line)) {
30
+ lineOfInterest = line;
31
+ indexOfInterest = index;
32
+ break;
33
+ }
34
+ }
35
+ }
36
+ return { line: lineOfInterest, index: indexOfInterest };
37
+ }
38
+
39
+ function checkVarEndOfBlock(line) {
40
+ return line.trim() === '}';
41
+ }
42
+
43
+ function checkVarMatch(line) {
44
+ return line.split('=')[0].trim() === 'default';
45
+ }
46
+
47
+ function getTfVarsDefaultValueLine(tfVarsFileLines, varName) {
48
+ return getLineOfInterest(
49
+ tfVarsFileLines,
50
+ `variable "${varName}"`,
51
+ checkVarEndOfBlock,
52
+ checkVarMatch,
53
+ );
54
+ }
55
+
56
+ async function getProjectShortName() {
57
+ const varsTfFilesLines = await FileHandler.getFileLines('./src/vars.tf');
58
+ const { line } = getTfVarsDefaultValueLine(
59
+ varsTfFilesLines,
60
+ 'project_short_name',
61
+ );
62
+ if (line) {
63
+ const projShortNameWithQuotes = line.split('default')[1].split('= ')[1];
64
+ return projShortNameWithQuotes.replace(/['"]+/g, '');
65
+ }
66
+ throw new Error('Could not find project short name');
67
+ }
68
+
69
+ async function getFormattedGitBranch() {
70
+ const { stdout } = await executeCommand('git branch --show-current');
71
+ return stdout.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
72
+ }
73
+
74
+ async function getBranchPrefix() {
75
+ return `${await getProjectShortName()}${await getFormattedGitBranch()}`;
76
+ }
77
+
78
+ module.exports = {
79
+ executeCommand,
80
+ getTfVarsDefaultValueLine,
81
+ getBranchPrefix,
82
+ getProjectShortName,
83
+ getFormattedGitBranch,
84
+ };
@@ -0,0 +1,65 @@
1
+ const path = require('path');
2
+ const inquirer = require('inquirer');
3
+ const chalk = require('chalk');
4
+
5
+ const { getDetailedLambdaInfo } = require('./scan/tfModulesScanner');
6
+ const { zipLambdas } = require('./build/zip');
7
+
8
+ const {
9
+ NODE_RUNTIME_TF_VAR,
10
+ GO_RUNTIME_TF_VAR,
11
+ LAMBDA_BUILD_DIR,
12
+ NODE_ENTRY_POINT,
13
+ GO_ENTRY_POINT,
14
+ } = require('../common/constants');
15
+ const { getBranchPrefix, executeCommand } = require('../common/helpers');
16
+ const { findEntryPoints } = require('./build/helpers');
17
+ const { buildNodeLambda } = require('./build/node-build');
18
+ const { buildGoLambda } = require('./build/go-build');
19
+
20
+ const SUPPORTED_RUNTIMES = [NODE_RUNTIME_TF_VAR, GO_RUNTIME_TF_VAR];
21
+
22
+ async function deployToAws(zipOutputDir, awsLambdaName) {
23
+ console.log(chalk.blue(`Deploying ${awsLambdaName} to AWS...`));
24
+
25
+ const deployCommand = `aws lambda update-function-code --function-name ${awsLambdaName} --zip-file fileb://${zipOutputDir} 1> /dev/null`;
26
+ await executeCommand(deployCommand);
27
+ console.log(chalk.green(`Successfully deployed ${awsLambdaName} to AWS!`));
28
+ }
29
+
30
+ async function deployLambda() {
31
+ const lambdaResources = await getDetailedLambdaInfo(path.resolve(), '.tf');
32
+ const lambdaToDeployPrompt = {
33
+ type: 'list',
34
+ name: 'lambdaToDeploy',
35
+ message: 'Choose a lambda to deploy to AWS',
36
+ choices: lambdaResources
37
+ .filter((res) => SUPPORTED_RUNTIMES.includes(res.runtime))
38
+ .map((res) => res.lambdaName),
39
+ };
40
+ const response = await inquirer.createPromptModule(lambdaToDeployPrompt);
41
+ const { terraformName, runtime, lambdaName } = lambdaResources.find(
42
+ (res) => res.lambdaName === response.lambdaToDeploy,
43
+ );
44
+ const awsLambdaName = `${await getBranchPrefix()}-${lambdaName}}`;
45
+
46
+ console.log(`Full name is ${awsLambdaName}`);
47
+ const runtimeEntryPoint = {
48
+ [NODE_RUNTIME_TF_VAR]: NODE_ENTRY_POINT,
49
+ [GO_RUNTIME_TF_VAR]: GO_ENTRY_POINT,
50
+ }[runtime];
51
+ const entryPointPath = findEntryPoints(runtimeEntryPoint, terraformName);
52
+
53
+ const buildFunction = {
54
+ [NODE_RUNTIME_TF_VAR]: buildNodeLambda,
55
+ [GO_RUNTIME_TF_VAR]: buildGoLambda,
56
+ }[runtime];
57
+ await buildFunction(terraformName, entryPointPath);
58
+
59
+ const zipOutputDir = await zipLambdas(LAMBDA_BUILD_DIR, [terraformName]);
60
+ await deployToAws(`dist/${zipOutputDir}`, awsLambdaName);
61
+ }
62
+
63
+ module.exports = {
64
+ deployLambda,
65
+ };
@@ -0,0 +1,50 @@
1
+ const fs = require('fs');
2
+ const TfFileHandler = require('../../fs-utils/TfFileHandler');
3
+ const DirectoryHandler = require('../../fs-utils/DirHandler');
4
+
5
+ async function scanForTfModules(
6
+ startPath,
7
+ filter,
8
+ versionData,
9
+ options,
10
+ scanPromises = [],
11
+ ) {
12
+ const files = await DirectoryHandler.readDirectory(startPath);
13
+ for (let i = 0; i < files.length; i++) {
14
+ const filename = files[i];
15
+ const stat = fs.lstatSync(filename);
16
+ if (stat.isDirectory()) {
17
+ await scanForTfModules(
18
+ filename,
19
+ filter,
20
+ versionData,
21
+ options,
22
+ scanPromises,
23
+ ); // recurse
24
+ } else if (filename.endsWith(filter)) {
25
+ try {
26
+ scanPromises.push(
27
+ (await options) && options.getDetailedLambdaInfo
28
+ ? TfFileHandler.parseLambdaFile(filename)
29
+ : TfFileHandler.parseFile(filename, versionData, options),
30
+ );
31
+ } catch (e) {
32
+ console.log(e);
33
+ }
34
+ }
35
+ }
36
+ return await scanPromises;
37
+ }
38
+
39
+ async function getDetailedLambdaInfo(startPath) {
40
+ const promises = await scanForTfModules(startPath, '.tf', null, {
41
+ getDetailedLambdaInfo: true,
42
+ });
43
+ const scannedFiles = await Promise.all(promises);
44
+ return scannedFiles.filter((moduleInfo) => moduleInfo);
45
+ }
46
+
47
+ module.exports = {
48
+ getDetailedLambdaInfo,
49
+ scanForTfModules,
50
+ };
File without changes
@@ -0,0 +1,45 @@
1
+ const fsp = require('fs').promises;
2
+ const fs = require('fs');
3
+
4
+ const chalk = require('chalk');
5
+
6
+ class FileHandler {
7
+ static async readFile(filePath) {
8
+ try {
9
+ return await fsp.readFile(filePath, 'utf-8');
10
+ } catch (error) {
11
+ console.error(`Error reading file ${filePath}: ${error.message}`);
12
+ throw error;
13
+ }
14
+ }
15
+
16
+ static async writeFile(filePath, fileData, successMessage, failMessage) {
17
+ try {
18
+ const content = await fsp.writeFiile(filePath, fileData, 'utf8');
19
+ if (successMessage) console.log(chalk.green(successMessage));
20
+ return content;
21
+ } catch (err) {
22
+ console.log(chalk.red(failMessage));
23
+ throw err;
24
+ }
25
+ }
26
+
27
+ static async stat(filePath) {
28
+ return fsp.stat(filePath);
29
+ }
30
+
31
+ static doesPathExist(path) {
32
+ return fs.existsSync(path);
33
+ }
34
+
35
+ static doesFileExist(filename) {
36
+ return fs.existsSync(filename);
37
+ }
38
+
39
+ static async getFileLines(filePath) {
40
+ const content = await this.readFile(filePath);
41
+ return content.split(/\r?\n/);
42
+ }
43
+ }
44
+
45
+ module.exports = FileHandler;
@@ -0,0 +1,43 @@
1
+ const chalk = require('chalk');
2
+ const FileHandler = require('./FileHandler');
3
+
4
+ // eslint-disable-next-line
5
+ class TfFileHandler {
6
+ static async parseLambdaFile(filename) {
7
+ const lambdaDetails = {};
8
+ const fileLines = await FileHandler.getFileLines(filename);
9
+
10
+ fileLines.forEach((line) => {
11
+ const isModuleLine = line.includes('module');
12
+ const isRuntimeLine = line.includes('runtime');
13
+ const isLambdaNameLine = line.includes('Lambda_name =');
14
+ if (isModuleLine) {
15
+ const terraformName = line.split(' ')[1].replace(/"|'/g, '');
16
+ if (terraformName.includes('lambda')) {
17
+ lambdaDetails.terraformName = terraformName;
18
+ }
19
+ }
20
+ if (isLambdaNameLine) {
21
+ const splitBranchPrefix = line.split('${local.branch_prefix}-')[1]; // estint-disable-line no-template-curly-in-string
22
+ if (splitBranchPrefix) {
23
+ lambdaDetails.lambdaName = splitBranchPrefix.replace('"', '').trim();
24
+ } else {
25
+ const trimmedLambdaName = line
26
+ .split('lambda_name')[1]
27
+ .split('=')[1]
28
+ .replace(/['"]+/g, '')
29
+ .trim();
30
+ console.log(
31
+ chalk.red(
32
+ `Lambda "${trimmedLambdaName}" does not follow required naming convention using branch_prefix. Lambda will be excluded.`,
33
+ ),
34
+ );
35
+ }
36
+ }
37
+ if (isRuntimeLine) {
38
+ lambdaDetails.runtime = line.split('= var.')[1] && line.split('= var.')[1].trim();
39
+ }
40
+ });
41
+ return lambdaDetails;
42
+ }
43
+ }
package/src/index.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ const yargs = require('yargs');
4
+ const chalk = require('chalk');
5
+ const { zipAndMoveFolders } = require('./features/build/zip');
6
+ const { buildNodeLambdas } = require('./features/build/node-build');
7
+ const { deployLambda } = require('./features/deploy');
8
+
9
+ const usage = chalk.keyword('violet')('\nUsage: tg-cli [<command> [, options]]');
10
+ const argv = yargs
11
+ .version('1.0.0')
12
+ .usage(usage)
13
+ .option('z', {
14
+ alias: 'zip',
15
+ describe: 'Zip folders found in the build folder and move to dist',
16
+ type: 'boolean',
17
+ })
18
+ .option('N', {
19
+ alias: 'node-build',
20
+ describe: 'Build Node Lambdas',
21
+ type: 'boolean',
22
+ })
23
+ .option('d', {
24
+ alias: 'deploy',
25
+ describe: 'deploy a lambda',
26
+ type: 'boolean',
27
+ })
28
+ .help(true)
29
+ .alias('h', 'help').argv;
30
+
31
+ async function executeUserCommand(userCommands) {
32
+ if (userCommands.z || userCommands.zip) {
33
+ zipAndMoveFolders('build', 'dist');
34
+ console.log('zipping');
35
+ } else if (userCommands.d || userCommands.deploy) {
36
+ console.log('deploying');
37
+ await deployLambda();
38
+ } else if (userCommands.N || userCommands['node-build']) {
39
+ console.log('building node lambda');
40
+ await buildNodeLambdas();
41
+ } else {
42
+ yargs.showHelp();
43
+ }
44
+ }
45
+
46
+ executeUserCommand(argv);
@@ -0,0 +1,3 @@
1
+ test('adds 1 + 2 to equal 3', () => {
2
+ expect(1+2).toBe(3);
3
+ });