diff --git a/package.json b/package.json index c53d310..8d0e6b7 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@types/js-yaml": "4", "@types/node": "18.16.0", "@types/pg": "8", + "@types/unzipper": "^0", "@typescript-eslint/eslint-plugin": "7.6.0", "@typescript-eslint/parser": "7.6.0", "@vitest/coverage-v8": "2.1.5", @@ -95,6 +96,7 @@ "sequelize": "6.37.2", "simple-git": "3.24.0", "tedious": "18.1.0", + "unzipper": "0.12.3", "xpath": "0.0.34", "yaml": "2.6.0", "zod": "3.23.6" diff --git a/src/apt/helpers/generateDependencyTree.ts b/src/apt/helpers/generateDependencyTree.ts new file mode 100644 index 0000000..d11daa9 --- /dev/null +++ b/src/apt/helpers/generateDependencyTree.ts @@ -0,0 +1,133 @@ +import { spawn } from 'child_process'; +import { existsSync, rmSync } from 'fs'; +import { glob } from 'glob'; +import { homedir } from 'os'; +import { basename, join } from 'path'; +import { Open } from 'unzipper'; + +import { TAPTDependencyTreeOptions } from '../../types/DCAPT'; +import { downloadApp } from './downloadApp'; + +export const generateDependencyTree = async (options: TAPTDependencyTreeOptions) => { + + // Placeholder for temporary directory + const tmpDir = join(homedir(), '.dcdx', 'tmp'); + + // Download the file from MPAC + console.log('Downloading archive from the Atlassian Marketplace'); + const file = await downloadApp(options.appKey); + + try { + // Placeholder for archive directory + const archiveDir = join(tmpDir, `.${options.appKey}`); + + try { + // Extract the file to a temporary directory + console.log(`Extracting archive to a temporary location (${archiveDir})`); + const archive = await Open.file(file); + await archive.extract({ path: archiveDir }) + + // Check if we are dealing with an OBR file + if (existsSync(join(archiveDir, 'obr.xml'))) { + + console.log('The archive is an OSGi Bundle Repository (OBR)'); + console.log('Searching for the main artifact'); + + // Get the main JAR file (which is located in the root directory) + const [ relativePathToJar ] = await glob(`*.jar`, { cwd: archiveDir }); + + // Make sure we actually found the main jar + if (!relativePathToJar) { + console.log('Failed to locate the main JAR file in the archive'); + return; + } else { + console.log(`Found the main artifact (${relativePathToJar})`); + + // Get the full path to the JAR file + const jarFile = join(archiveDir, relativePathToJar); + + // Placeholder directroy in which we will be extracting the main JAR + const jarDir = join(archiveDir, basename(jarFile, '.jar')); + + // Extract the main JAR file into the placeholder directory + console.log(`Extracting the main artifact to a temporary location (${jarDir})`); + const archive = await Open.file(jarFile); + await archive.extract({ path: jarDir }); + + // Now try to find the POM file for the main JAR + console.log(`Searching for POM file in the main artifact`); + const [ relativePathToPOM ] = await glob(`META-INF/maven/**/pom.xml`, { cwd: jarDir }); + + // Make sure we have found the POM file + if (!relativePathToPOM) { + console.log('Failed to locate the POM file in the main artifact'); + return; + } else { + console.log(`Found the POM file`); + + // Get the full path to the POM file + const pomFile = join(jarDir, relativePathToPOM); + + // Ask Maven to generate the depdency tree graph! + console.log('Asking Apache Maven to generate the dependency graph'); + await new Promise((resolve, reject) => { + const maven = spawn( + 'mvn', + [ + 'dependency:tree', + '-f', pomFile, + '-DoutputType=dot', + `-DoutputFile=${options.outputFile}`, + ], + { stdio: 'inherit' } + ); + maven.on('exit', (code) => (code === 0) ? resolve() : reject(new Error(`Apache Maven exited with code ${code}`))); + }); + + console.log('Finished generating the dependency graph'); + } + } + + // This is actually already the main JAR file + } else { + + // Now try to find the POM file for the main JAR + console.log(`Searching for POM file in the artifact`); + const [ relativePathToPOM ] = await glob(`META-INF/maven/**/pom.xml`, { cwd: archiveDir }); + + // Make sure we have found the POM file + if (!relativePathToPOM) { + console.log('Failed to locate the POM file in the artifact'); + return; + } else { + console.log(`Found the POM file`); + + // Get the full path to the POM file + const pomFile = join(archiveDir, relativePathToPOM); + + // Ask Maven to generate the depdency tree graph! + console.log('Asking Apache Maven to generate the dependency graph'); + await new Promise((resolve, reject) => { + const maven = spawn( + 'mvn', + [ + 'dependency:tree', + '-f', pomFile, + '-DoutputType=dot', + `-DoutputFile=${options.outputFile}`, + ], + { stdio: 'inherit' } + ); + maven.on('exit', (code) => (code === 0) ? resolve() : reject(new Error(`Apache Maven exited with code ${code}`))); + }); + + console.log('Finished generating the dependency graph'); + } + } + } finally { + rmSync(archiveDir, { force: true, recursive: true }); + } + } finally { + rmSync(file, { force: true }); + } +} \ No newline at end of file diff --git a/src/commands/apt.ts b/src/commands/apt.ts index e6fda98..b97c44a 100644 --- a/src/commands/apt.ts +++ b/src/commands/apt.ts @@ -2,8 +2,10 @@ import { Command as Commander, Option } from 'commander'; import { gracefulExit } from 'exit-hook'; +import { join } from 'path'; import { cwd } from 'process'; +import { generateDependencyTree } from '../apt/helpers/generateDependencyTree'; import { generatePerformanceReport } from '../apt/helpers/generatePerformanceReport'; import { generateScalabilityReport } from '../apt/helpers/generateScalabilityReport'; import { getHostLicense } from '../apt/helpers/getHostLicense'; @@ -21,7 +23,7 @@ import { Performance } from '../apt/performance'; import { Scalability } from '../apt/scalability'; import { ActionHandler } from '../helpers/ActionHandler'; import { SupportedApplications } from '../types/Application'; -import { PerformanceTestTypes, ReportTypes, TAPTArgs, TAPTPerformanceReportArgs, TAPTPerformanceTestArgs, TAPTProvisionArgs, TAPTRestartArgs, TAPTScalabilityReportArgs, TAPTScalabilityTestArgs, TAPTTeardownArgs } from '../types/DCAPT'; +import { PerformanceTestTypes, ReportTypes, TAPTArgs, TAPTDependencyTreeArgs, TAPTPerformanceReportArgs, TAPTPerformanceTestArgs, TAPTProvisionArgs, TAPTRestartArgs, TAPTScalabilityReportArgs, TAPTScalabilityTestArgs, TAPTTeardownArgs } from '../types/DCAPT'; const program = new Commander(); @@ -296,6 +298,18 @@ const TeardownCommand = () => ({ } }) +const DependencyTreeCommand = () => ({ + action: async (options: TAPTDependencyTreeArgs) => { + await generateDependencyTree({ + appKey: options.appKey, + outputFile: options.outputFile || join(cwd(), 'maven_dependency_tree.gv') + }); + }, + errorHandler: async () => { + + } +}) + program .name('dcdx apt') .showHelpAfterError(true); @@ -466,6 +480,14 @@ program .addOption(new Option('-y, --force', 'Use default values for input questions when available').default(false)) .action(options => ActionHandler(program, TeardownCommand(), options)); +program + .command('dependencies') + .description('Generate the Data Center App Performance Testing dependency tree') + .addOption(new Option('--appKey ', 'The key of the app (for automated installation)')) + .addOption(new Option('-O, --outputFile ', 'Specify the output file where to store the generated dependency tree (defaults to `./maven_dependency_tree.gv`)')) + .action(options => ActionHandler(program, DependencyTreeCommand(), options)); + + program.parseAsync(process.argv).catch(() => gracefulExit(1)); process.on('SIGINT', () => { diff --git a/src/types/DCAPT.ts b/src/types/DCAPT.ts index 232d5ce..7344c8e 100644 --- a/src/types/DCAPT.ts +++ b/src/types/DCAPT.ts @@ -201,6 +201,18 @@ export const APTTeardownOptions = z.object({ export const APTRestartArgs = APTTeardownArgs.extend({}); export const APTRestartOptions = APTTeardownOptions.extend({}); +export const APTDependencyTreeArgs = z.object({ + appKey: z.string(), + outputFile: z.string() +}).partial({ + outputFile: true +}); + +export const APTDependencyTreeOptions = z.object({ + appKey: z.string(), + outputFile: z.string() +}); + export const APTArgs = z.intersection( APTProvisionOptions, APTPerformanceTestArgs, @@ -235,3 +247,5 @@ export type TAPTTeardownOptions = z.infer; export type TTestResults = z.infer; export type TReportTypes = z.infer; +export type TAPTDependencyTreeArgs = z.infer; +export type TAPTDependencyTreeOptions = z.infer; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 69bbc22..d10a45e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1946,6 +1946,15 @@ __metadata: languageName: node linkType: hard +"@types/unzipper@npm:^0": + version: 0.10.10 + resolution: "@types/unzipper@npm:0.10.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/10e9da33791be1087adb25adc2fe4d5ab267dae51fbcf7b1f10d0aca3130a13ef5fed994d7be45af8c465ff3946bc360a53eff6e5aab4eb9ac9489477535342f + languageName: node + linkType: hard + "@types/validator@npm:^13.7.17": version: 13.11.9 resolution: "@types/validator@npm:13.11.9" @@ -2637,6 +2646,13 @@ __metadata: languageName: node linkType: hard +"bluebird@npm:~3.7.2": + version: 3.7.2 + resolution: "bluebird@npm:3.7.2" + checksum: 10c0/680de03adc54ff925eaa6c7bb9a47a0690e8b5de60f4792604aae8ed618c65e6b63a7893b57ca924beaf53eee69c5af4f8314148c08124c550fe1df1add897d2 + languageName: node + linkType: hard + "bottleneck@npm:^2.15.3": version: 2.19.5 resolution: "bottleneck@npm:2.19.5" @@ -3348,6 +3364,7 @@ __metadata: "@types/js-yaml": "npm:4" "@types/node": "npm:18.16.0" "@types/pg": "npm:8" + "@types/unzipper": "npm:^0" "@typescript-eslint/eslint-plugin": "npm:7.6.0" "@typescript-eslint/parser": "npm:7.6.0" "@vitest/coverage-v8": "npm:2.1.5" @@ -3383,6 +3400,7 @@ __metadata: tedious: "npm:18.1.0" typescript: "npm:5.4.4" typescript-eslint: "npm:7.6.0" + unzipper: "npm:0.12.3" vitest: "npm:2.1.5" vitest-mock-process: "npm:1.0.4" xpath: "npm:0.0.34" @@ -3614,7 +3632,7 @@ __metadata: languageName: node linkType: hard -"duplexer2@npm:~0.1.0": +"duplexer2@npm:~0.1.0, duplexer2@npm:~0.1.4": version: 0.1.4 resolution: "duplexer2@npm:0.1.4" dependencies: @@ -4836,7 +4854,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -6773,6 +6791,13 @@ __metadata: languageName: node linkType: hard +"node-int64@npm:^0.4.0": + version: 0.4.0 + resolution: "node-int64@npm:0.4.0" + checksum: 10c0/a6a4d8369e2f2720e9c645255ffde909c0fbd41c92ea92a5607fc17055955daac99c1ff589d421eee12a0d24e99f7bfc2aabfeb1a4c14742f6c099a51863f31a + languageName: node + linkType: hard + "nodemon@npm:3.1.0": version: 3.1.0 resolution: "nodemon@npm:3.1.0" @@ -9804,6 +9829,19 @@ __metadata: languageName: node linkType: hard +"unzipper@npm:0.12.3": + version: 0.12.3 + resolution: "unzipper@npm:0.12.3" + dependencies: + bluebird: "npm:~3.7.2" + duplexer2: "npm:~0.1.4" + fs-extra: "npm:^11.2.0" + graceful-fs: "npm:^4.2.2" + node-int64: "npm:^0.4.0" + checksum: 10c0/4cae2ad23bfd47011d5f8a6d61fb1dc0e4b5008bc3896e6f3d5ab946a64e9482714992a988128bce541440aa646e16e5e5c9bf35e49097edbaf833e7f814d36d + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1"