diff --git a/agent/.gitignore b/agent/.gitignore index 4963527b..332d3a58 100644 --- a/agent/.gitignore +++ b/agent/.gitignore @@ -9,6 +9,7 @@ /coverage setaws.sh /.nyc_output/ +/test/pewpew.* # production /build diff --git a/agent/README.md b/agent/README.md index 38a6de4c..da985945 100644 --- a/agent/README.md +++ b/agent/README.md @@ -15,6 +15,11 @@ $ npm i && npm run build ``` +## Mac and Windows Testing +The unit tests, integration, and acceptance tests are designed to run on Linux. As such, the pewpew executable files required for running on Linux are checked into the tree in the test server so that the files are available for our Github Actions (`test/pewpew`). + +To override these tests for mac or windows, the pewpew exectuable must be named `pewpew.exe` for Windows and `pewpew.mac` for Mac. These files should then be dropped in the `test/` folder. + ## Test ```bash diff --git a/agent/createtest/pewpewtest.spec.ts b/agent/createtest/pewpewtest.spec.ts index 8f8f93d6..ebb3fc8d 100644 --- a/agent/createtest/pewpewtest.spec.ts +++ b/agent/createtest/pewpewtest.spec.ts @@ -1,6 +1,7 @@ import { LogLevel, MessageType, + PEWPEW_VERSION_LATEST, PpaasS3File, PpaasS3Message, PpaasTestId, @@ -83,7 +84,7 @@ describe("PewPewTest Create Test", () => { s3Folder, yamlFile: createTestFilename, testRunTimeMn: 2, - version: "latest", + version: PEWPEW_VERSION_LATEST, envVariables: { SERVICE_URL_AGENT: "127.0.0.1:8080" }, restartOnFailure: false, additionalFiles: [], @@ -309,7 +310,7 @@ describe("PewPewTest Create Test", () => { testId: ppaasTestId.testId, s3Folder, yamlFile: createTestFilename, - version: "latest", + version: PEWPEW_VERSION_LATEST, envVariables: { SERVICE_URL_AGENT: "127.0.0.1:8080", RUST_LOG: "info", diff --git a/agent/src/pewpewtest.ts b/agent/src/pewpewtest.ts index 94a96957..aa481a08 100644 --- a/agent/src/pewpewtest.ts +++ b/agent/src/pewpewtest.ts @@ -38,7 +38,7 @@ const DEFAULT_PEWPEW_PARAMS = [ "-w" ]; -const PEWPEW_PATH: string = process.env.PEWPEW_PATH || "pewpew"; +const PEWPEW_PATH: string = process.env.PEWPEW_PATH || util.PEWPEW_BINARY_EXECUTABLE; const DOWNLOAD_PEWPEW: boolean = (process.env.DOWNLOAD_PEWPEW || "false") === "true"; const LOCAL_FILE_LOCATION: string = process.env.LOCAL_FILE_LOCATION || process.env.TEMP || "/tmp"; const RESULTS_FILE_MAX_WAIT: number = parseInt(process.env.RESULTS_FILE_MAX_WAIT || "0", 10) || 5000; // S3_UPLOAD_INTERVAL + this Could take up to a minute @@ -70,7 +70,7 @@ export async function findYamlCreatedFiles (localPath: string, yamlFile: string, additionalFiles = additionalFiles || []; // Find files that aren't the yamlFile or additionalFile, a results file, or the pewpew executable const YamlCreatedFiles = files.filter((file) => file !== yamlFile && !additionalFiles!.includes(file) - && !(file.startsWith("stats-") && file.endsWith(".json")) && file !== "pewpew" && file !== "pewpew.exe"); + && !(file.startsWith("stats-") && file.endsWith(".json")) && !util.PEWPEW_BINARY_EXECUTABLE_NAMES.includes(file)); log(`YamlCreatedFiles: ${YamlCreatedFiles}}`, LogLevel.DEBUG); if (YamlCreatedFiles.length > 0) { return YamlCreatedFiles; // Don't return the joined path @@ -86,9 +86,9 @@ export async function findYamlCreatedFiles (localPath: string, yamlFile: string, // Export for testing export function versionGreaterThan (currentVersion: string, compareVersion: string): boolean { // If the current version is latest then we're always greater than or equal to - if (currentVersion === "latest") { return true; } + if (currentVersion === util.PEWPEW_VERSION_LATEST) { return true; } // If the compareVersion is latest, then only currrentVersion=latest is greater - if (compareVersion === "latest") { return false; } + if (compareVersion === util.PEWPEW_VERSION_LATEST) { return false; } return semver.gt(currentVersion, compareVersion); } @@ -373,28 +373,20 @@ export class PewPewTest { // Download the pewpew executable if needed if (DOWNLOAD_PEWPEW) { // version check in the test message - const version = this.testMessage.version || "latest"; + const version = this.testMessage.version || util.PEWPEW_VERSION_LATEST; const localDirectory = this.localPath; - const s3Folder = "pewpew/" + version; - this.log(`os.platform() = ${platform()}`, LogLevel.DEBUG, { version, s3Folder }); - if (platform() === "win32") { - const pewpewS3File: PpaasS3File = new PpaasS3File({ - filename: "pewpew.exe", - s3Folder, - localDirectory - }); - pewpewPath = await pewpewS3File.download(true); - this.log(`getFile(pewpew.exe) result = ${pewpewPath}`, LogLevel.DEBUG); - } else { - // If the version isn't there, this will throw (since we can't find it) - const pewpewS3File: PpaasS3File = new PpaasS3File({ - filename: "pewpew", - s3Folder, - localDirectory - }); - pewpewPath = await pewpewS3File.download(true); - this.log(`getFile(pewpew) result = ${pewpewPath}`, LogLevel.DEBUG); - // We need to make it executable + const s3Folder = `${util.PEWPEW_BINARY_FOLDER}/${version}`; + this.log(`os.platform() = ${platform()}`, LogLevel.DEBUG, { version, s3Folder, filename: util.PEWPEW_BINARY_EXECUTABLE }); + const pewpewS3File: PpaasS3File = new PpaasS3File({ + filename: util.PEWPEW_BINARY_EXECUTABLE, + s3Folder, + localDirectory + }); + pewpewPath = await pewpewS3File.download(true); + this.log(`getFile("${util.PEWPEW_BINARY_EXECUTABLE}") result = ${pewpewPath}`, LogLevel.DEBUG); + // If the version isn't there, this will throw (since we can't find it) + if (platform() !== "win32") { + // We need to make it executable for non-windows await fs.chmod(pewpewPath, 0o775); } // Always call this even if it isn't logged to make sure the file downloaded diff --git a/agent/src/tests.ts b/agent/src/tests.ts index 2d63a89a..14ebdc7b 100644 --- a/agent/src/tests.ts +++ b/agent/src/tests.ts @@ -1,5 +1,7 @@ import { LogLevel, + PEWPEW_BINARY_FOLDER, + PEWPEW_VERSION_LATEST, PpaasTestId, PpaasTestMessage, TestMessage, @@ -19,8 +21,8 @@ logger.config.LogFileName = "ppaas-agent"; const UNIT_TEST_FOLDER = process.env.UNIT_TEST_FOLDER || "test"; const yamlFile = "basicwithenv.yaml"; -const version = "latest"; -const PEWPEW_PATH = process.env.PEWPEW_PATH || pathJoin(UNIT_TEST_FOLDER, "pewpew"); +const version = PEWPEW_VERSION_LATEST; +const PEWPEW_PATH = process.env.PEWPEW_PATH || pathJoin(UNIT_TEST_FOLDER, util.PEWPEW_BINARY_EXECUTABLE); const buildTestContents = ` vars: rampTime: 10s @@ -72,7 +74,7 @@ export async function buildTest ({ }), s3.uploadFile({ filepath: PEWPEW_PATH, - s3Folder: `pewpew/${version}`, + s3Folder: `${PEWPEW_BINARY_FOLDER}/${version}`, publicRead: false, contentType: "application/octet-stream" }) diff --git a/agent/test/pewpew b/agent/test/pewpew index cf50ea86..fb8be72b 100755 Binary files a/agent/test/pewpew and b/agent/test/pewpew differ diff --git a/agent/test/pewpewtest.spec.ts b/agent/test/pewpewtest.spec.ts index 2b1ab17f..f411f40e 100644 --- a/agent/test/pewpewtest.spec.ts +++ b/agent/test/pewpewtest.spec.ts @@ -1,11 +1,13 @@ import { LogLevel, + PEWPEW_VERSION_LATEST, PpaasTestId, PpaasTestStatus, TestStatus, TestStatusMessage, log, - logger + logger, + util } from "@fs/ppaas-common"; import { copyTestStatus, @@ -26,17 +28,9 @@ describe("PewPewTest", () => { let localFiles: string[]; before (async () => { - localFiles = await readdir(UNIT_TEST_FILEDIR); + localFiles = (await readdir(UNIT_TEST_FILEDIR)) + .filter((filename) => filename !== UNIT_TEST_FILENAME && !util.PEWPEW_BINARY_EXECUTABLE_NAMES.includes(filename)); log(`localFiles = ${JSON.stringify(localFiles)}`, LogLevel.DEBUG); - const unitTestFound = localFiles.indexOf(UNIT_TEST_FILENAME); - if (unitTestFound >= 0) { - localFiles.splice(unitTestFound, 1); - } - const pewpewFound = localFiles.indexOf("pewpew"); - if (pewpewFound >= 0) { - localFiles.splice(pewpewFound, 1); - } - log(`localFiles removed = ${JSON.stringify(localFiles)}`, LogLevel.DEBUG); }); it("Find Yaml should find nothing when everything passed", (done: Mocha.Done) => { @@ -85,17 +79,17 @@ describe("PewPewTest", () => { describe("versionGreaterThan", () => { it("latest is always greater", (done: Mocha.Done) => { - expect(versionGreaterThan("latest", "")).to.equal(true); + expect(versionGreaterThan(PEWPEW_VERSION_LATEST, "")).to.equal(true); done(); }); it("latest is always greater than latest", (done: Mocha.Done) => { - expect(versionGreaterThan("latest", "latest")).to.equal(true); + expect(versionGreaterThan(PEWPEW_VERSION_LATEST, PEWPEW_VERSION_LATEST)).to.equal(true); done(); }); it("greater than latest is false", (done: Mocha.Done) => { - expect(versionGreaterThan("0.5.5", "latest")).to.equal(false); + expect(versionGreaterThan("0.5.5", PEWPEW_VERSION_LATEST)).to.equal(false); done(); }); diff --git a/common/integration/ppaasteststatus.spec.ts b/common/integration/ppaasteststatus.spec.ts index 7450ad08..5256c4dc 100644 --- a/common/integration/ppaasteststatus.spec.ts +++ b/common/integration/ppaasteststatus.spec.ts @@ -1,5 +1,6 @@ import { LogLevel, + PEWPEW_VERSION_LATEST, PpaasTestId, PpaasTestStatus, TestStatus, @@ -33,7 +34,7 @@ describe("PpaasTestStatus", () => { resultsFilename: [ppaasTestId.testId + ".json"], status: TestStatus.Running, errors: ["Test Error"], - version: "latest", + version: PEWPEW_VERSION_LATEST, queueName: "unittest", userId: "unittestuser" }; diff --git a/common/src/index.ts b/common/src/index.ts index e4f687e3..e83157d2 100644 --- a/common/src/index.ts +++ b/common/src/index.ts @@ -3,6 +3,16 @@ import * as logger from "./util/log"; import * as s3 from "./util/s3"; import * as sqs from "./util/sqs"; import * as util from "./util/util"; +import { + APPLICATION_NAME, + PEWPEW_BINARY_EXECUTABLE, + PEWPEW_BINARY_EXECUTABLE_NAMES, + PEWPEW_BINARY_FOLDER, + PEWPEW_VERSION_LATEST, + SYSTEM_NAME, + poll, + sleep + } from "./util/util"; import { LogLevel, log } from "./util/log"; import { MakeTestIdOptions, PpaasTestId } from "./ppaastestid"; import { PpaasS3File, PpaasS3FileCopyOptions, PpaasS3FileOptions } from "./s3file"; @@ -29,6 +39,14 @@ export { util, log, LogLevel, + APPLICATION_NAME, + PEWPEW_BINARY_EXECUTABLE, + PEWPEW_BINARY_EXECUTABLE_NAMES, + PEWPEW_BINARY_FOLDER, + PEWPEW_VERSION_LATEST, + SYSTEM_NAME, + poll, + sleep, PpaasCommunicationsMessage, PpaasS3Message, PpaasTestId, diff --git a/common/src/ppaastestid.ts b/common/src/ppaastestid.ts index f35dc6a1..92ce93f2 100644 --- a/common/src/ppaastestid.ts +++ b/common/src/ppaastestid.ts @@ -68,7 +68,7 @@ export class PpaasTestId { const yamlname: string = (path.basename(yamlFile, path.extname(yamlFile)).toLocaleLowerCase() + (profile || "").toLocaleLowerCase()) .replace(/[^a-z0-9]/g, ""); - if (yamlname === "pewpew") { + if (yamlname.startsWith("pewpew")) { throw new Error("Yaml File cannot be named PewPew"); } const newTestId: PpaasTestId = new PpaasTestId(yamlname, dateString || this.getDateString()); diff --git a/common/src/s3file.ts b/common/src/s3file.ts index 62874db3..9d984b5f 100644 --- a/common/src/s3file.ts +++ b/common/src/s3file.ts @@ -14,12 +14,12 @@ import { uploadFile } from "./util/s3"; import { LogLevel, log } from "./util/log"; +import { PEWPEW_BINARY_EXECUTABLE_NAMES, sleep } from "./util/util"; import { PutObjectCommandInput, _Object as S3Object } from "@aws-sdk/client-s3"; import { access, stat } from "fs/promises"; import { S3File } from "../types"; import { Stats } from "fs"; import { URL } from "url"; -import { sleep } from "./util/util"; const RESULTS_UPLOAD_RETRY: number = parseInt(process.env.RESULTS_UPLOAD_RETRY || "0", 10) || 5; @@ -105,7 +105,7 @@ export class PpaasS3File implements S3File { this.contentType = "text/plain"; break; } - if (filename === "pewpew" || filename === "pewpew.exe") { + if (PEWPEW_BINARY_EXECUTABLE_NAMES.includes(filename)) { this.contentType = "application/octet-stream"; } log(`contentType: ${this.contentType}`, LogLevel.DEBUG); diff --git a/common/src/util/util.ts b/common/src/util/util.ts index e36e8b83..dd3d634e 100644 --- a/common/src/util/util.ts +++ b/common/src/util/util.ts @@ -1,6 +1,4 @@ -import * as _fs from "fs/promises"; import * as os from "os"; -import { LogLevel, log } from "./log"; export const APPLICATION_NAME: string = process.env.APPLICATION_NAME || "pewpewagent"; export const CONTROLLER_APPLICATION_NAME: string = process.env.CONTROLLER_APPLICATION_NAME || "pewpewcontroller"; @@ -10,6 +8,24 @@ export const SYSTEM_NAME: string = process.env.SYSTEM_NAME || "unittests"; export const CONTROLLER_ENV = process.env.CONTROLLER_ENV; export const AGENT_ENV = process.env.AGENT_ENV; +export const PEWPEW_BINARY_FOLDER: string = "pewpew"; +export const PEWPEW_VERSION_LATEST: string = "latest"; +export const PEWPEW_BINARY_EXECUTABLE_LINUX = "pewpew"; +export const PEWPEW_BINARY_EXECUTABLE_WINDOWS = "pewpew.exe"; +export const PEWPEW_BINARY_EXECUTABLE_MAC = "pewpew.mac"; +export const PEWPEW_BINARY_EXECUTABLE = process.env.PEWPEW_BINARY_EXECUTABLE + || os.platform() === "win32" + ? PEWPEW_BINARY_EXECUTABLE_WINDOWS + : os.platform() === "darwin" + ? PEWPEW_BINARY_EXECUTABLE_MAC + : PEWPEW_BINARY_EXECUTABLE_LINUX; +export const PEWPEW_BINARY_EXECUTABLE_NAMES = [ + PEWPEW_BINARY_EXECUTABLE_LINUX, + PEWPEW_BINARY_EXECUTABLE_WINDOWS, + PEWPEW_BINARY_EXECUTABLE_MAC +]; + + /** This applications PREFIX. No overrides */ export const PREFIX_DEFAULT: string = `${APPLICATION_NAME}-${SYSTEM_NAME}`.toUpperCase().replace(/-/g, "_"); let PREFIX_CONTROLLER_ENV: string | undefined; @@ -34,33 +50,12 @@ export const getPrefix = (controllerEnv?: boolean | string): string => { return PREFIX_DEFAULT; }; -/** @deprecated Use `fs/promises` */ -export const fs = { - /** @deprecated Use `fs/promises` */ - access: _fs.access, - /** @deprecated Use `fs/promises` */ - chmod: _fs.chmod, - /** @deprecated Use `fs/promises` */ - mkdir: _fs.mkdir, - /** @deprecated Use `fs/promises` */ - readdir: _fs.readdir, - /** @deprecated Use `fs/promises` */ - readFile: _fs.readFile, - /** @deprecated Use `fs/promises` */ - rename: _fs.rename, - /** @deprecated Use `fs/promises` */ - unlink: _fs.unlink, - /** @deprecated Use `rimraf` or `fs/promises` */ - rmdir: _fs.rmdir, - /** @deprecated Use `fs/promises` */ - stat: _fs.stat -}; - export async function sleep (ms: number): Promise { try { await new Promise((resolve) => setTimeout(resolve, ms)); } catch (error: unknown) { - log("sleep Error", LogLevel.ERROR, error); // swallow it + // eslint-disable-next-line no-console + console.error("sleep Error", error); // swallow it } } diff --git a/common/test/ppaastestmessage.spec.ts b/common/test/ppaastestmessage.spec.ts index 9d1ac640..f2447e97 100644 --- a/common/test/ppaastestmessage.spec.ts +++ b/common/test/ppaastestmessage.spec.ts @@ -1,4 +1,11 @@ -import { AgentQueueDescription, LogLevel, PpaasTestMessage, TestMessage, log } from "../src/index"; +import { + AgentQueueDescription, + LogLevel, + PEWPEW_VERSION_LATEST, + PpaasTestMessage, + TestMessage, + log +} from "../src/index"; import { UNIT_TEST_FILENAME, UNIT_TEST_KEY_PREFIX } from "../test/s3.spec"; import { mockReceiveMessageAttributes, @@ -36,7 +43,7 @@ describe("PpaasTestMessage", () => { }, restartOnFailure: true, bucketSizeMs: 60000, - version: "latest", + version: PEWPEW_VERSION_LATEST, additionalFiles: ["file1", "file2"], userId: "bruno@madrigal.family", bypassParser: false @@ -45,7 +52,16 @@ describe("PpaasTestMessage", () => { before(() => { mockSqs(); log("QUEUE_URL_TEST=" + [...QUEUE_URL_TEST], LogLevel.DEBUG); - ppaasUnitTestMessage = new PPaasUnitTestMessage({ testId, s3Folder, yamlFile, testRunTimeMn, envVariables: {}, restartOnFailure: true, bucketSizeMs: 60000, version: "latest" }); + ppaasUnitTestMessage = new PPaasUnitTestMessage({ + testId, + s3Folder, + yamlFile, + testRunTimeMn, + envVariables: {}, + restartOnFailure: true, + bucketSizeMs: 60000, + version: PEWPEW_VERSION_LATEST + }); }); after(() => { @@ -102,7 +118,16 @@ describe("PpaasTestMessage", () => { it("extendMessageVisibility should succeed", (done: Mocha.Done) => { if (ppaasTestMessage === undefined) { - ppaasTestMessage = new PpaasTestMessage({ testId, s3Folder, yamlFile, testRunTimeMn, envVariables: {}, restartOnFailure: true, bucketSizeMs: 60000, version: "latest" }); + ppaasTestMessage = new PpaasTestMessage({ + testId, + s3Folder, + yamlFile, + testRunTimeMn, + envVariables: {}, + restartOnFailure: true, + bucketSizeMs: 60000, + version: PEWPEW_VERSION_LATEST + }); } if (ppaasTestMessage.receiptHandle === undefined) { ppaasTestMessage.receiptHandle = "unit-test-receipt-handle"; @@ -112,7 +137,16 @@ describe("PpaasTestMessage", () => { it("deleteMessageFromQueue should succeed", (done: Mocha.Done) => { if (ppaasTestMessage === undefined) { - ppaasTestMessage = new PpaasTestMessage({ testId, s3Folder, yamlFile, testRunTimeMn, envVariables: {}, restartOnFailure: true, bucketSizeMs: 60000, version: "latest" }); + ppaasTestMessage = new PpaasTestMessage({ + testId, + s3Folder, + yamlFile, + testRunTimeMn, + envVariables: {}, + restartOnFailure: true, + bucketSizeMs: 60000, + version: PEWPEW_VERSION_LATEST + }); } if (ppaasTestMessage.receiptHandle === undefined) { ppaasTestMessage.receiptHandle = "unit-test-receipt-handle"; diff --git a/common/test/ppaasteststatus.spec.ts b/common/test/ppaasteststatus.spec.ts index de0e8636..04a33a64 100644 --- a/common/test/ppaasteststatus.spec.ts +++ b/common/test/ppaasteststatus.spec.ts @@ -1,5 +1,6 @@ import { LogLevel, + PEWPEW_VERSION_LATEST, PpaasTestId, PpaasTestStatus, TestStatus, @@ -46,7 +47,7 @@ describe("PpaasTestStatus", () => { resultsFilename: [ppaasTestId.testId + ".json"], status: TestStatus.Running, errors: ["Test Error"], - version: "latest", + version: PEWPEW_VERSION_LATEST, queueName: "unittest", userId: "unittestuser" }; diff --git a/common/test/s3file.spec.ts b/common/test/s3file.spec.ts index c63ca4a5..c4b4861a 100644 --- a/common/test/s3file.spec.ts +++ b/common/test/s3file.spec.ts @@ -1,5 +1,6 @@ import { LogLevel, + PEWPEW_BINARY_EXECUTABLE_NAMES, PpaasS3File, PpaasTestId, S3File, @@ -148,13 +149,20 @@ describe("PpaasS3File", () => { }); it("pewpew should be octet-stream", (done: Mocha.Done) => { - const ppaasS3File: PpaasS3File = new PpaasS3File({ - filename: "pewpew", - s3Folder: unitTestKeyPrefix, - localDirectory: UNIT_TEST_LOCAL_FILE_LOCATION - }); - expect(ppaasS3File.contentType).to.equal("application/octet-stream"); - done(); + try { + for (const pewpewExecutable of PEWPEW_BINARY_EXECUTABLE_NAMES) { + const ppaasS3File: PpaasS3File = new PpaasS3File({ + filename: pewpewExecutable, + s3Folder: unitTestKeyPrefix, + localDirectory: UNIT_TEST_LOCAL_FILE_LOCATION + }); + expect(ppaasS3File.contentType).to.equal("application/octet-stream"); + } + done(); + } catch (error) { + log("pewpew should be octet-stream error", LogLevel.ERROR, error); + done(error); + } }); it("should be set public-read", (done: Mocha.Done) => { diff --git a/controller/.gitignore b/controller/.gitignore index 6d7e1aa1..1b1e6558 100644 --- a/controller/.gitignore +++ b/controller/.gitignore @@ -10,7 +10,7 @@ /storybook-static system-test-encrypt-key* /.nyc_output -/test/pewpew +/test/pewpew* /test/password.txt /pewpew-okta-creds-db /pewpew-okta-creds-db-np diff --git a/controller/README.md b/controller/README.md index b4bdead7..bc64af70 100644 --- a/controller/README.md +++ b/controller/README.md @@ -15,6 +15,11 @@ For your full deployment you should have environment variables injected into Clo $ npm i && npm run build ``` +## Mac and Windows Testing +The unit tests, integration, and acceptance tests are designed to run on Linux. As such, the pewpew executable files required for running on Linux are checked into the tree in the test server so that the files are available for our Github Actions (`test/pewpew.zip`). + +To override these tests for mac or windows, the pewpew exectuable must be named `pewpew.exe` for Windows and `pewpew.mac` for Mac. These files should then be zipped up as `pewpew.exe.zip` or `pewpew.mac.zip` correspondingly. Then either override the `PEWPEW_ZIP_FILEPATH` environment variable to point to the full path to your zip file, or drop the zipped file in the `test/` folder. + ## Test ```bash # You must set your aws credentials to start or run tests diff --git a/controller/acceptance/pewpew.spec.ts b/controller/acceptance/pewpew.spec.ts index 5b6c9549..04029f4b 100644 --- a/controller/acceptance/pewpew.spec.ts +++ b/controller/acceptance/pewpew.spec.ts @@ -3,7 +3,11 @@ import { FormDataPewPew, TestManagerError } from "../types"; -import { LogLevel, log } from "@fs/ppaas-common"; +import { + LogLevel, + PEWPEW_BINARY_EXECUTABLE, + log +} from "@fs/ppaas-common"; import _axios, { AxiosRequestConfig, AxiosResponse as Response } from "axios"; import FormData from "form-data"; import { createReadStream } from "fs"; @@ -32,7 +36,7 @@ async function fetch ( // Re-create these here so we don't have to run yamlparser.spec by importing it const UNIT_TEST_FOLDER = process.env.UNIT_TEST_FOLDER || "test"; -const PEWPEW_FILEPATH = path.join(UNIT_TEST_FOLDER, "pewpew.zip"); +const PEWPEW_ZIP_FILEPATH = process.env.PEWPEW_ZIP_FILEPATH || path.join(UNIT_TEST_FOLDER, PEWPEW_BINARY_EXECUTABLE + ".zip"); // Beanstalk __URL const integrationUrl = "http://" + (process.env.BUILD_APP_URL || `localhost:${process.env.PORT || "8081"}`); @@ -94,10 +98,10 @@ describe("PewPew API Integration", () => { describe("POST /pewpew", () => { it("POST /pewpew should respond 200 OK", (done: Mocha.Done) => { - const filename: string = path.basename(PEWPEW_FILEPATH); + const filename: string = path.basename(PEWPEW_ZIP_FILEPATH); const formData: FormDataPewPew = { additionalFiles: { - value: createReadStream(PEWPEW_FILEPATH), + value: createReadStream(PEWPEW_ZIP_FILEPATH), options: { filename } } }; @@ -142,10 +146,10 @@ describe("PewPew API Integration", () => { }); it("POST /pewpew as latest should respond 200 OK", (done: Mocha.Done) => { - const filename: string = path.basename(PEWPEW_FILEPATH); + const filename: string = path.basename(PEWPEW_ZIP_FILEPATH); const formData: FormDataPewPew = { additionalFiles: [{ - value: createReadStream(PEWPEW_FILEPATH), + value: createReadStream(PEWPEW_ZIP_FILEPATH), options: { filename } }], latest: "true" @@ -207,10 +211,10 @@ describe("PewPew API Integration", () => { after(async () => { // Put the version back try { - const filename: string = path.basename(PEWPEW_FILEPATH); + const filename: string = path.basename(PEWPEW_ZIP_FILEPATH); const formData: FormDataPewPew = { additionalFiles: { - value: createReadStream(PEWPEW_FILEPATH), + value: createReadStream(PEWPEW_ZIP_FILEPATH), options: { filename } } }; diff --git a/controller/components/TestInfo/story.tsx b/controller/components/TestInfo/story.tsx index 22e807e7..6096706e 100644 --- a/controller/components/TestInfo/story.tsx +++ b/controller/components/TestInfo/story.tsx @@ -5,6 +5,7 @@ import { PpaasTestId } from "@fs/ppaas-common/dist/src/ppaastestid"; import React from "react"; import { TestData } from "../../types/testmanager"; import { TestStatus } from "@fs/ppaas-common/dist/types"; +import { latestPewPewVersion } from "../../pages/api/util/clientutil"; /** * Developing and visually testing components in isolation before composing them in your app is useful. @@ -40,7 +41,7 @@ const fullTest: Required = { lastUpdated: new Date(Date.now() - 300000), lastChecked: new Date(Date.now() - 100000), errors: ["error1", "error2", "error3"], - version: "latest", + version: latestPewPewVersion, queueName: "unittest", userId: "bruno.madrigal@pewpew.org" }; diff --git a/controller/integration/pewpew.spec.ts b/controller/integration/pewpew.spec.ts index 9fb2e6ba..34460888 100644 --- a/controller/integration/pewpew.spec.ts +++ b/controller/integration/pewpew.spec.ts @@ -1,18 +1,40 @@ -import { AuthPermission, AuthPermissions, ErrorResponse, PewPewVersionsResponse, TestManagerError } from "../types"; +import { + AuthPermission, + AuthPermissions, + ErrorResponse, + PewPewVersionsResponse, + TestManagerError +} from "../types"; import type { File, Files } from "formidable"; -import { LogLevel, PpaasS3File, log, logger } from "@fs/ppaas-common"; -import { PEWPEW_BINARY_FOLDER, ParsedForm, createFormidableFile, unzipFile } from "../pages/api/util/util"; -import { VERSION_TAG_NAME, deletePewPew, getCurrentPewPewLatestVersion, getPewPewVersionsInS3, getPewpew, postPewPew } from "../pages/api/util/pewpew"; +import { + LogLevel, + PEWPEW_BINARY_EXECUTABLE, + PEWPEW_BINARY_FOLDER, + PpaasS3File, + log, + logger, + sleep +} from "@fs/ppaas-common"; +import { ParsedForm, createFormidableFile, unzipFile } from "../pages/api/util/util"; +import { + VERSION_TAG_NAME, + deletePewPew, + getCurrentPewPewLatestVersion, + getPewPewVersionsInS3, + getPewpew, + postPewPew +} from "../pages/api/util/pewpew"; import { expect } from "chai"; import { latestPewPewVersion } from "../pages/api/util/clientutil"; import path from "path"; +import { platform } from "os"; import semver from "semver"; import { waitForSecrets } from "../pages/api/util/secrets"; logger.config.LogFileName = "ppaas-controller"; const UNIT_TEST_FOLDER = process.env.UNIT_TEST_FOLDER || "test"; -const PEWPEW_FILEPATH = path.join(UNIT_TEST_FOLDER, "pewpew.zip"); +const PEWPEW_ZIP_FILEPATH = process.env.PEWPEW_ZIP_FILEPATH || path.join(UNIT_TEST_FOLDER, PEWPEW_BINARY_EXECUTABLE + ".zip"); const authAdmin: AuthPermissions = { authPermission: AuthPermission.Admin, @@ -20,8 +42,8 @@ const authAdmin: AuthPermissions = { }; const pewpewZipFile: File = createFormidableFile( - path.basename(PEWPEW_FILEPATH), - PEWPEW_FILEPATH, + path.basename(PEWPEW_ZIP_FILEPATH), + PEWPEW_ZIP_FILEPATH, "unittest", 1, null @@ -41,6 +63,10 @@ describe("PewPew Util Integration", () => { additionalFiles: unzippedFiles as any as File }; log("new files " + filename, LogLevel.DEBUG, files); + if (platform() === "win32") { + // Windows gets EBUSY trying to run pewpew --version since the unzip still hasn't released + await sleep(100); + } } catch (error) { log("Error unzipping " + filename, LogLevel.ERROR, error); throw error; @@ -48,25 +74,39 @@ describe("PewPew Util Integration", () => { await waitForSecrets(); }); - describe("getPewPewVersionsInS3", () => { - it("getPewPewVersionsInS3() should return array with elements", (done: Mocha.Done) => { - getPewPewVersionsInS3().then((result: string[]) => { - log("getPewPewVersionsInS3()", LogLevel.DEBUG, result); - expect(result).to.not.equal(undefined); - expect(Array.isArray(result), JSON.stringify(result)).to.equal(true); - expect(result.length).to.be.greaterThan(0); - for (const version of result) { - if (version !== latestPewPewVersion) { - log(`semver.valid(${version}) = ${semver.valid(version)}`, LogLevel.DEBUG); - expect(semver.valid(version), `semver.valid(${version})`).to.not.equal(null); - } - } - if (!sharedPewPewVersions) { - sharedPewPewVersions = result; - } - done(); - }).catch((error) => done(error)); - }); + after(async () => { + try { + const parsedForm: ParsedForm = { + fields: {}, + files + }; + log("postPewPew parsedForm", LogLevel.DEBUG, parsedForm); + const res: ErrorResponse = await postPewPew(parsedForm, authAdmin); + log("postPewPew res", LogLevel.DEBUG, res); + expect(res.status, JSON.stringify(res.json)).to.equal(200); + const body: TestManagerError = res.json; + log("body: " + JSON.stringify(body), LogLevel.DEBUG, body); + expect(body).to.not.equal(undefined); + expect(body.message).to.not.equal(undefined); + expect(body.message).to.include("PewPew uploaded, version"); + expect(body.message).to.not.include("as latest"); + const match: RegExpMatchArray | null = body.message.match(/PewPew uploaded, version: (\d+\.\d+\.\d+)/); + log(`pewpew match: ${match}`, LogLevel.DEBUG, match); + expect(match, "pewpew match").to.not.equal(null); + expect(match!.length, "pewpew match.length").to.be.greaterThan(1); + const version: string = match![1]; + uploadedPewPewVersion = version; + log("uploadedPewPewVersion: " + uploadedPewPewVersion, LogLevel.DEBUG); + const s3Versions: string[] = await getPewPewVersionsInS3(); + expect(s3Versions).to.not.equal(undefined); + expect(Array.isArray(s3Versions), JSON.stringify(s3Versions)).to.equal(true); + expect(s3Versions.length).to.be.greaterThan(0); + sharedPewPewVersions = s3Versions; + expect(s3Versions).to.include(version); + } catch (error) { + log("deletePewPew after error", LogLevel.ERROR, error); + throw error; + } }); describe("postPewPew", () => { @@ -100,12 +140,13 @@ describe("PewPew Util Integration", () => { log("sharedPewPewVersions: " + sharedPewPewVersions, LogLevel.DEBUG); try { const pewpewFiles = await PpaasS3File.getAllFilesInS3({ - s3Folder: `${PEWPEW_BINARY_FOLDER}/${version}`, - localDirectory: process.env.TEMP || "/tmp" + s3Folder: `${PEWPEW_BINARY_FOLDER}/${version}/`, + localDirectory: process.env.TEMP || "/tmp", + extension: PEWPEW_BINARY_EXECUTABLE }); expect(pewpewFiles).to.not.equal(undefined); expect(pewpewFiles.length).to.be.greaterThan(0); - const pewpewFile = pewpewFiles.find( (file) => file.filename === "pewpew"); + const pewpewFile = pewpewFiles.find((file) => file.filename === PEWPEW_BINARY_EXECUTABLE); expect(pewpewFile).to.not.equal(undefined); expect(pewpewFile?.tags).to.not.equal(undefined); expect(pewpewFile?.tags?.get(VERSION_TAG_NAME)).to.equal(version); @@ -151,12 +192,13 @@ describe("PewPew Util Integration", () => { log("sharedPewPewVersions: " + sharedPewPewVersions, LogLevel.DEBUG); try { const pewpewFiles = await PpaasS3File.getAllFilesInS3({ - s3Folder: `${PEWPEW_BINARY_FOLDER}/${latestPewPewVersion}`, - localDirectory: process.env.TEMP || "/tmp" + s3Folder: `${PEWPEW_BINARY_FOLDER}/${latestPewPewVersion}/`, + localDirectory: process.env.TEMP || "/tmp", + extension: PEWPEW_BINARY_EXECUTABLE }); expect(pewpewFiles).to.not.equal(undefined); expect(pewpewFiles.length).to.be.greaterThan(0); - const pewpewFile = pewpewFiles.find( (file) => file.filename === "pewpew"); + const pewpewFile = pewpewFiles.find((file) => file.filename === PEWPEW_BINARY_EXECUTABLE); expect(pewpewFile).to.not.equal(undefined); expect(pewpewFile?.tags).to.not.equal(undefined); expect(pewpewFile?.tags?.get(VERSION_TAG_NAME)).to.equal(version); @@ -173,21 +215,44 @@ describe("PewPew Util Integration", () => { }); }); + describe("getPewPewVersionsInS3", () => { + it("getPewPewVersionsInS3() should return array with elements", (done: Mocha.Done) => { + getPewPewVersionsInS3().then((result: string[]) => { + log("getPewPewVersionsInS3()", LogLevel.DEBUG, result); + expect(result).to.not.equal(undefined); + expect(Array.isArray(result), JSON.stringify(result)).to.equal(true); + expect(result.length).to.be.greaterThan(0); + for (const version of result) { + if (version !== latestPewPewVersion) { + log(`semver.valid(${version}) = ${semver.valid(version)}`, LogLevel.DEBUG); + expect(semver.valid(version), `semver.valid(${version})`).to.not.equal(null); + } + } + if (!sharedPewPewVersions) { + sharedPewPewVersions = result; + } + done(); + }).catch((error) => done(error)); + }); + }); + describe("getPewpew", () => { before(async () => { try { const pewpewFiles = await PpaasS3File.getAllFilesInS3({ - s3Folder: `${PEWPEW_BINARY_FOLDER}/${latestPewPewVersion}`, - localDirectory: process.env.TEMP || "/tmp" + s3Folder: `${PEWPEW_BINARY_FOLDER}/${latestPewPewVersion}/`, + localDirectory: process.env.TEMP || "/tmp", + extension: PEWPEW_BINARY_EXECUTABLE }); expect(pewpewFiles).to.not.equal(undefined); - const pewpewFile = pewpewFiles.find( (file) => file.filename === "pewpew"); + const pewpewFile = pewpewFiles.find((file) => file.filename === PEWPEW_BINARY_EXECUTABLE); expect(pewpewFile).to.not.equal(undefined); } catch (error) { - log("getCurrentPewPewLatest Version failed: ", LogLevel.ERROR, error); + log(latestPewPewVersion + " Pewpew executable not found in S3. failed: ", LogLevel.ERROR, error); throw error; } }); + it("getPewpew should respond 200 OK", (done: Mocha.Done) => { expect(sharedPewPewVersions, "sharedPewPewVersions").to.not.equal(undefined); getPewpew().then((res: ErrorResponse | PewPewVersionsResponse) => { diff --git a/controller/integration/secrets.spec.ts b/controller/integration/secrets.spec.ts index f3d82fe2..4b03d103 100644 --- a/controller/integration/secrets.spec.ts +++ b/controller/integration/secrets.spec.ts @@ -11,7 +11,7 @@ import { getGlobalSecretsConfig, getKey, init as initSecrets, - config as secretsConfig, + internalConfig as secretsConfig, waitForSecrets } from "../pages/api/util/secrets"; // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/controller/integration/testmanager.spec.ts b/controller/integration/testmanager.spec.ts index 1399fad3..f3b4492f 100644 --- a/controller/integration/testmanager.spec.ts +++ b/controller/integration/testmanager.spec.ts @@ -124,7 +124,7 @@ describe("TestManager Integration", () => { expect(sharedPewPewVersions, "sharedPewPewVersions").to.not.equal(undefined); expect(sharedPewPewVersions!.length, "sharedPewPewVersions.length").to.be.greaterThan(0); numberedVersion = sharedPewPewVersions!.find((pewpewVersion: string) => pewpewVersion !== latestPewPewVersion); - expect(numberedVersion).to.not.equal(undefined); + expect(numberedVersion, "numberedVersion").to.not.equal(undefined); log("numberedVersion", LogLevel.DEBUG, { numberedVersion }); const basicFilenameWithEnv = path.basename(BASIC_FILEPATH_WITH_ENV); const ppaasTestIdWithEnv: PpaasTestId = PpaasTestId.makeTestId(basicFilenameWithEnv); diff --git a/controller/next.config.js b/controller/next.config.js index 4dcfb382..606d9548 100644 --- a/controller/next.config.js +++ b/controller/next.config.js @@ -3,6 +3,8 @@ const { access, symlink } = require('fs/promises'); const { join } = require('path'); +const CopyPlugin = require("copy-webpack-plugin"); +const { platform } = require('os'); if (process.env.BASE_PATH && !process.env.BASE_PATH.startsWith("/")) { const errorMessage = "process.env.BASE_PATH must start with a '/' found " + process.env.BASE_PATH; @@ -53,9 +55,20 @@ const nextConfig = { if (!config.experiments) { config.experiments = {}; } config.experiments.asyncWebAssembly = true; - // https://github.com/vercel/next.js/issues/25852#issuecomment-1057059000 + // https://github.com/vercel/next.js/issues/25852 + // Compiling we run into an issue where it can't find the config wasm. + // On Linux the workaround is to create a symlink to the correct location + // On Windows, the symlinks fail so we must copy the file config.plugins.push( - new (class { + platform() === "win32" + // https://github.com/vercel/next.js/issues/25852#issuecomment-1727385542 + ? new CopyPlugin({ + patterns: [ + { from: "../lib/config-wasm/pkg/config_wasm_bg.wasm", to: "./" }, + ], + }) + // https://github.com/vercel/next.js/issues/25852#issuecomment-1057059000 + : new (class { apply(compiler) { compiler.hooks.afterEmit.tapPromise( 'SymlinkWebpackPlugin', diff --git a/controller/package.json b/controller/package.json index 55bc2e22..c689f2a7 100644 --- a/controller/package.json +++ b/controller/package.json @@ -104,6 +104,7 @@ "@typescript-eslint/parser": "^7.0.0", "aws-sdk-client-mock": "^3.0.0", "chai": "^4.3.7", + "copy-webpack-plugin": "^12.0.2", "dotenv": "^16.0.0", "dotenv-flow": "^4.0.1", "esbuild": ">=0.17", diff --git a/controller/pages/admin.tsx b/controller/pages/admin.tsx index 5e6c86b5..798e43f9 100644 --- a/controller/pages/admin.tsx +++ b/controller/pages/admin.tsx @@ -23,6 +23,7 @@ import DropFile from "../components/DropFile"; import FilesList from "../components/FilesList"; import Layout from "../components/Layout"; import { Line } from "rc-progress"; +import { PEWPEW_BINARY_EXECUTABLE_NAMES } from "@fs/ppaas-common/dist/src/util/util"; import { authPage } from "./api/util/authserver"; import { getServerSideProps as getPropsPewPewVersions } from "../components/PewPewVersions/initialProps"; import styled from "styled-components"; @@ -95,7 +96,7 @@ const Admin = ({ authPermission, versionInitalProps, error: propsError }: AdminP const additionalFiles: File[] = []; for (const file of filelist) { // log("File Upload File", LogLevel.DEBUG, file); - if (file.name === "pewpew" || file.name === "pewpew.exe" || file.name.endsWith(".zip")) { + if (PEWPEW_BINARY_EXECUTABLE_NAMES.includes(file.name) || file.name.endsWith(".zip")) { const existingIndex = state.additionalFiles.findIndex((additionalFile: File) => additionalFile.name === file.name); if (existingIndex >= 0) { // We can't have two files with the same name even if their path's are different. They go in the same s3 folder diff --git a/controller/pages/api/util/clientutil.ts b/controller/pages/api/util/clientutil.ts index d1234529..24b05cce 100644 --- a/controller/pages/api/util/clientutil.ts +++ b/controller/pages/api/util/clientutil.ts @@ -2,6 +2,8 @@ import { LogLevel, log } from "./log"; import { TestData, TestManagerError, TestManagerMessage } from "../../../types"; import { AxiosError } from "axios"; import { IncomingMessage } from "http"; +// Must import from sub-path to avoid other dependencies +import { PEWPEW_VERSION_LATEST } from "@fs/ppaas-common/dist/src/util/util"; import getConfig from "next/config"; import semver from "semver"; @@ -116,7 +118,7 @@ export function getHourMinuteFromTimestamp (datetime: number): string { return date.toTimeString().split(" ")[0]; } -export const latestPewPewVersion: string = "latest"; +export const latestPewPewVersion: string = PEWPEW_VERSION_LATEST; /** * Sorts the array by versions returning the latest versions first and invalid versions last. diff --git a/controller/pages/api/util/pewpew.ts b/controller/pages/api/util/pewpew.ts index 52b73db4..d7aaca1f 100644 --- a/controller/pages/api/util/pewpew.ts +++ b/controller/pages/api/util/pewpew.ts @@ -6,13 +6,15 @@ import { import type { Fields, File, Files } from "formidable"; import { LOCAL_FILE_LOCATION, - PEWPEW_BINARY_FOLDER, ParsedForm, getLogAuthPermissions, uploadFile } from "./util"; import { LogLevel, + PEWPEW_BINARY_EXECUTABLE, + PEWPEW_BINARY_EXECUTABLE_NAMES, + PEWPEW_BINARY_FOLDER, PpaasS3File, log, logger, @@ -31,8 +33,6 @@ const execFile = promisify(_execFile); logger.config.LogFileName = "ppaas-controller"; const deleteS3 = s3.deleteObject; -export const PEWPEW_EXECUTABLE_NAME: string = "pewpew"; -const PEWPEW_EXECUTABLE_NAME_WINDOWS: string = "pewpew.exe"; export const VERSION_TAG_NAME: string = "version"; /** @@ -45,7 +45,7 @@ export async function getPewPewVersionsInS3 (): Promise { const pewpewFiles: PpaasS3File[] = await PpaasS3File.getAllFilesInS3({ s3Folder: PEWPEW_BINARY_FOLDER, localDirectory: LOCAL_FILE_LOCATION, - extension: PEWPEW_EXECUTABLE_NAME + extension: PEWPEW_BINARY_EXECUTABLE }); if (pewpewFiles.length === 0) { throw new Error("No pewpew binaries found in s3"); @@ -103,9 +103,9 @@ export async function postPewPew (parsedForm: ParsedForm, authPermissions: AuthP if (!files || fileKeys.length === 0 || !fileKeys.includes("additionalFiles")) { return { json: { message: "Must provide a additionalFiles minimally" }, status: 400 }; } else if ( - ((Array.isArray(files.additionalFiles) && files.additionalFiles.every((file: File) => file && (file.originalFilename === PEWPEW_EXECUTABLE_NAME || file.originalFilename === PEWPEW_EXECUTABLE_NAME_WINDOWS))) - || (!Array.isArray(files.additionalFiles) && (files.additionalFiles.originalFilename === PEWPEW_EXECUTABLE_NAME || files.additionalFiles.originalFilename === PEWPEW_EXECUTABLE_NAME_WINDOWS))) - ) { + ((Array.isArray(files.additionalFiles) && files.additionalFiles.every((file: File) => file && file.originalFilename && PEWPEW_BINARY_EXECUTABLE_NAMES.includes(file.originalFilename))) + || (!Array.isArray(files.additionalFiles) && (files.additionalFiles.originalFilename && PEWPEW_BINARY_EXECUTABLE_NAMES.includes(files.additionalFiles.originalFilename)))) + ) { // Everything here is just pepew if (Array.isArray(fields.latest)) { return { json: { message: "Only one 'latest' is allowed" }, status: 400 }; @@ -116,7 +116,7 @@ export async function postPewPew (parsedForm: ParsedForm, authPermissions: AuthP const latest: boolean = fields.latest === "true"; // Run pewpew --version and parse the version - const pewpewVersionBinaryName = os.platform() === "win32" ? PEWPEW_EXECUTABLE_NAME_WINDOWS : PEWPEW_EXECUTABLE_NAME; + const pewpewVersionBinaryName = PEWPEW_BINARY_EXECUTABLE; log(`os.platform(): ${os.platform()}`, LogLevel.DEBUG, { platform: os.platform(), pewpewVersionBinary: pewpewVersionBinaryName }); // Find the binary for our platform. @@ -148,7 +148,7 @@ export async function postPewPew (parsedForm: ParsedForm, authPermissions: AuthP const uploadPromises: Promise[] = []; const versionLogDisplay = latest ? `${version} as latest` : version; const versionFolder = latest ? latestPewPewVersion : version; - log(PEWPEW_EXECUTABLE_NAME + " only upload, version: " + versionLogDisplay, LogLevel.DEBUG, files); + log(PEWPEW_BINARY_EXECUTABLE + " only upload, version: " + versionLogDisplay, LogLevel.DEBUG, files); // Pass in an override Map to override the default tags and not set a "test" tag const tags = new Map([[PEWPEW_BINARY_FOLDER, "true"], [VERSION_TAG_NAME, version]]); if (Array.isArray(files.additionalFiles)) { @@ -162,14 +162,9 @@ export async function postPewPew (parsedForm: ParsedForm, authPermissions: AuthP // If latest version is being updated: if (latest) { global.currentLatestVersion = version; - try { - - log("Sucessfully saved PewPew's latest version to file. ", LogLevel.INFO); - } catch (error) { - log("Writing latest PewPew's latest version to file failed: ", LogLevel.ERROR, error); - } + log("Sucessfully updated currentLatestVersion. ", LogLevel.INFO, version); } - log(PEWPEW_EXECUTABLE_NAME + " only uploaded, version: " + versionLogDisplay, LogLevel.INFO, { files, authPermissions: getLogAuthPermissions(authPermissions) }); + log(PEWPEW_BINARY_EXECUTABLE + " only uploaded, version: " + versionLogDisplay, LogLevel.INFO, { files, authPermissions: getLogAuthPermissions(authPermissions) }); return { json: { message: "PewPew uploaded, version: " + versionLogDisplay }, status: 200 }; } else { // We're missing pewpew uploads @@ -212,7 +207,7 @@ export async function deletePewPew (query: Partial ({ Key, Value })) }; log("TagResourceCommand request", LogLevel.DEBUG, input); - const response: TagResourceCommandOutput = await config.secretsClient.send(new TagResourceCommand(input)); + const response: TagResourceCommandOutput = await internalConfig.secretsClient.send(new TagResourceCommand(input)); log("TagResourceCommand succeeded", LogLevel.DEBUG, { ...response, "$metadata": undefined @@ -128,7 +129,7 @@ export async function deleteSecret (secretKeyName: string, force?: boolean): Pro ForceDeleteWithoutRecovery: force }; log("DeleteSecretCommand request", LogLevel.DEBUG, input); - const response: DeleteSecretCommandOutput = await config.secretsClient.send(new DeleteSecretCommand(input)); + const response: DeleteSecretCommandOutput = await internalConfig.secretsClient.send(new DeleteSecretCommand(input)); log("DeleteSecretCommand succeeded", LogLevel.DEBUG, { ...response, "$metadata": undefined @@ -148,7 +149,7 @@ export async function getSecretValue (secretKeyName: string): Promise { SecretId: secretKeyName }; log("GetSecretValueCommand request", LogLevel.DEBUG, input); - const response: GetSecretValueCommandOutput = await config.secretsClient.send(new GetSecretValueCommand(input)); + const response: GetSecretValueCommandOutput = await internalConfig.secretsClient.send(new GetSecretValueCommand(input)); log("GetSecretValueCommand succeeded", LogLevel.TRACE, response); log("GetSecretValueCommand succeeded", LogLevel.DEBUG, { ...response, @@ -195,14 +196,14 @@ export async function getKey (secretKeyName: string): Promise { /** Async function that loads and waits for the encryption key */ async function getAndSetEncryptionKey (): Promise { - if (!config.encryptionKeyName) { + if (!internalConfig.encryptionKeyName) { throw new Error("SECRETS_ENCRYPTION_KEY_NAME environment variable not set"); } if (getGlobalSecretsConfig().encryptionKey === undefined) { // Keep trying to get the key try { log("getAndSetEncryptionKey start", LogLevel.DEBUG); - const key: string = await getKey(config.encryptionKeyName); + const key: string = await getKey(internalConfig.encryptionKeyName); log("getAndSetEncryptionKey finished", LogLevel.DEBUG); getGlobalSecretsConfig().encryptionKey = Buffer.from(key, "hex"); } catch (error) { @@ -218,7 +219,7 @@ getAndSetEncryptionKey() /** Async function that loads and waits for the client secret */ async function getAndSetOpenIdClientSecret (): Promise { - if (!config.openIdClientSecret) { + if (!internalConfig.openIdClientSecret) { throw new Error("SECRETS_OPENID_CLIENT_SECRET_NAME environment variable not set"); } if (getGlobalSecretsConfig().openIdClientSecret === undefined) { @@ -226,7 +227,7 @@ async function getAndSetOpenIdClientSecret (): Promise { try { log("getAndSetOpenIdClientSecret start", LogLevel.DEBUG); - const key: string = await getKey(config.openIdClientSecret); + const key: string = await getKey(internalConfig.openIdClientSecret); getGlobalSecretsConfig().openIdClientSecret = Buffer.from(key, "ascii"); // OpenId is an ascii password log("getAndSetOpenIdClientSecret finished", LogLevel.DEBUG); } catch (error) { diff --git a/controller/pages/api/util/testmanager.ts b/controller/pages/api/util/testmanager.ts index 4ebeeec0..e8bf25b6 100644 --- a/controller/pages/api/util/testmanager.ts +++ b/controller/pages/api/util/testmanager.ts @@ -19,7 +19,6 @@ import { } from "../../../types"; import { ENCRYPTED_TEST_SCHEDULER_FOLDERNAME, - PEWPEW_BINARY_FOLDER, ParsedForm, createFormidableFile, getLogAuthPermissions, @@ -32,6 +31,8 @@ import { EnvironmentVariables, LogLevel, MessageType, + PEWPEW_BINARY_EXECUTABLE_NAMES, + PEWPEW_BINARY_FOLDER, PpaasS3File, PpaasS3Message, PpaasTestId, @@ -195,7 +196,7 @@ async function getUpdatedTestDataFromStoredData (storedTestData: StoredTestData) ppaasTestStatus.status = TestStatus.Failed; ppaasTestStatus.errors = [...(ppaasTestStatus.errors || []), "End time or last status update were more than fifteen minutes ago, status manually changed to Failed"]; storedTestData.lastChecked = new Date(); - ppaasTestStatus.writeStatus().catch((error) => log("Could not write testStatus to S3 for testId " + storedTestData.testId, LogLevel.ERROR, error)); + ppaasTestStatus.writeStatus().catch((error: unknown) => log("Could not write testStatus to S3 for testId " + storedTestData.testId, LogLevel.ERROR, error)); TestScheduler.addHistoricalTest(ppaasTestStatus.getTestId(), undefined, ppaasTestStatus.startTime, ppaasTestStatus.endTime, ppaasTestStatus.status) .catch(() => { /* noop */ }); } @@ -1078,7 +1079,8 @@ export abstract class TestManager { let version: string | undefined; if (fieldKeys.includes("version")) { // Validate the version is in s3 - if (Array.isArray(fields.version) || !fields.version || !await PpaasS3File.existsInS3("pewpew/" + fields.version)) { + if (Array.isArray(fields.version) || !fields.version + || !await PpaasS3File.existsInS3(`${PEWPEW_BINARY_FOLDER}/${fields.version}/`)) { return { json: { message: "Received an invalid version: " + fields.version }, status: 400 }; } version = fields.version; @@ -1166,7 +1168,7 @@ export abstract class TestManager { } Object.assign(environmentVariablesFile, parsedEnv); - } else if (file.originalFilename === "pewpew" || file.originalFilename === "pewpew.exe") { + } else if (PEWPEW_BINARY_EXECUTABLE_NAMES.includes(file.originalFilename)) { if (authPermission < AuthPermission.Admin) { log("Unauthorized User attempted to use custom pewpew binary.", LogLevel.WARN, { yamlFile }); return { json: { message: "User is not authorized to use custom pewpew binaries. If you think this is an error, please contact the PerformanceQA team." }, status: 403 }; @@ -1310,7 +1312,7 @@ export abstract class TestManager { return { json: { message: "Invalid Yaml filename", error: `${error}` }, status: 400 }; } // Check for pewpew/ and settings/ and reject - if ((ppaasTestId.yamlFile) === PEWPEW_BINARY_FOLDER || ppaasTestId.yamlFile === ENCRYPTED_TEST_SCHEDULER_FOLDERNAME) { + if (ppaasTestId.yamlFile.startsWith(PEWPEW_BINARY_FOLDER) || ppaasTestId.yamlFile.startsWith(ENCRYPTED_TEST_SCHEDULER_FOLDERNAME)) { return { json: { message: ppaasTestId.yamlFile + " is a reserved word and cannot be used for a yaml file. Please change your yaml filename" }, status: 400 }; } const testId = ppaasTestId.testId; diff --git a/controller/pages/api/util/util.ts b/controller/pages/api/util/util.ts index e78eb999..59aa26a4 100644 --- a/controller/pages/api/util/util.ts +++ b/controller/pages/api/util/util.ts @@ -7,7 +7,8 @@ import { LogLevel, PpaasS3File, log, - logger + logger, + sleep } from "@fs/ppaas-common"; import { Entry as ZipEntry, ZipFile, Options as ZipOptions, open as _yauzlOpen } from "yauzl"; import { formatError, isYamlFile } from "./clientutil"; @@ -19,11 +20,11 @@ import formidable, { Files, Options as FormidableOptions } from "formidable"; +import { platform, tmpdir } from "os"; import { NextApiRequest as Request } from "next"; import { createWriteStream } from "fs"; import fs from "fs/promises"; import { promisify } from "util"; -import { tmpdir } from "os"; const yauzlOpen: (path: string, options: ZipOptions) => Promise = promisify(_yauzlOpen); // We have to set this before we make any log calls @@ -32,7 +33,6 @@ logger.config.LogFileName = "ppaas-controller"; export const LOCAL_FILE_LOCATION: string = process.env.LOCAL_FILE_LOCATION || process.env.TEMP || tmpdir() || "/tmp"; const MAX_FILE_SIZE_MB = parseInt(process.env.MAX_FILE_SIZE_MB || "0") || 500; const MAX_ZIP_FILE_COUNT = 10; -export const PEWPEW_BINARY_FOLDER: string = "pewpew"; export const ENCRYPTED_TEST_SCHEDULER_FOLDERNAME = "settings"; export const ENCRYPTED_TEST_SCHEDULER_FILENAME = "testscheduler.json"; @@ -263,6 +263,7 @@ export async function parseZip (formFiles: Files): Promise { return nonYamlFiles; }; if (fileKeys.includes("additionalFiles")) { + let hasZipFiles: boolean = false; const additionalFiles: File | File[] = formFiles.additionalFiles; log("parseForm currentFile", LogLevel.DEBUG, additionalFiles); if (Array.isArray(additionalFiles)) { @@ -271,6 +272,7 @@ export async function parseZip (formFiles: Files): Promise { // Need to do an "as" cast here for typechecking for (const file of additionalFiles) { if (file.originalFilename?.endsWith(".zip")) { + hasZipFiles = true; // Remove this one and add the unzipped const unzippedFiles: File[] = await unzipFile(file); // Check if any are yamlFiles @@ -285,10 +287,15 @@ export async function parseZip (formFiles: Files): Promise { formFiles.additionalFiles = newFiles; } else { if (additionalFiles.originalFilename?.endsWith(".zip")) { + hasZipFiles = true; const unzippedFiles: File[] = await unzipFile(additionalFiles); formFiles.additionalFiles = filterYamlFromUnzipped(unzippedFiles); } } + if (hasZipFiles && platform() === "win32") { + // There's a race condition on Windows where we can't use the file immediately because the unzip has it locked + await sleep(100); + } } // If additionalFiles is an empty array, remove it if (Array.isArray(formFiles.additionalFiles) && formFiles.additionalFiles.length === 0) { diff --git a/controller/test/mock.ts b/controller/test/mock.ts index 267579e6..7780fcab 100644 --- a/controller/test/mock.ts +++ b/controller/test/mock.ts @@ -57,7 +57,7 @@ import { IV_LENGTH as SECRET_LENGTH, getGlobalSecretsConfig, init as initSecrets, - config as secretsConfig + internalConfig as secretsConfig } from "../pages/api/util/secrets"; import { Readable } from "stream"; import { constants as bufferConstants } from "node:buffer"; diff --git a/controller/test/pewpew.spec.ts b/controller/test/pewpew.spec.ts index 6b33b52f..0c13e6d1 100644 --- a/controller/test/pewpew.spec.ts +++ b/controller/test/pewpew.spec.ts @@ -6,15 +6,22 @@ import { TestManagerError } from "../types"; import type { File, Files } from "formidable"; -import { LogLevel, PpaasTestId, log, logger } from "@fs/ppaas-common"; import { + LogLevel, + PEWPEW_BINARY_EXECUTABLE, PEWPEW_BINARY_FOLDER, + PpaasTestId, + log, + logger, + sleep +} from "@fs/ppaas-common"; +import { ParsedForm, createFormidableFile, unzipFile } from "../pages/api/util/util"; +import { TestScheduler, TestSchedulerItem } from "../pages/api/util/testscheduler"; import { - PEWPEW_EXECUTABLE_NAME, VERSION_TAG_NAME, deletePewPew, getCurrentPewPewLatestVersion, @@ -22,7 +29,6 @@ import { getPewpew, postPewPew } from "../pages/api/util/pewpew"; -import { TestScheduler, TestSchedulerItem } from "../pages/api/util/testscheduler"; import { mockGetObjectTagging, mockListObject, @@ -35,12 +41,13 @@ import { _Object as S3Object } from "@aws-sdk/client-s3"; import { expect } from "chai"; import { latestPewPewVersion } from "../pages/api/util/clientutil"; import path from "path"; +import { platform } from "os"; import semver from "semver"; logger.config.LogFileName = "ppaas-controller"; const UNIT_TEST_FOLDER = process.env.UNIT_TEST_FOLDER || "test"; -const PEWPEW_FILEPATH = path.join(UNIT_TEST_FOLDER, "pewpew.zip"); +const PEWPEW_ZIP_FILEPATH = process.env.PEWPEW_ZIP_FILEPATH || path.join(UNIT_TEST_FOLDER, PEWPEW_BINARY_EXECUTABLE + ".zip"); const authAdmin: AuthPermissions = { authPermission: AuthPermission.Admin, @@ -48,8 +55,8 @@ const authAdmin: AuthPermissions = { }; const pewpewZipFile: File = createFormidableFile( - path.basename(PEWPEW_FILEPATH), - PEWPEW_FILEPATH, + path.basename(PEWPEW_ZIP_FILEPATH), + PEWPEW_ZIP_FILEPATH, "unittest", 1, null @@ -59,7 +66,7 @@ const invalidFiles = { }; const pewpewS3Folder = PEWPEW_BINARY_FOLDER; -const pewpewFilename = PEWPEW_EXECUTABLE_NAME; +const pewpewFilename = PEWPEW_BINARY_EXECUTABLE; const versions = ["0.5.10", "0.5.11", "0.5.12-preview1", "0.5.12-preview2", "latest"]; const s3Object: S3Object = { LastModified: new Date(), @@ -98,6 +105,10 @@ describe("PewPew Util", () => { mixedFiles = { additionalFiles: [...unzippedFiles, pewpewZipFile] as any as File }; + if (platform() === "win32") { + // Windows gets EBUSY trying to run pewpew --version since the unzip still hasn't released + await sleep(100); + } } catch (error) { log("Error unzipping " + filename, LogLevel.ERROR, error); throw error; diff --git a/controller/test/pewpew.zip b/controller/test/pewpew.zip index 412f548b..e8115cf7 100644 Binary files a/controller/test/pewpew.zip and b/controller/test/pewpew.zip differ diff --git a/package-lock.json b/package-lock.json index 090c4242..aaab02e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -181,6 +181,7 @@ "@typescript-eslint/parser": "^7.0.0", "aws-sdk-client-mock": "^3.0.0", "chai": "^4.3.7", + "copy-webpack-plugin": "^12.0.2", "dotenv": "^16.0.0", "dotenv-flow": "^4.0.1", "esbuild": ">=0.17", @@ -4835,6 +4836,18 @@ "integrity": "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==", "dev": true }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -10359,6 +10372,136 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/copy-webpack-plugin/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/copy-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/core-js-compat": { "version": "3.36.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", @@ -12936,9 +13079,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -14872,9 +15015,9 @@ } }, "node_modules/jose": { - "version": "4.15.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", - "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -20671,6 +20814,18 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -20950,9 +21105,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz", - "integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", "dev": true, "dependencies": { "colorette": "^2.0.10",