diff --git a/lib/main.js b/lib/main.js index a710675e..83e69edc 100644 --- a/lib/main.js +++ b/lib/main.js @@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { @@ -32,524 +42,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getExistingEnv = exports.getExistingParams = exports.getExistingCriteria = void 0; -const core = __importStar(require("@actions/core")); -const map = __importStar(require("./mappers")); -const util = __importStar(require("./util")); -const TestKind_1 = require("./engine/TestKind"); +const util = __importStar(require("./models/FileUtils")); +const UtilModels_1 = require("./models/UtilModels"); const fs = __importStar(require("fs")); -const util_1 = require("util"); -const FeatureFlagService_1 = require("./services/FeatureFlagService"); -const FeatureFlags_1 = require("./services/FeatureFlags"); -const resultFolder = 'loadTest'; -const reportZipFileName = 'report.zip'; -const resultZipFileName = 'results.zip'; -let baseURL = ''; -let testId = ''; -let existingCriteria = {}; -let existingParams = {}; -let existingEnv = {}; -var FileType; -(function (FileType) { - FileType["JMX_FILE"] = "JMX_FILE"; - FileType["USER_PROPERTIES"] = "USER_PROPERTIES"; - FileType["ADDITIONAL_ARTIFACTS"] = "ADDITIONAL_ARTIFACTS"; - FileType["ZIPPED_ARTIFACTS"] = "ZIPPED_ARTIFACTS"; - FileType["URL_TEST_CONFIG"] = "URL_TEST_CONFIG"; - FileType["TEST_SCRIPT"] = "TEST_SCRIPT"; -})(FileType || (FileType = {})); +const core = __importStar(require("@actions/core")); +const AuthenticationUtils_1 = require("./models/AuthenticationUtils"); +const TaskModels_1 = require("./models/TaskModels"); +const APISupport_1 = require("./models/APISupport"); function run() { return __awaiter(this, void 0, void 0, function* () { try { - yield map.getInputParams(); - yield getLoadTestResource(); - testId = map.getTestId(); - yield getTestAPI(false); - if (fs.existsSync(resultFolder)) { - util.deleteFile(resultFolder); - } - fs.mkdirSync(resultFolder); - yield createTestAPI(); + let authContext = new AuthenticationUtils_1.AuthenticationUtils(); + let yamlConfig = new TaskModels_1.YamlConfig(); + let apiSupport = new APISupport_1.APISupport(authContext, yamlConfig); + yield authContext.authorize(); + yield apiSupport.getResource(); + yield apiSupport.getTestAPI(false); + if (fs.existsSync(UtilModels_1.resultFolder)) { + util.deleteFile(UtilModels_1.resultFolder); + } + fs.mkdirSync(UtilModels_1.resultFolder); + yield apiSupport.createTestAPI(); } catch (err) { core.setFailed(err.message); } }); } -function getTestAPI(validate_1) { - return __awaiter(this, arguments, void 0, function* (validate, returnTestObj = false) { - var _a, _b; - var urlSuffix = "tests/" + testId + "?api-version=" + util.apiConstants.latestVersion; - urlSuffix = baseURL + urlSuffix; - let header = yield map.getTestHeader(); - let testResult = yield util.httpClientRetries(urlSuffix, header, 'get', 3, ""); - if (testResult.message.statusCode == 401 || testResult.message.statusCode == 403) { - var message = "Service Principal does not have sufficient permissions. Please assign " - + "the Load Test Contributor role to the service principal. Follow the steps listed at " - + "https://docs.microsoft.com/azure/load-testing/tutorial-cicd-github-actions#configure-the-github-actions-workflow-to-run-a-load-test "; - throw new Error(message); - } - if (testResult.message.statusCode != 200 && testResult.message.statusCode != 201) { - if (validate) { // validate is called, then get should not be false, and this validate had retries because of the conflicts in jmx test, so lets not print in the console, instead put this in the error itself. - let testObj = yield util.getResultObj(testResult); - let err = ((_a = testObj === null || testObj === void 0 ? void 0 : testObj.error) === null || _a === void 0 ? void 0 : _a.message) ? (_b = testObj === null || testObj === void 0 ? void 0 : testObj.error) === null || _b === void 0 ? void 0 : _b.message : util.ErrorCorrection(testResult); - throw new Error(err); - } - else if (!validate && testResult.message.statusCode != 404) { // if not validate, then its to check if it is edit or create thats all, so it should not throw the error for 404. - let testObj = yield util.getResultObj(testResult); - console.log(testObj ? testObj : util.ErrorCorrection(testResult)); - throw new Error("Error in getting the test."); - } - // note : kumarmoh - /// else { - // do nothing if the validate = false and status code is 404, as it is for create test. - // } this is just for comment - } - if (testResult.message.statusCode == 200) { - let testObj = yield util.getResultObj(testResult); - if (testObj == null) { - throw new Error(util.ErrorCorrection(testResult)); - } - if (testObj.kind == null) { - testObj.kind = testObj.testType; - } - var inputScriptFileInfo = testObj.kind == TestKind_1.TestKind.URL ? testObj.inputArtifacts.urlTestConfigFileInfo : testObj.inputArtifacts.testScriptFileInfo; - if (validate) { - if (returnTestObj) { - return [inputScriptFileInfo.validationStatus, testObj]; - } - return inputScriptFileInfo.validationStatus; - } - else { - if (!(0, util_1.isNullOrUndefined)(testObj.passFailCriteria) && !(0, util_1.isNullOrUndefined)(testObj.passFailCriteria.passFailMetrics)) - existingCriteria = testObj.passFailCriteria.passFailMetrics; - if (testObj.secrets != null) - existingParams = testObj.secrets; - if (testObj.environmentVariables != null) - existingEnv = testObj.environmentVariables; - } - } - }); -} -function deleteFileAPI(filename) { - return __awaiter(this, void 0, void 0, function* () { - var urlSuffix = "tests/" + testId + "/files/" + filename + "?api-version=" + util.apiConstants.latestVersion; - urlSuffix = baseURL + urlSuffix; - let header = yield map.getTestHeader(); - let delFileResult = yield util.httpClientRetries(urlSuffix, header, 'del', 3, ""); - if (delFileResult.message.statusCode != 204) { - let delFileObj = yield util.getResultObj(delFileResult); - let Message = delFileObj ? delFileObj.message : util.ErrorCorrection(delFileResult); - throw new Error(Message); - } - }); -} -function createTestAPI() { - return __awaiter(this, void 0, void 0, function* () { - var urlSuffix = "tests/" + testId + "?api-version=" + util.apiConstants.latestVersion; - urlSuffix = baseURL + urlSuffix; - var createData = map.createTestData(); - let header = yield map.createTestHeader(); - let createTestresult = yield util.httpClientRetries(urlSuffix, header, 'patch', 3, JSON.stringify(createData)); - if (createTestresult.message.statusCode != 200 && createTestresult.message.statusCode != 201) { - let testRunObj = yield util.getResultObj(createTestresult); - console.log(testRunObj ? testRunObj : util.ErrorCorrection(createTestresult)); - throw new Error("Error in creating test: " + testId); - } - if (createTestresult.message.statusCode == 201) { - console.log("Creating a new load test '" + testId + "' "); - console.log("Successfully created load test " + testId); - } - else { - console.log("Test '" + testId + "' already exists"); - // test script will anyway be updated by the GH-actions in later steps, this will be error if the test script is not present in the test. - // this will be error in the url tests when the quick test is getting updated to the url test. so removing this. - let testObj = yield util.getResultObj(createTestresult); - var testFiles = testObj.inputArtifacts; - if (testFiles.userPropUrl != null) { - console.log(`Deleting the existing UserProperty file.`); - yield deleteFileAPI(testFiles.userPropFileInfo.fileName); - } - if (testFiles.testScriptFileInfo != null) { - console.log(`Deleting the existing TestScript file.`); - yield deleteFileAPI(testFiles.testScriptFileInfo.fileName); - } - if (testFiles.additionalFileInfo != null) { - // delete existing files which are not present in yaml, the files which are in yaml will anyway be uploaded again. - let existingFiles = []; - let file; - for (file of testFiles.additionalFileInfo) { - existingFiles.push(file.fileName); - } - for (file of map.getConfigFiles()) { - let indexOfFile = existingFiles.indexOf(file); - if (indexOfFile != -1) { - existingFiles.splice(indexOfFile, 1); - } - } - for (file of map.getZipFiles()) { - let indexOfFile = existingFiles.indexOf(file); - if (indexOfFile != -1) { - existingFiles.splice(indexOfFile, 1); - } - } - if (existingFiles.length > 0) { - console.log(`Deleting the ${existingFiles.length} existing test file(s) which is(are) not in the configuration yaml file.`); - } - for (file of existingFiles) { - yield deleteFileAPI(file); - } - } - } - yield uploadConfigFile(); - }); -} -function uploadTestPlan() { - return __awaiter(this, void 0, void 0, function* () { - let retry = 5; - let filepath = map.getTestFile(); - let filename = map.getFileName(filepath); - var urlSuffix = "tests/" + testId + "/files/" + filename + "?api-version=" + util.apiConstants.latestVersion; - let fileType = FileType.TEST_SCRIPT; - if (map.getTestKind() == TestKind_1.TestKind.URL) { - fileType = FileType.URL_TEST_CONFIG; - } - urlSuffix = baseURL + urlSuffix + ("&fileType=" + fileType); - let headers = yield map.UploadAndValidateHeader(); - let uploadresult = yield util.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); - if (uploadresult.message.statusCode != 201) { - let uploadObj = yield util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading TestPlan for the created test"); - } - else { - console.log("Uploaded test plan for the test"); - var minutesToAdd = 10; - var startTime = new Date(); - var maxAllowedTime = new Date(startTime.getTime() + minutesToAdd * 60000); - var validationStatus = "VALIDATION_INITIATED"; - var testObj; - while (maxAllowedTime > (new Date()) && (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED" || validationStatus == null)) { - try { - [validationStatus, testObj] = yield getTestAPI(true, true); - } - catch (e) { - retry--; - if (retry == 0) { - throw new Error("Unable to validate the test plan. Please retry. Failed with error :" + e); - } - } - yield util.sleep(5000); - } - console.log("Validation status of the test plan: " + validationStatus); - if (validationStatus == null || validationStatus == "VALIDATION_SUCCESS") { - console.log(`Validated test plan for the test successfully.`); - // Get errors from all files - var fileErrors = util.getAllFileErrors(testObj); - if (Object.keys(fileErrors).length > 0) { - console.log("Validation failed for the following files:"); - for (const [file, error] of Object.entries(fileErrors)) { - console.log(`File: ${file}, Error: ${error}`); - } - throw new Error("Validation of one or more files failed. Please correct the errors and try again."); - } - yield createTestRun(); - } - else if (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED") - throw new Error("TestPlan validation timeout. Please try again."); - else - throw new Error("TestPlan validation Failed."); - } - }); -} -function uploadConfigFile() { - return __awaiter(this, void 0, void 0, function* () { - let configFiles = map.getConfigFiles(); - if (configFiles != undefined && configFiles.length > 0) { - for (let filepath of configFiles) { - let filename = map.getFileName(filepath); - var urlSuffix = "tests/" + testId + "/files/" + filename + "?api-version=" + util.apiConstants.latestVersion + ("&fileType=" + FileType.ADDITIONAL_ARTIFACTS); - urlSuffix = baseURL + urlSuffix; - let headers = yield map.UploadAndValidateHeader(); - let uploadresult = yield util.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); - if (uploadresult.message.statusCode != 201) { - let uploadObj = yield util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading config file for the created test"); - } - } - console.log(`Uploaded ${configFiles.length} configuration file(s) for the test successfully.`); - } - yield uploadZipArtifacts(); - }); -} -function uploadZipArtifacts() { - return __awaiter(this, void 0, void 0, function* () { - var _a; - let zipFiles = map.getZipFiles(); - if (zipFiles != undefined && zipFiles.length > 0) { - console.log("Uploading and validating the zip artifacts"); - for (let filepath of zipFiles) { - let filename = map.getFileName(filepath); - var urlSuffix = "tests/" + testId + "/files/" + filename + "?api-version=" + util.apiConstants.latestVersion + "&fileType=" + FileType.ZIPPED_ARTIFACTS; - urlSuffix = baseURL + urlSuffix; - let headers = yield map.UploadAndValidateHeader(); - let uploadresult = yield util.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); - if (uploadresult.message.statusCode != 201) { - let uploadObj = yield util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading config file for the created test"); - } - } - var minutesToAdd = 5; - let startTime = new Date(); - var maxAllowedTime = new Date(startTime.getTime() + minutesToAdd * 60000); - let flagValidationPending = true; - let zipInvalid = false; - let zipFailureReason = ""; - // TODO(harshanb): Remove this check once the feature flag is enabled by default. - let isTestScriptFragmentEnabled = yield FeatureFlagService_1.FeatureFlagService.isFeatureEnabledAsync(FeatureFlags_1.FeatureFlags.enableTestScriptFragments, baseURL); - let zipValidationTerminateStates = ["VALIDATION_SUCCESS", "VALIDATION_FAILURE"]; - if (isTestScriptFragmentEnabled) { - // NOT_VALIDATED is a terminal state for the file validation and actual validation will be performed during test script upload - zipValidationTerminateStates.push("NOT_VALIDATED"); - } - while (maxAllowedTime > (new Date()) && flagValidationPending) { - var urlSuffix = "tests/" + testId + "?api-version=" + util.apiConstants.latestVersion; - urlSuffix = baseURL + urlSuffix; - let header = yield map.getTestHeader(); - let testResult = yield util.httpClientRetries(urlSuffix, header, 'get', 3, ""); - let testObj = yield util.getResultObj(testResult); - if (testResult.message.statusCode != 200 && testResult.message.statusCode != 201) { - console.log(testObj ? testObj : util.ErrorCorrection(testResult)); - throw new Error("Error in getting the test."); - } - flagValidationPending = false; - if (testObj && testObj.inputArtifacts && testObj.inputArtifacts.additionalFileInfo) { - for (const file of testObj.inputArtifacts.additionalFileInfo) { - if (file.fileType == FileType.ZIPPED_ARTIFACTS && (file.validationStatus in zipValidationTerminateStates)) { - flagValidationPending = true; - break; - } - else if (file.fileType == FileType.ZIPPED_ARTIFACTS && file.validationStatus == "VALIDATION_FAILURE") { - zipInvalid = true; - zipFailureReason = (_a = file.validationFailureDetails) !== null && _a !== void 0 ? _a : "Validation failed for the zip artifact with an unknown error."; - break; - } - } - } - else { - break; - } - if (zipInvalid) { - break; - } - yield util.sleep(3000); - } - if (zipInvalid) { - throw new Error(`Validation of one or more zip artifacts failed with Error : "${zipFailureReason}".`); - } - else if (flagValidationPending) { - throw new Error("Validation of one or more zip artifacts timed out. Please retry."); - } - if (isTestScriptFragmentEnabled) { - console.log(`Uploaded ${zipFiles.length} zip artifact(s) for the test successfully.`); - } - else { - console.log(`Uploaded and validated ${zipFiles.length} zip artifact(s) for the test successfully.`); - } - } - var statuscode = yield uploadPropertyFile(); - if (statuscode == 201) { - yield uploadTestPlan(); - } - }); -} -function uploadPropertyFile() { - return __awaiter(this, void 0, void 0, function* () { - let propertyFile = map.getPropertyFile(); - if (propertyFile != undefined) { - let filename = map.getFileName(propertyFile); - var urlSuffix = "tests/" + testId + "/files/" + filename + "?api-version=" + util.apiConstants.latestVersion + "&fileType=" + FileType.USER_PROPERTIES; - urlSuffix = baseURL + urlSuffix; - let headers = yield map.UploadAndValidateHeader(); - let uploadresult = yield util.httpClientRetries(urlSuffix, headers, 'put', 3, propertyFile); - if (uploadresult.message.statusCode != 201) { - let uploadObj = yield util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading TestPlan for the created test"); - } - console.log(`Uploaded user properties file for the test successfully.`); - } - return 201; - }); -} -function createTestRun() { - return __awaiter(this, void 0, void 0, function* () { - const tenantId = map.getTenantId(); - const testRunId = util.getUniqueId(); - var urlSuffix = "test-runs/" + testRunId + "?tenantId=" + tenantId + "&api-version=" + util.apiConstants.latestVersion; - urlSuffix = baseURL + urlSuffix; - const ltres = core.getInput('loadTestResource'); - const runDisplayName = core.getInput('loadTestRunName'); - const runDescription = core.getInput('loadTestRunDescription'); - const subName = yield map.getSubName(); - try { - var startData = map.startTestData(testRunId, runDisplayName, runDescription); - console.log("Creating and running a testRun for the test"); - let header = yield map.createTestHeader(); - let startTestresult = yield util.httpClientRetries(urlSuffix, header, 'patch', 3, JSON.stringify(startData)); - let testRunDao = yield util.getResultObj(startTestresult); - if (startTestresult.message.statusCode != 200 && startTestresult.message.statusCode != 201) { - console.log(testRunDao ? testRunDao : util.ErrorCorrection(startTestresult)); - throw new Error("Error in running the test"); - } - let startTime = new Date(); - let testRunName = testRunDao.displayName; - let status = testRunDao.status; - if (status == "ACCEPTED") { - console.log("\nView the load test run in Azure portal by following the steps:"); - console.log("1. Go to your Azure Load Testing resource '" + ltres + "' in subscription '" + subName + "'"); - console.log("2. On the Tests page, go to test '" + testId + "'"); - console.log("3. Go to test run '" + testRunName + "'\n"); - yield getTestRunAPI(testRunId, status, startTime); - } - } - catch (err) { - if (!err.message) - err.message = "Error in running the test"; - throw new Error(err.message); - } - }); -} -function getTestRunAPI(testRunId, testStatus, startTime) { - return __awaiter(this, void 0, void 0, function* () { - var urlSuffix = "test-runs/" + testRunId + "?api-version=" + util.apiConstants.latestVersion; - urlSuffix = baseURL + urlSuffix; - while (!util.isTerminalTestStatus(testStatus)) { - let header = yield map.getTestRunHeader(); - let testRunResult = yield util.httpClientRetries(urlSuffix, header, 'get', 3, ""); - let testRunObj = yield util.getResultObj(testRunResult); - if (testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201) { - console.log(testRunObj ? testRunObj : util.ErrorCorrection(testRunResult)); - throw new Error("Error in getting the test run"); - } - testStatus = testRunObj.status; - if (util.isTerminalTestStatus(testStatus)) { - let vusers = null; - let count = 0; - let reportsAvailable = false; - console.log("Test run completed. Polling for statistics and dashboard report to populate."); - // Polling for max 3 min for statistics and pass fail criteria to populate - while ((!reportsAvailable || (0, util_1.isNullOrUndefined)(vusers)) && count < 18) { - yield util.sleep(10000); - let header = yield map.getTestRunHeader(); - let testRunResult = yield util.httpClientRetries(urlSuffix, header, 'get', 3, ""); - testRunObj = yield util.getResultObj(testRunResult); - if (testRunObj == null) { - throw new Error(util.ErrorCorrection(testRunResult)); - } - if (testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201) { - console.log(testRunObj ? testRunObj : util.ErrorCorrection(testRunResult)); - throw new Error("Error in getting the test run"); - } - vusers = testRunObj.virtualUsers; - count++; - let testReport = util.getReportFolder(testRunObj.testArtifacts); - if (testReport) { - reportsAvailable = true; - } - } - if (testRunObj && testRunObj.startDateTime) { - startTime = new Date(testRunObj.startDateTime); - } - let endTime = new Date(); - if (testRunObj && testRunObj.endDateTime) { - endTime = new Date(testRunObj.endDateTime); - } - util.printTestDuration(testRunObj.virtualUsers, startTime, endTime, testStatus); - if (!(0, util_1.isNullOrUndefined)(testRunObj.passFailCriteria) && !(0, util_1.isNullOrUndefined)(testRunObj.passFailCriteria.passFailMetrics)) - util.printCriteria(testRunObj.passFailCriteria.passFailMetrics); - if (testRunObj.testRunStatistics != null) - util.printClientMetrics(testRunObj.testRunStatistics); - let testResultUrl = util.getResultFolder(testRunObj.testArtifacts); - if (testResultUrl != null) { - const response = yield util.httpClientRetries(testResultUrl, {}, 'get', 3, ""); - if (response.message.statusCode != 200) { - let respObj = yield util.getResultObj(response); - console.log(respObj ? respObj : util.ErrorCorrection(response)); - throw new Error("Error in fetching results "); - } - else { - yield util.uploadFileToResultsFolder(response, resultZipFileName); - } - } - let testReportUrl = util.getReportFolder(testRunObj.testArtifacts); - if (testReportUrl != null) { - const response = yield util.httpClientRetries(testReportUrl, {}, 'get', 3, ""); - if (response.message.statusCode != 200) { - let respObj = yield util.getResultObj(response); - console.log(respObj ? respObj : util.ErrorCorrection(response)); - throw new Error("Error in fetching report "); - } - else { - yield util.uploadFileToResultsFolder(response, reportZipFileName); - } - } - if (testRunObj.status === "FAILED" || testRunObj.status === "CANCELLED") { - console.log("Please go to the Portal for more error details: " + testRunObj.portalUrl); - core.setFailed("TestStatus: " + testRunObj.status); - return; - } - if (testRunObj.testResult === "FAILED" || testRunObj.testResult === "CANCELLED") { - core.setFailed("TestResult: " + testRunObj.testResult); - return; - } - return; - } - else { - if (!util.isTerminalTestStatus(testStatus)) { - if (testStatus === "DEPROVISIONING" || testStatus === "DEPROVISIONED" || testStatus != "EXECUTED") - yield util.sleep(5000); - else - yield util.sleep(20000); - } - } - } - }); -} -function getLoadTestResource() { - return __awaiter(this, void 0, void 0, function* () { - let id = map.getResourceId(); - let armUrl = map.getARMEndpoint(); - let armEndpointSuffix = id + "?api-version=" + util.apiConstants.cp2022Version; - let armEndpoint = new URL(armEndpointSuffix, armUrl); - var header = map.armTokenHeader(); - let response = yield util.httpClientRetries(armEndpoint.toString(), header, 'get', 3, ""); - var resource_name = core.getInput('loadTestResource'); - if (response.message.statusCode == 404) { - var message = "The Azure Load Testing resource " + resource_name + " does not exist. Please provide an existing resource."; - throw new Error(message); - } - let respObj = yield util.getResultObj(response); - if (response.message.statusCode != 200) { - console.log(respObj ? respObj : util.ErrorCorrection(response)); - throw new Error("Error fetching resource " + resource_name); - } - let dataPlaneUrl = respObj.properties.dataPlaneURI; - baseURL = 'https://' + dataPlaneUrl + '/'; - }); -} -function getExistingCriteria() { - return existingCriteria; -} -exports.getExistingCriteria = getExistingCriteria; -function getExistingParams() { - return existingParams; -} -exports.getExistingParams = getExistingParams; -function getExistingEnv() { - return existingEnv; -} -exports.getExistingEnv = getExistingEnv; run(); diff --git a/lib/mappers.js b/lib/mappers.js deleted file mode 100644 index ec216cdc..00000000 --- a/lib/mappers.js +++ /dev/null @@ -1,675 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getARMEndpoint = exports.getTenantId = exports.getFileName = exports.getTestId = exports.getZipFiles = exports.getConfigFiles = exports.getPropertyFile = exports.getTestFile = exports.getYamlPath = exports.getTestKind = exports.getDefaultTestRunName = exports.getDefaultTestName = exports.getSubName = exports.getInputParams = exports.getResourceId = exports.getTestHeader = exports.getTestRunHeader = exports.startTestData = exports.armTokenHeader = exports.UploadAndValidateHeader = exports.createTestHeader = exports.createTestData = exports.getToken = void 0; -const core = __importStar(require("@actions/core")); -const yaml = require("js-yaml"); -const jwt_decode = __importStar(require("jwt-decode")); -const fs = __importStar(require("fs")); -const child_process_1 = require("child_process"); -const util = __importStar(require("./util")); -const index = __importStar(require("./main")); -const util_1 = require("util"); -const pathLib = require("path"); -const { Readable } = require("stream"); -const TestKind_1 = require("./engine/TestKind"); -const EngineUtil = __importStar(require("./engine/Util")); -var testId = ""; -var displayName = ""; -var testdesc = "SampleTest"; -var engineInstances = "1"; -var testPlan = ""; -var propertyFile = null; -var configFiles = []; -var zipFiles = []; -var token = ""; -var resourceId = ""; -var subscriptionID = ""; -var environment = "AzureCloud"; -var armTokenScope = "https://management.core.windows.net"; -var dataPlaneTokenScope = "https://loadtest.azure-dev.com"; -var armEndpoint = "https://management.azure.com"; -var tenantId = ""; -var yamlFile = ""; -var passFailCriteria = []; -var regionalLoadTestConfig = null; -var autoStop = null; -var kvRefId = null; -var kvRefType = null; -var subnetId = null; -var splitCSVs = null; -var certificate = null; -let kind; -let publicIPDisabled = false; -; -var paramType; -(function (paramType) { - paramType["env"] = "env"; - paramType["secrets"] = "secrets"; - paramType["cert"] = "cert"; -})(paramType || (paramType = {})); -let failCriteria = {}; -let secretsYaml = {}; -let secretsRun = {}; -let envYaml = {}; -let envRun = {}; -let failureCriteriaValue = {}; -function getExistingData() { - var existingCriteria = index.getExistingCriteria(); - var existingCriteriaIds = Object.keys(existingCriteria); - getFailureCriteria(existingCriteriaIds); - var existingParams = index.getExistingParams(); - for (var key in existingParams) { - if (!secretsYaml.hasOwnProperty(key)) - secretsYaml[key] = null; - } - var existingEnv = index.getExistingEnv(); - for (var key in existingEnv) { - if (!envYaml.hasOwnProperty(key)) - envYaml[key] = null; - } -} -function getToken() { - return token; -} -exports.getToken = getToken; -function createTestData() { - getExistingData(); - var data = { - testId: testId, - description: testdesc, - displayName: displayName, - quickStartTest: false, // always quick test will be false because GH-actions doesnot support it now. - loadTestConfiguration: { - engineInstances: engineInstances, - splitAllCSVs: splitCSVs, - optionalLoadTestConfig: null, - regionalLoadTestConfig: regionalLoadTestConfig, - }, - secrets: secretsYaml, - kind: kind, - certificate: certificate, - environmentVariables: envYaml, - passFailCriteria: { - passFailMetrics: failCriteria, - }, - autoStopCriteria: autoStop, - subnetId: subnetId, - publicIPDisabled: publicIPDisabled, - keyvaultReferenceIdentityType: kvRefType, - keyvaultReferenceIdentityId: kvRefId, - }; - return data; -} -exports.createTestData = createTestData; -function createTestHeader() { - return __awaiter(this, void 0, void 0, function* () { - let headers = { - "content-type": "application/merge-patch+json", - Authorization: "Bearer " + token, - }; - return headers; - }); -} -exports.createTestHeader = createTestHeader; -function UploadAndValidateHeader() { - return __awaiter(this, void 0, void 0, function* () { - let headers = { - Authorization: "Bearer " + token, - "content-type": "application/octet-stream", - }; - return headers; - }); -} -exports.UploadAndValidateHeader = UploadAndValidateHeader; -function armTokenHeader() { - let headers = { - Authorization: "Bearer " + token, - }; - return headers; -} -exports.armTokenHeader = armTokenHeader; -function startTestData(testRunName, runDisplayName, runDescription) { - var data = { - testRunId: testRunName, - displayName: runDisplayName ? runDisplayName : getDefaultTestRunName(), - description: runDescription - ? runDescription - : "Started using GitHub Actions", - testId: testId, - secrets: secretsRun, - environmentVariables: envRun, - }; - return data; -} -exports.startTestData = startTestData; -function getTestRunHeader() { - return __awaiter(this, void 0, void 0, function* () { - if (!isExpired()) { - yield getAccessToken(dataPlaneTokenScope); - } - let headers = { - "content-type": "application/json", - Authorization: "Bearer " + token, - }; - return headers; - }); -} -exports.getTestRunHeader = getTestRunHeader; -function isExpired() { - const header = jwt_decode.jwtDecode(token); - const now = Math.floor(Date.now() / 1000); - return header && header.exp && header.exp > now; -} -function getTestHeader() { - return __awaiter(this, void 0, void 0, function* () { - yield getAccessToken(dataPlaneTokenScope); - let headers = { - "content-type": "application/json", - Authorization: "Bearer " + token, - }; - return headers; - }); -} -exports.getTestHeader = getTestHeader; -function getResourceId() { - const rg = core.getInput("resourceGroup"); - const ltres = core.getInput("loadTestResource"); - if ((0, util_1.isNullOrUndefined)(rg) || rg == '') { - throw new Error(`The input field "resourceGroup" is empty. Provide an existing resource group name.`); - } - if ((0, util_1.isNullOrUndefined)(ltres) || ltres == '') { - throw new Error(`The input field "loadTestResource" is empty. Provide an existing load test resource name.`); - } - resourceId = "/subscriptions/" + subscriptionID + "/resourcegroups/" + rg + "/providers/microsoft.loadtestservice/loadtests/" + ltres; - return resourceId; -} -exports.getResourceId = getResourceId; -function getInputParams() { - return __awaiter(this, void 0, void 0, function* () { - var _a, _b, _c, _d; - yield setEndpointAndScope(); - yield getAccessToken(armTokenScope); - yamlFile = core.getInput("loadTestConfigFile"); - if ((0, util_1.isNullOrUndefined)(yamlFile) || yamlFile == '') { - throw new Error(`The input field "loadTestConfigFile" is empty. Provide the path to load test yaml file.`); - } - if (!(pathLib.extname(yamlFile) === ".yaml" || - pathLib.extname(yamlFile) === ".yml")) - throw new Error("The Load Test configuration file should be of type .yaml or .yml"); - const config = yaml.load(fs.readFileSync(yamlFile, "utf8")); - let validConfig = util.checkValidityYaml(config); - if (!validConfig.valid) { - throw new Error(validConfig.error + ` Refer to the load test YAML syntax at https://learn.microsoft.com/azure/load-testing/reference-test-config-yaml`); - } - testId = (_a = config.testId) !== null && _a !== void 0 ? _a : config.testName; - testId = testId.toLowerCase(); - displayName = (_b = config.displayName) !== null && _b !== void 0 ? _b : testId; - testdesc = config.description; - engineInstances = (_c = config.engineInstances) !== null && _c !== void 0 ? _c : 1; - let path = pathLib.dirname(yamlFile); - testPlan = pathLib.join(path, config.testPlan); - kind = (_d = config.testType) !== null && _d !== void 0 ? _d : TestKind_1.TestKind.JMX; - let framework = EngineUtil.getLoadTestFrameworkModelFromKind(kind); - if (config.configurationFiles != null) { - let tempconfigFiles = []; - tempconfigFiles = config.configurationFiles; - for (let file of tempconfigFiles) { - if (kind == TestKind_1.TestKind.URL && !util.checkFileType(file, 'csv')) { - throw new Error("Only CSV files are allowed as configuration files for a URL-based test."); - } - file = pathLib.join(path, file); - configFiles.push(file); - } - } - if (config.zipArtifacts != undefined) { - let tempZipFiles = []; - tempZipFiles = config.zipArtifacts; - if (kind == TestKind_1.TestKind.URL && tempZipFiles.length > 0) { - throw new Error("Zip artifacts are not supported for the URL-based test."); - } - for (let file of tempZipFiles) { - file = pathLib.join(path, file); - zipFiles.push(file); - } - ; - } - if (config.splitAllCSVs != undefined) { - splitCSVs = config.splitAllCSVs; - } - if (config.failureCriteria != undefined) { - passFailCriteria = config.failureCriteria; - getPassFailCriteria(); - } - if (config.subnetId != undefined) { - subnetId = config.subnetId; - } - if (config.publicIPDisabled != undefined) { - publicIPDisabled = (config.publicIPDisabled); - } - if (config.properties != undefined && config.properties.userPropertyFile != undefined) { - if (kind == TestKind_1.TestKind.URL) { - throw new Error("User property file is not supported for the URL-based test."); - } - if (!util.checkFileTypes(config.properties.userPropertyFile, framework.userPropertyFileExtensions)) { - throw new Error(`User property file with extension other than ${framework.ClientResources.userPropertyFileExtensionsFriendly} is not permitted.`); - } - var propFile = config.properties.userPropertyFile; - propertyFile = pathLib.join(path, propFile); - } - if (config.secrets != undefined) { - kvRefType = "SystemAssigned"; - getParameters(config.secrets, paramType.secrets); - } - if (config.env != undefined) { - getParameters(config.env, paramType.env); - } - if (config.certificates != undefined) { - getParameters(config.certificates, paramType.cert); - } - if (config.autoStop != undefined) { - getAutoStopCriteria(config.autoStop); - } - if (config.keyVaultReferenceIdentity != undefined) { - kvRefType = "UserAssigned"; - kvRefId = config.keyVaultReferenceIdentity; - } - if (config.regionalLoadTestConfig != undefined) { - regionalLoadTestConfig = getMultiRegionLoadTestConfig(config.regionalLoadTestConfig); - } - getRunTimeParams(); - validateTestRunParams(); - if (testId === "" || - (0, util_1.isNullOrUndefined)(testId) || - testPlan === "" || - (0, util_1.isNullOrUndefined)(testPlan)) { - throw new Error("The required fields testName/testPlan are missing in " + yamlFile + "."); - } - }); -} -exports.getInputParams = getInputParams; -function getSubName() { - return __awaiter(this, void 0, void 0, function* () { - try { - const cmdArguments = ["account", "show"]; - var result = yield execAz(cmdArguments); - let name = result.name; - return name; - } - catch (err) { - const message = `An error occurred while getting credentials from ` + - `Azure CLI for getting subscription name: ${err.message}`; - throw new Error(message); - } - }); -} -exports.getSubName = getSubName; -function getAccessToken(aud) { - return __awaiter(this, void 0, void 0, function* () { - try { - const cmdArguments = ["account", "get-access-token", "--resource"]; - cmdArguments.push(aud); - var result = yield execAz(cmdArguments); - token = result.accessToken; - subscriptionID = result.subscription; - tenantId = result.tenant; - return token; - } - catch (err) { - const message = `An error occurred while getting credentials from ` + - `Azure CLI for getting access token: ${err.message}`; - throw new Error(message); - } - }); -} -function setEndpointAndScope() { - return __awaiter(this, void 0, void 0, function* () { - try { - const cmdArguments = ["cloud", "show"]; - var result = yield execAz(cmdArguments); - let env = result ? result.name : null; - environment = env ? env : environment; - let endpointUrl = (result && result.endpoints) ? result.endpoints.resourceManager : null; - armEndpoint = endpointUrl ? endpointUrl : armEndpoint; - if (environment == 'AzureUSGovernment') { - dataPlaneTokenScope = 'https://cnt-prod.loadtesting.azure.us'; - armTokenScope = 'https://management.usgovcloudapi.net'; - } - } - catch (err) { - const message = `An error occurred while getting credentials from ` + - `Azure CLI for setting endPoint and scope: ${err.message}`; - throw new Error(message); - } - }); -} -function execAz(cmdArguments) { - return __awaiter(this, void 0, void 0, function* () { - const azCmd = process.platform === "win32" ? "az.cmd" : "az"; - return new Promise((resolve, reject) => { - (0, child_process_1.execFile)(azCmd, [...cmdArguments, "--out", "json"], { encoding: "utf8", shell: true }, (error, stdout) => { - if (error) { - return reject(error); - } - try { - return resolve(JSON.parse(stdout)); - } - catch (err) { - const msg = `An error occurred while parsing the output "${stdout}", of ` + - `the cmd az "${cmdArguments}": ${err.nmessage}.`; - return reject(new Error(msg)); - } - }); - }); - }); -} -function getDefaultTestName() { - const a = new Date(Date.now()).toLocaleString(); - const b = a.split(", "); - const c = a.split(" "); - return "Test_" + b[0] + "_" + c[1] + c[2]; -} -exports.getDefaultTestName = getDefaultTestName; -function getDefaultTestRunName() { - const a = new Date(Date.now()).toLocaleString(); - const b = a.split(", "); - const c = a.split(" "); - return "TestRun_" + b[0] + "_" + c[1] + c[2]; -} -exports.getDefaultTestRunName = getDefaultTestRunName; -function getParameters(obj, type) { - if (type == paramType.secrets) { - for (var index in obj) { - var val = obj[index]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid secret name at ${str}`); - } - if (!validateUrl(val.value)) { - throw new Error(`Invalid secret url at ${str}`); - } - secretsYaml[val.name] = { type: "AKV_SECRET_URI", value: val.value }; - } - } - else if (type == paramType.env) { - for (var index in obj) { - var val = obj[index]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid environment name at ${str}`); - } - envYaml[val.name] = val.value; - } - } - else if (type == paramType.cert) { - for (var index in obj) { - var val = obj[index]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid certificate name at ${str}`); - } - if (!validateUrl(val.value)) - throw new Error(`Invalid certificate url at ${str}`); - certificate = { name: val.name, type: "AKV_CERT_URI", value: val.value }; - break; - } - } -} -function validateUrl(url) { - //var r = new RegExp(/(http|https):\/\/.*\/secrets\/[/a-zA-Z0-9]+$/); - var pattern = /https:\/\/+[a-zA-Z0-9_-]+\.+(?:vault|vault-int)+\.+(?:azure|azure-int|usgovcloudapi|microsoftazure)+\.+(?:net|cn|de)+\/+(?:secrets|certificates|keys|storage)+\/+[a-zA-Z0-9_-]+\/+|[a-zA-Z0-9]+$/; - var r = new RegExp(pattern); - return r.test(url); -} -function getRunTimeParams() { - var secretRun = core.getInput("secrets"); - if (secretRun != "") { - try { - var obj = JSON.parse(secretRun); - for (var index in obj) { - var val = obj[index]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid secret name at pipeline params at ${str}`); - } - secretsRun[val.name] = { type: "SECRET_VALUE", value: val.value }; - } - } - catch (error) { - console.log(error); - throw new Error("Invalid format of secrets in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); - } - } - var eRun = core.getInput("env"); - if (eRun != "") { - try { - var obj = JSON.parse(eRun); - for (var index in obj) { - var val = obj[index]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid environment name at pipeline params at ${str}`); - } - envRun[val.name] = val.value; - } - } - catch (error) { - console.log(error); - throw new Error("Invalid format of env in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); - } - } -} -function validateTestRunParams() { - let runDisplayName = core.getInput("loadTestRunName"); - let runDescription = core.getInput("loadTestRunDescription"); - if (runDisplayName && util.invalidDisplayName(runDisplayName)) - throw new Error("Invalid test run name. Test run name must be between 2 to 50 characters."); - if (runDescription && util.invalidDescription(runDescription)) - throw new Error("Invalid test run description. Test run description must be less than 100 characters."); -} -function getTestKind() { - return kind; -} -exports.getTestKind = getTestKind; -function getYamlPath() { - return yamlFile; -} -exports.getYamlPath = getYamlPath; -function getTestFile() { - return testPlan; -} -exports.getTestFile = getTestFile; -function getPropertyFile() { - return propertyFile; -} -exports.getPropertyFile = getPropertyFile; -function getConfigFiles() { - return configFiles; -} -exports.getConfigFiles = getConfigFiles; -function getZipFiles() { - return zipFiles; -} -exports.getZipFiles = getZipFiles; -function getTestId() { - return testId; -} -exports.getTestId = getTestId; -function getFileName(filepath) { - var filename = pathLib.basename(filepath); - return filename; -} -exports.getFileName = getFileName; -function getTenantId() { - return tenantId; -} -exports.getTenantId = getTenantId; -function getARMEndpoint() { - return armEndpoint; -} -exports.getARMEndpoint = getARMEndpoint; -function getPassFailCriteria() { - passFailCriteria.forEach((criteria) => { - let data = { - aggregate: "", - clientMetric: "", - condition: "", - value: "", - requestName: "", - action: "", - }; - if (typeof criteria !== "string") { - var request = Object.keys(criteria)[0]; - data.requestName = request; - criteria = criteria[request]; - } - let tempStr = ""; - for (let i = 0; i < criteria.length; i++) { - if (criteria[i] == "(") { - data.aggregate = tempStr.trim(); - tempStr = ""; - } - else if (criteria[i] == ")") { - data.clientMetric = tempStr; - tempStr = ""; - } - else if (criteria[i] == ",") { - data.condition = tempStr - .substring(0, util.indexOfFirstDigit(tempStr)) - .trim(); - data.value = tempStr.substr(util.indexOfFirstDigit(tempStr)).trim(); - tempStr = ""; - } - else { - tempStr += criteria[i]; - } - } - if (criteria.indexOf(",") != -1) { - data.action = tempStr.trim(); - } - else { - data.condition = tempStr - .substring(0, util.indexOfFirstDigit(tempStr)) - .trim(); - data.value = tempStr.substr(util.indexOfFirstDigit(tempStr)).trim(); - } - ValidateAndAddCriteria(data); - }); -} -function ValidateAndAddCriteria(data) { - if (data.action == "") - data.action = "continue"; - data.value = util.removeUnits(data.value); - if (!util.validCriteria(data)) - throw new Error("Invalid Failure Criteria"); - var key = data.clientMetric + - " " + - data.aggregate + - " " + - data.condition + - " " + - data.action; - if (data.requestName != "") { - key = key + " " + data.requestName; - } - var val = parseInt(data.value); - var currVal = val; - if (failureCriteriaValue.hasOwnProperty(key)) - currVal = failureCriteriaValue[key]; - if (data.condition == ">") { - failureCriteriaValue[key] = val < currVal ? val : currVal; - } - else { - failureCriteriaValue[key] = val > currVal ? val : currVal; - } -} -function getFailureCriteria(existingCriteriaIds) { - var numberOfExistingCriteria = existingCriteriaIds.length; - var index = 0; - for (var key in failureCriteriaValue) { - var splitted = key.split(" "); - var criteriaId = index < numberOfExistingCriteria - ? existingCriteriaIds[index++] - : util.getUniqueId(); - failCriteria[criteriaId] = { - clientMetric: splitted[0], - aggregate: splitted[1], - condition: splitted[2], - action: splitted[3], - value: failureCriteriaValue[key], - requestName: splitted.length > 4 ? splitted.slice(4).join(" ") : null, - }; - } - for (; index < numberOfExistingCriteria; index++) { - failCriteria[existingCriteriaIds[index]] = null; - } -} -function getAutoStopCriteria(autoStopInput) { - if (autoStopInput == null) { - autoStop = null; - return; - } - if (typeof autoStopInput == "string") { - if (autoStopInput == "disable") { - let data = { - autoStopDisabled: true, - errorRate: 0, - errorRateTimeWindowInSeconds: 60, - }; - autoStop = data; - } - else { - throw new Error("Invalid value, for disabling auto stop use 'autoStop: disable'"); - } - } - else { - let data = { - autoStopDisabled: false, - errorRate: autoStopInput.errorPercentage, - errorRateTimeWindowInSeconds: autoStopInput.timeWindow, - }; - autoStop = data; - } -} -function getMultiRegionLoadTestConfig(multiRegionalConfig) { - let parsedMultiRegionConfiguration = []; - multiRegionalConfig.forEach(regionConfig => { - let data = { - region: regionConfig.region, - engineInstances: regionConfig.engineInstances, - }; - parsedMultiRegionConfiguration.push(data); - }); - return parsedMultiRegionConfiguration; -} diff --git a/lib/models/APISupport.js b/lib/models/APISupport.js new file mode 100644 index 00000000..0c3b235f --- /dev/null +++ b/lib/models/APISupport.js @@ -0,0 +1,462 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.APISupport = void 0; +const util_1 = require("util"); +const UtilModels_1 = require("./UtilModels"); +const TestKind_1 = require("./engine/TestKind"); +const Util = __importStar(require("./util")); +const FileUtils = __importStar(require("./FileUtils")); +const core = __importStar(require("@actions/core")); +const FetchUtil = __importStar(require("./FetchHelper")); +class APISupport { + constructor(authContext, yamlModel) { + this.baseURL = ''; + this.existingParams = { secrets: {}, env: {}, passFailCriteria: {} }; + this.authContext = authContext; + this.yamlModel = yamlModel; + this.testId = this.yamlModel.testId; + } + getResource() { + return __awaiter(this, void 0, void 0, function* () { + let id = this.authContext.resourceId; + let armUrl = this.authContext.armEndpoint; + let armEndpointSuffix = id + "?api-version=" + UtilModels_1.ApiVersionConstants.cp2022Version; + let armEndpoint = new URL(armEndpointSuffix, armUrl); + let header = yield this.authContext.armTokenHeader(); + let response = yield FetchUtil.httpClientRetries(armEndpoint.toString(), header, 'get', 3, ""); + let resource_name = core.getInput('loadTestResource'); + if (response.message.statusCode == 404) { + var message = `The Azure Load Testing resource ${resource_name} does not exist. Please provide an existing resource.`; + throw new Error(message); + } + let respObj = yield Util.getResultObj(response); + if (response.message.statusCode != 200) { + console.log(respObj ? respObj : Util.ErrorCorrection(response)); + throw new Error("Error fetching resource " + resource_name); + } + let dataPlaneUrl = respObj.properties.dataPlaneURI; + this.baseURL = 'https://' + dataPlaneUrl + '/'; + }); + } + getTestAPI(validate_1) { + return __awaiter(this, arguments, void 0, function* (validate, returnTestObj = false) { + var _a, _b, _c, _d; + var urlSuffix = "tests/" + this.testId + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL + urlSuffix; + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.get); + let testResult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'get', 3, ""); + if (testResult.message.statusCode == 401 || testResult.message.statusCode == 403) { + var message = "Service Principal does not have sufficient permissions. Please assign " + + "the Load Test Contributor role to the service principal. Follow the steps listed at " + + "https://docs.microsoft.com/azure/load-testing/tutorial-cicd-github-actions#configure-the-github-actions-workflow-to-run-a-load-test "; + throw new Error(message); + } + if (testResult.message.statusCode != 200 && testResult.message.statusCode != 201) { + if (validate) { // validate is called, then get should not be false, and this validate had retries because of the conflicts in jmx test, so lets not print in the console, instead put this in the error itself. + let testObj = yield Util.getResultObj(testResult); + let err = ((_a = testObj === null || testObj === void 0 ? void 0 : testObj.error) === null || _a === void 0 ? void 0 : _a.message) ? (_b = testObj === null || testObj === void 0 ? void 0 : testObj.error) === null || _b === void 0 ? void 0 : _b.message : Util.ErrorCorrection(testResult); + throw new Error(err); + } + else if (!validate && testResult.message.statusCode != 404) { // if not validate, then its to check if it is edit or create thats all, so it should not throw the error for 404. + let testObj = yield Util.getResultObj(testResult); + console.log(testObj ? testObj : Util.ErrorCorrection(testResult)); + throw new Error("Error in getting the test."); + } + // note : kumarmoh + /// else { + // do nothing if the validate = false and status code is 404, as it is for create test. + // } this is just for comment + } + if (testResult.message.statusCode == 200) { + let testObj = yield Util.getResultObj(testResult); + if (testObj == null) { + throw new Error(Util.ErrorCorrection(testResult)); + } + let inputScriptFileInfo = testObj.kind == TestKind_1.TestKind.URL ? (_c = testObj.inputArtifacts) === null || _c === void 0 ? void 0 : _c.urlTestConfigFileInfo : (_d = testObj.inputArtifacts) === null || _d === void 0 ? void 0 : _d.testScriptFileInfo; + if (validate) { + if (returnTestObj) { + return [inputScriptFileInfo === null || inputScriptFileInfo === void 0 ? void 0 : inputScriptFileInfo.validationStatus, testObj]; + } + return inputScriptFileInfo === null || inputScriptFileInfo === void 0 ? void 0 : inputScriptFileInfo.validationStatus; + } + else { + if (!(0, util_1.isNullOrUndefined)(testObj.passFailCriteria) && !(0, util_1.isNullOrUndefined)(testObj.passFailCriteria.passFailMetrics)) + this.existingParams.passFailCriteria = testObj.passFailCriteria.passFailMetrics; + if (testObj.secrets != null) { + this.existingParams.secrets = testObj.secrets; + } + if (testObj.environmentVariables != null) { + this.existingParams.env = testObj.environmentVariables; + } + } + } + }); + } + deleteFileAPI(filename) { + return __awaiter(this, void 0, void 0, function* () { + var urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL + urlSuffix; + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.delete); + let delFileResult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'del', 3, ""); + if (delFileResult.message.statusCode != 204) { + let delFileObj = yield Util.getResultObj(delFileResult); + let Message = delFileObj ? delFileObj.message : Util.ErrorCorrection(delFileResult); + throw new Error(Message); + } + }); + } + createTestAPI() { + return __awaiter(this, void 0, void 0, function* () { + let urlSuffix = "tests/" + this.testId + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL + urlSuffix; + let createData = this.yamlModel.getCreateTestData(this.existingParams); + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.patch); + let createTestresult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'patch', 3, JSON.stringify(createData)); + if (createTestresult.message.statusCode != 200 && createTestresult.message.statusCode != 201) { + let testRunObj = yield Util.getResultObj(createTestresult); + console.log(testRunObj ? testRunObj : Util.ErrorCorrection(createTestresult)); + throw new Error("Error in creating test: " + this.testId); + } + if (createTestresult.message.statusCode == 201) { + console.log("Creating a new load test " + this.testId); + console.log("Successfully created load test " + this.testId); + } + else { + console.log("Test '" + this.testId + "' already exists"); + // test script will anyway be updated by the ado in later steps, this will be error if the test script is not present in the test. + // this will be error in the url tests when the quick test is getting updated to the url test. so removing this. + let testObj = yield Util.getResultObj(createTestresult); + var testFiles = testObj.inputArtifacts; + if (testFiles.userPropUrl != null) { + console.log(`Deleting the existing UserProperty file.`); + yield this.deleteFileAPI(testFiles.userPropFileInfo.fileName); + } + if (testFiles.testScriptFileInfo != null) { + console.log(`Deleting the existing TestScript file.`); + yield this.deleteFileAPI(testFiles.testScriptFileInfo.fileName); + } + if (testFiles.additionalFileInfo != null) { + // delete existing files which are not present in yaml, the files which are in yaml will anyway be uploaded again. + let existingFiles = []; + let file; + for (file of testFiles.additionalFileInfo) { + existingFiles.push(file.fileName); + } + for (let file of this.yamlModel.configurationFiles) { + file = this.yamlModel.getFileName(file); + let indexOfFile = existingFiles.indexOf(file); + if (indexOfFile != -1) { + existingFiles.splice(indexOfFile, 1); + } + } + for (let file of this.yamlModel.zipArtifacts) { + file = this.yamlModel.getFileName(file); + let indexOfFile = existingFiles.indexOf(file); + if (indexOfFile != -1) { + existingFiles.splice(indexOfFile, 1); + } + } + if (existingFiles.length > 0) { + console.log(`Deleting the ${existingFiles.length} existing test file(s) which is(are) not in the configuration yaml file.`); + } + for (const file of existingFiles) { + yield this.deleteFileAPI(file); + } + } + } + yield this.uploadConfigFile(); + }); + } + uploadTestPlan() { + return __awaiter(this, void 0, void 0, function* () { + let retry = 5; + let filepath = this.yamlModel.testPlan; + let filename = this.yamlModel.getFileName(filepath); + let urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; + let fileType = UtilModels_1.FileType.TEST_SCRIPT; + if (this.yamlModel.kind == TestKind_1.TestKind.URL) { + fileType = UtilModels_1.FileType.URL_TEST_CONFIG; + } + urlSuffix = this.baseURL + urlSuffix + ("&fileType=" + fileType); + let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.put); + let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); + if (uploadresult.message.statusCode != 201) { + let uploadObj = yield Util.getResultObj(uploadresult); + console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + throw new Error("Error in uploading TestPlan for the created test"); + } + else { + console.log("Uploaded test plan for the test"); + let minutesToAdd = 10; + let startTime = new Date(); + let maxAllowedTime = new Date(startTime.getTime() + minutesToAdd * 60000); + let validationStatus = "VALIDATION_INITIATED"; + let testObj = null; + while (maxAllowedTime > (new Date()) && (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED" || validationStatus == null)) { + try { + [validationStatus, testObj] = (yield this.getTestAPI(true, true)); + } + catch (e) { + retry--; + if (retry == 0) { + throw new Error("Unable to validate the test plan. Please retry. Failed with error :" + e); + } + } + yield Util.sleep(5000); + } + console.log("Validation status of the test plan: " + validationStatus); + if (validationStatus == null || validationStatus == "VALIDATION_SUCCESS") { + console.log(`Validated test plan for the test successfully.`); + // Get errors from all files + let fileErrors = Util.getAllFileErrors(testObj); + if (Object.keys(fileErrors).length > 0) { + console.log("Validation failed for the following files:"); + for (const [file, error] of Object.entries(fileErrors)) { + console.log(`File: ${file}, Error: ${error}`); + } + throw new Error("Validation of one or more files failed. Please correct the errors and try again."); + } + yield this.createTestRun(); + } + else if (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED") + throw new Error("TestPlan validation timeout. Please try again."); + else + throw new Error("TestPlan validation Failed."); + } + }); + } + uploadConfigFile() { + return __awaiter(this, void 0, void 0, function* () { + let configFiles = this.yamlModel.configurationFiles; + if (configFiles != undefined && configFiles.length > 0) { + for (let filepath of configFiles) { + let filename = this.yamlModel.getFileName(filepath); + let urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version + ("&fileType=" + UtilModels_1.FileType.ADDITIONAL_ARTIFACTS); + urlSuffix = this.baseURL + urlSuffix; + let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.put); + let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); + if (uploadresult.message.statusCode != 201) { + let uploadObj = yield Util.getResultObj(uploadresult); + console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + throw new Error("Error in uploading config file for the created test"); + } + } + ; + console.log(`Uploaded ${configFiles.length} configuration file(s) for the test successfully.`); + } + yield this.uploadZipArtifacts(); + }); + } + uploadZipArtifacts() { + return __awaiter(this, void 0, void 0, function* () { + let zipFiles = this.yamlModel.zipArtifacts; + if (zipFiles != undefined && zipFiles.length > 0) { + console.log("Uploading and validating the zip artifacts"); + for (const filepath of zipFiles) { + let filename = this.yamlModel.getFileName(filepath); + var urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version + "&fileType=" + UtilModels_1.FileType.ZIPPED_ARTIFACTS; + urlSuffix = this.baseURL + urlSuffix; + let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.put); + let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); + if (uploadresult.message.statusCode != 201) { + let uploadObj = yield Util.getResultObj(uploadresult); + console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + throw new Error("Error in uploading config file for the created test"); + } + } + console.log(`Uploaded ${zipFiles.length} zip artifact(s) for the test successfully.`); + } + let statuscode = yield this.uploadPropertyFile(); + if (statuscode == 201) + yield this.uploadTestPlan(); + }); + } + uploadPropertyFile() { + return __awaiter(this, void 0, void 0, function* () { + let propertyFile = this.yamlModel.propertyFile; + if (propertyFile != undefined && propertyFile != '') { + let filename = this.yamlModel.getFileName(propertyFile); + let urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version + "&fileType=" + UtilModels_1.FileType.USER_PROPERTIES; + urlSuffix = this.baseURL + urlSuffix; + let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.put); + let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, 'put', 3, propertyFile, true); + if (uploadresult.message.statusCode != 201) { + let uploadObj = yield Util.getResultObj(uploadresult); + console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + throw new Error("Error in uploading TestPlan for the created test"); + } + console.log(`Uploaded user properties file for the test successfully.`); + } + return 201; + }); + } + createTestRun() { + return __awaiter(this, void 0, void 0, function* () { + const testRunId = Util.getUniqueId(); + let urlSuffix = "test-runs/" + testRunId + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL + urlSuffix; + try { + var startData = this.yamlModel.getStartTestData(); + console.log("Creating and running a testRun for the test"); + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.patch); + let startTestresult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'patch', 3, JSON.stringify(startData)); + let testRunDao = yield Util.getResultObj(startTestresult); + if (startTestresult.message.statusCode != 200 && startTestresult.message.statusCode != 201) { + console.log(testRunDao ? testRunDao : Util.ErrorCorrection(startTestresult)); + throw new Error("Error in running the test"); + } + let startTime = new Date(); + let portalUrl = testRunDao.portalUrl; + let status = testRunDao.status; + if (status == "ACCEPTED") { + console.log("View the load test run in progress at: " + portalUrl); + yield this.getTestRunAPI(testRunId, status, startTime); + } + } + catch (err) { + if (!err.message) + err.message = "Error in running the test"; + throw new Error(err.message); + } + }); + } + getTestRunAPI(testRunId, testStatus, startTime) { + return __awaiter(this, void 0, void 0, function* () { + let urlSuffix = "test-runs/" + testRunId + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL + urlSuffix; + while (!Util.isTerminalTestStatus(testStatus)) { + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.get); + let testRunResult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'get', 3, ""); + let testRunObj = yield Util.getResultObj(testRunResult); + if (testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201) { + console.log(testRunObj ? testRunObj : Util.ErrorCorrection(testRunResult)); + throw new Error("Error in getting the test run"); + } + testStatus = testRunObj.status; + if (Util.isTerminalTestStatus(testStatus)) { + let vusers = null; + let count = 0; + let reportsAvailable = false; + console.log("Test run completed. Polling for statistics and dashboard report to populate."); + // Polling for max 3 min for statistics and pass fail criteria to populate + while ((!reportsAvailable || (0, util_1.isNullOrUndefined)(vusers)) && count < 18) { + yield Util.sleep(10000); + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.get); + let testRunResult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'get', 3, ""); + testRunObj = yield Util.getResultObj(testRunResult); + if (testRunObj == null) { + throw new Error(Util.ErrorCorrection(testRunResult)); + } + if (testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201) { + console.log(testRunResult ? testRunResult : Util.ErrorCorrection(testRunResult)); + throw new Error("Error in getting the test run"); + } + vusers = testRunObj.virtualUsers; + count++; + let testReport = Util.getReportFolder(testRunObj.testArtifacts); + if (testReport) { + reportsAvailable = true; + } + } + if (testRunObj && testRunObj.startDateTime) { + startTime = new Date(testRunObj.startDateTime); + } + let endTime = new Date(); + if (testRunObj && testRunObj.endDateTime) { + endTime = new Date(testRunObj.endDateTime); + } + Util.printTestDuration(testRunObj); + if (!(0, util_1.isNullOrUndefined)(testRunObj.passFailCriteria) && !(0, util_1.isNullOrUndefined)(testRunObj.passFailCriteria.passFailMetrics)) + Util.printCriteria(testRunObj.passFailCriteria.passFailMetrics); + if (testRunObj.testRunStatistics != null && testRunObj.testRunStatistics != undefined) + Util.printClientMetrics(testRunObj.testRunStatistics); + let testResultUrl = Util.getResultFolder(testRunObj.testArtifacts); + if (testResultUrl != null) { + const response = yield FetchUtil.httpClientRetries(testResultUrl, {}, 'get', 3, ""); + if (response.message.statusCode != 200) { + let respObj = yield Util.getResultObj(response); + console.log(respObj ? respObj : Util.ErrorCorrection(response)); + throw new Error("Error in fetching results "); + } + else { + yield FileUtils.uploadFileToResultsFolder(response, UtilModels_1.resultZipFileName); + } + } + let testReportUrl = Util.getReportFolder(testRunObj.testArtifacts); + if (testReportUrl != null) { + const response = yield FetchUtil.httpClientRetries(testReportUrl, {}, 'get', 3, ""); + if (response.message.statusCode != 200) { + let respObj = yield Util.getResultObj(response); + console.log(respObj ? respObj : Util.ErrorCorrection(response)); + throw new Error("Error in fetching report "); + } + else { + yield FileUtils.uploadFileToResultsFolder(response, UtilModels_1.reportZipFileName); + } + } + if (!(0, util_1.isNull)(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { + core.setFailed("TestResult: " + testRunObj.testResult); + return; + } + if (!(0, util_1.isNull)(testRunObj.status) && Util.isStatusFailed(testRunObj.status)) { + console.log("Please go to the Portal for more error details: " + testRunObj.portalUrl); + core.setFailed("TestStatus: " + testRunObj.status); + return; + } + return; + } + else { + if (!Util.isTerminalTestStatus(testStatus)) { + if (testStatus === "DEPROVISIONING" || testStatus === "DEPROVISIONED" || testStatus != "EXECUTED") + yield Util.sleep(5000); + else + yield Util.sleep(20000); + } + } + } + }); + } +} +exports.APISupport = APISupport; diff --git a/lib/models/AuthenticationUtils.js b/lib/models/AuthenticationUtils.js new file mode 100644 index 00000000..e9a04ace --- /dev/null +++ b/lib/models/AuthenticationUtils.js @@ -0,0 +1,175 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AuthenticationUtils = void 0; +const util_1 = require("util"); +const core = __importStar(require("@actions/core")); +const child_process_1 = require("child_process"); +const UtilModels_1 = require("./UtilModels"); +const jwt_decode_1 = require("jwt-decode"); +class AuthenticationUtils { + constructor() { + this.dataPlanetoken = ''; + this.controlPlaneToken = ''; + this.subscriptionId = ''; + this.env = 'AzureCloud'; + this.armTokenScope = 'https://management.core.windows.net'; + this.dataPlaneTokenScope = 'https://loadtest.azure-dev.com'; + this.armEndpoint = 'https://management.azure.com'; + this.resourceId = ''; + } + authorize() { + return __awaiter(this, void 0, void 0, function* () { + // NOTE: This will set the subscription id + yield this.getTokenAPI(UtilModels_1.TokenScope.ControlPlane); + const rg = core.getInput('resourceGroup'); + const ltres = core.getInput('loadTestResource'); + if ((0, util_1.isNullOrUndefined)(rg) || rg == '') { + throw new Error(`The input field "resourceGroup" is empty. Provide an existing resource group name.`); + } + if ((0, util_1.isNullOrUndefined)(ltres) || ltres == '') { + throw new Error(`The input field "loadTestResource" is empty. Provide an existing load test resource name.`); + } + this.resourceId = "/subscriptions/" + this.subscriptionId + "/resourcegroups/" + rg + "/providers/microsoft.loadtestservice/loadtests/" + ltres; + yield this.setEndpointAndScope(); + }); + } + setEndpointAndScope() { + return __awaiter(this, void 0, void 0, function* () { + try { + const cmdArguments = ["cloud", "show"]; + var result = yield this.execAz(cmdArguments); + let env = result ? result.name : null; + this.env = env ? env : this.env; + let endpointUrl = (result && result.endpoints) ? result.endpoints.resourceManager : null; + this.armEndpoint = endpointUrl ? endpointUrl : this.armEndpoint; + if (this.env == 'AzureUSGovernment') { + this.dataPlaneTokenScope = 'https://cnt-prod.loadtesting.azure.us'; + this.armTokenScope = 'https://management.usgovcloudapi.net'; + } + } + catch (err) { + const message = `An error occurred while getting credentials from ` + + `Azure CLI for setting endPoint and scope: ${err.message}`; + throw new Error(message); + } + }); + } + getTokenAPI(scope) { + return __awaiter(this, void 0, void 0, function* () { + let tokenScopeDecoded = scope == UtilModels_1.TokenScope.Dataplane ? this.dataPlaneTokenScope : this.armTokenScope; + try { + const cmdArguments = ["account", "get-access-token", "--resource"]; + cmdArguments.push(tokenScopeDecoded); + var result = yield this.execAz(cmdArguments); + let token = result.accessToken; + // NOTE: Setting the subscription id + this.subscriptionId = result.subscription; + scope == UtilModels_1.TokenScope.ControlPlane ? this.controlPlaneToken = token : this.dataPlanetoken = token; + return token; + } + catch (err) { + const message = `An error occurred while getting credentials from ` + `Azure CLI: ${err.message}`; + throw new Error(message); + } + }); + } + execAz(cmdArguments) { + return __awaiter(this, void 0, void 0, function* () { + const azCmd = process.platform === "win32" ? "az.cmd" : "az"; + return new Promise((resolve, reject) => { + (0, child_process_1.execFile)(azCmd, [...cmdArguments, "--out", "json"], { encoding: "utf8", shell: true }, (error, stdout) => { + if (error) { + return reject(error); + } + try { + return resolve(JSON.parse(stdout)); + } + catch (err) { + const msg = `An error occurred while parsing the output "${stdout}", of ` + + `the cmd az "${cmdArguments}": ${err.message}.`; + return reject(new Error(msg)); + } + }); + }); + }); + } + isValid(scope) { + let token = scope == UtilModels_1.TokenScope.Dataplane ? this.dataPlanetoken : this.controlPlaneToken; + try { + let header = token && (0, jwt_decode_1.jwtDecode)(token); + const now = Math.floor(Date.now() / 1000); + return (header && (header === null || header === void 0 ? void 0 : header.exp) && header.exp + 2 > now); + } + catch (error) { + console.log("Error in getting the token"); + } + } + getDataPlaneHeader(apicallType) { + return __awaiter(this, void 0, void 0, function* () { + var _a; + if (!this.isValid(UtilModels_1.TokenScope.Dataplane)) { + let tokenRes = yield this.getTokenAPI(UtilModels_1.TokenScope.Dataplane); + this.dataPlanetoken = tokenRes; + } + let headers = { + 'content-type': (_a = UtilModels_1.ContentTypeMap[apicallType]) !== null && _a !== void 0 ? _a : 'application/json', + 'Authorization': 'Bearer ' + this.dataPlanetoken + }; + return headers; + }); + } + armTokenHeader() { + return __awaiter(this, void 0, void 0, function* () { + // right now only get calls from the GH, so no need of content type for now for the get calls. + var tokenRes = yield this.getTokenAPI(UtilModels_1.TokenScope.ControlPlane); + this.controlPlaneToken = tokenRes; + let headers = { + 'Authorization': 'Bearer ' + this.controlPlaneToken, + }; + return headers; + }); + } +} +exports.AuthenticationUtils = AuthenticationUtils; diff --git a/lib/models/FetchHelper.js b/lib/models/FetchHelper.js new file mode 100644 index 00000000..6fba7639 --- /dev/null +++ b/lib/models/FetchHelper.js @@ -0,0 +1,95 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.httpClientRetries = httpClientRetries; +const util_1 = require("./util"); +const UtilModels_1 = require("./UtilModels"); +const httpc = __importStar(require("typed-rest-client/HttpClient")); +const FileUtils_1 = require("./FileUtils"); +const httpClient = new httpc.HttpClient('MALT-GHACTION'); +const core = __importStar(require("@actions/core")); +// (note mohit): shift to the enum later. +function httpClientRetries(urlSuffix_1, header_1, method_1) { + return __awaiter(this, arguments, void 0, function* (urlSuffix, header, method, retries = 1, data, isUploadCall = true, log = true) { + let httpResponse; + try { + let correlationId = `gh-actions-${(0, util_1.getUniqueId)()}`; + header[UtilModels_1.correlationHeader] = correlationId; // even if we put console.debug its printing along with the logs, so lets just go ahead with the differentiation with azdo, so we can search the timeframe for azdo in correlationid and resource filter. + if (method == 'get') { + httpResponse = yield httpClient.get(urlSuffix, header); + } + else if (method == 'del') { + httpResponse = yield httpClient.del(urlSuffix, header); + } + else if (method == 'put' && isUploadCall) { + let fileContent = (0, FileUtils_1.uploadFileData)(data); + httpResponse = yield httpClient.request(method, urlSuffix, fileContent, header); + } + else { + httpResponse = yield httpClient.request(method, urlSuffix, data, header); + } + if (httpResponse.message.statusCode != undefined && httpResponse.message.statusCode >= 300) { + core.debug(`correlation id : ${correlationId}`); + } + if (httpResponse.message.statusCode != undefined && [408, 429, 502, 503, 504].includes(httpResponse.message.statusCode)) { + let err = yield (0, util_1.getResultObj)(httpResponse); + throw { message: (err && err.error && err.error.message) ? err.error.message : (0, util_1.ErrorCorrection)(httpResponse) }; // throwing as message to catch it as err.message + } + return httpResponse; + } + catch (err) { + if (retries) { + let sleeptime = (5 - retries) * 1000 + Math.floor(Math.random() * 5001); + if (log) { + console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime / 1000} seconds`); + } + yield (0, util_1.sleep)(sleeptime); + return yield httpClientRetries(urlSuffix, header, method, retries - 1, data); + } + else { + throw new Error(`Operation did not succeed after 3 retries. Pipeline failed with error : ${err.message}`); + } + } + }); +} diff --git a/lib/models/FileUtils.js b/lib/models/FileUtils.js new file mode 100644 index 00000000..c5d77c4e --- /dev/null +++ b/lib/models/FileUtils.js @@ -0,0 +1,106 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.uploadFileToResultsFolder = uploadFileToResultsFolder; +exports.deleteFile = deleteFile; +exports.uploadFileData = uploadFileData; +const path_1 = __importDefault(require("path")); +const UtilModels_1 = require("./UtilModels"); +const fs = __importStar(require("fs")); +const stream_1 = require("stream"); +function uploadFileToResultsFolder(response_1) { + return __awaiter(this, arguments, void 0, function* (response, fileName = UtilModels_1.resultZipFileName) { + try { + const filePath = path_1.default.join(UtilModels_1.resultFolder, fileName); + const file = fs.createWriteStream(filePath); + return new Promise((resolve, reject) => { + file.on("error", (err) => reject(err)); + const stream = response.message.pipe(file); + stream.on("close", () => { + try { + resolve(filePath); + } + catch (err) { + reject(err); + } + }); + }); + } + catch (err) { + err.message = "Error in fetching the results of the testRun"; + throw new Error(err); + } + }); +} +function deleteFile(foldername) { + if (fs.existsSync(foldername)) { + fs.readdirSync(foldername).forEach((file, index) => { + const curPath = path_1.default.join(foldername, file); + if (fs.lstatSync(curPath).isDirectory()) { + deleteFile(curPath); + } + else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(foldername); + } +} +function uploadFileData(filepath) { + try { + let filedata = fs.readFileSync(filepath); + const readable = new stream_1.Readable(); + readable._read = () => { }; + readable.push(filedata); + readable.push(null); + return readable; + } + catch (err) { + err.message = "File not found " + filepath; + throw new Error(err.message); + } +} diff --git a/lib/models/PayloadModels.js b/lib/models/PayloadModels.js new file mode 100644 index 00000000..b75facdd --- /dev/null +++ b/lib/models/PayloadModels.js @@ -0,0 +1,15 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CertificateMetadata = void 0; +class CertificateMetadata { +} +exports.CertificateMetadata = CertificateMetadata; +; +; +; +; +; +; +; +; +; diff --git a/lib/models/TaskModels.js b/lib/models/TaskModels.js new file mode 100644 index 00000000..fb729106 --- /dev/null +++ b/lib/models/TaskModels.js @@ -0,0 +1,370 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.YamlConfig = void 0; +const util_1 = require("util"); +const pathLib = require('path'); +const Util = __importStar(require("./util")); +const EngineUtil = __importStar(require("./engine/Util")); +const TestKind_1 = require("./engine/TestKind"); +const yaml = require('js-yaml'); +const fs = __importStar(require("fs")); +const UtilModels_1 = require("./UtilModels"); +const core = __importStar(require("@actions/core")); +class YamlConfig { + constructor() { + var _a, _b, _c, _d, _e; + this.testId = ''; + this.displayName = ''; + this.description = ''; + this.testPlan = ''; + this.kind = TestKind_1.TestKind.JMX; + this.engineInstances = 1; + this.publicIPDisabled = false; + this.configurationFiles = []; + this.zipArtifacts = []; + this.splitAllCSVs = false; + this.propertyFile = null; + this.env = {}; + this.certificates = null; + this.secrets = {}; + this.failureCriteria = {}; // this is yaml model. + this.passFailApiModel = {}; // this is api model. + this.autoStop = null; + this.keyVaultReferenceIdentity = null; + this.keyVaultReferenceIdentityType = UtilModels_1.ManagedIdentityType.SystemAssigned; + this.regionalLoadTestConfig = null; + this.runTimeParams = { env: {}, secrets: {}, runDisplayName: '', runDescription: '', testId: '', testRunId: '' }; + let yamlFile = (_a = core.getInput('loadTestConfigFile')) !== null && _a !== void 0 ? _a : ''; + if ((0, util_1.isNullOrUndefined)(yamlFile) || yamlFile == '') { + throw new Error(`The input field "loadTestConfigFile" is empty. Provide the path to load test yaml file.`); + } + let yamlPath = yamlFile; + if (!(pathLib.extname(yamlPath) === ".yaml" || pathLib.extname(yamlPath) === ".yml")) + throw new Error("The Load Test configuration file should be of type .yaml or .yml"); + const config = yaml.load(fs.readFileSync(yamlPath, 'utf8')); + let validConfig = Util.checkValidityYaml(config); + if (!validConfig.valid) { + throw new Error(validConfig.error + ` Refer to the load test YAML syntax at https://learn.microsoft.com/azure/load-testing/reference-test-config-yaml`); + } + this.testId = (_b = config.testId) !== null && _b !== void 0 ? _b : config.testName; + this.testId = this.testId.toLowerCase(); + this.displayName = (_c = config.displayName) !== null && _c !== void 0 ? _c : this.testId; + this.description = config.description; + this.engineInstances = (_d = config.engineInstances) !== null && _d !== void 0 ? _d : 1; + let path = pathLib.dirname(yamlPath); + this.testPlan = pathLib.join(path, config.testPlan); + this.kind = (_e = config.testType) !== null && _e !== void 0 ? _e : TestKind_1.TestKind.JMX; + let framework = EngineUtil.getLoadTestFrameworkModelFromKind(this.kind); + if (config.configurationFiles != undefined) { + var tempconfigFiles = []; + tempconfigFiles = config.configurationFiles; + for (let file of tempconfigFiles) { + if (this.kind == TestKind_1.TestKind.URL && !Util.checkFileType(file, 'csv')) { + throw new Error("Only CSV files are allowed as configuration files for a URL-based test."); + } + file = pathLib.join(path, file); + this.configurationFiles.push(file); + } + ; + } + if (config.zipArtifacts != undefined) { + var tempconfigFiles = []; + tempconfigFiles = config.zipArtifacts; + if (this.kind == TestKind_1.TestKind.URL && tempconfigFiles.length > 0) { + throw new Error("Zip artifacts are not supported for the URL-based test."); + } + for (let file of tempconfigFiles) { + file = pathLib.join(path, file); + this.zipArtifacts.push(file); + } + ; + } + if (config.splitAllCSVs != undefined) { + this.splitAllCSVs = config.splitAllCSVs; + } + if (config.failureCriteria != undefined) { + this.failureCriteria = Util.getPassFailCriteriaFromString(config.failureCriteria); + } + if (config.autoStop != undefined) { + this.autoStop = this.getAutoStopCriteria(config.autoStop); + } + if (config.subnetId != undefined) { + this.subnetId = (config.subnetId); + } + if (config.publicIPDisabled != undefined) { + this.publicIPDisabled = (config.publicIPDisabled); + } + if (config.properties != undefined && config.properties.userPropertyFile != undefined) { + if (this.kind == TestKind_1.TestKind.URL) { + throw new Error("User property file is not supported for the URL-based test."); + } + let propFile = config.properties.userPropertyFile; + this.propertyFile = pathLib.join(path, propFile); + if (!Util.checkFileTypes(config.properties.userPropertyFile, framework.userPropertyFileExtensions)) { + throw new Error(`User property file with extension other than ${framework.ClientResources.userPropertyFileExtensionsFriendly} is not permitted.`); + } + } + if (config.secrets != undefined) { + this.keyVaultReferenceIdentityType = UtilModels_1.ManagedIdentityType.SystemAssigned; + this.secrets = this.parseParameters(config.secrets, UtilModels_1.ParamType.secrets); + } + if (config.env != undefined) { + this.env = this.parseParameters(config.env, UtilModels_1.ParamType.env); + } + if (config.certificates != undefined) { + this.certificates = this.parseParameters(config.certificates, UtilModels_1.ParamType.cert); + } + if (config.keyVaultReferenceIdentity != undefined) { + this.keyVaultReferenceIdentityType = UtilModels_1.ManagedIdentityType.UserAssigned; + this.keyVaultReferenceIdentity = config.keyVaultReferenceIdentity; + } + if (config.regionalLoadTestConfig != undefined) { + this.regionalLoadTestConfig = this.getMultiRegionLoadTestConfig(config.regionalLoadTestConfig); + } + // commenting out for now, will re-write this logic with the changed options. + // if(config.engineBuiltInIdentityType != undefined) { + // engineBuiltInIdentityType = config.engineBuiltInIdentityType; + // } + // if(config.engineBuiltInIdentityIds != undefined) { + // engineBuiltInIdentityIds = config.engineBuiltInIdentityIds; + // } + if (this.testId === '' || (0, util_1.isNullOrUndefined)(this.testId) || this.testPlan === '' || (0, util_1.isNullOrUndefined)(this.testPlan)) { + throw new Error("The required fields testId/testPlan are missing in " + yamlPath + "."); + } + this.runTimeParams = this.getRunTimeParams(); + Util.validateTestRunParamsFromPipeline(this.runTimeParams); + } + getRunTimeParams() { + var _a, _b; + var secretRun = core.getInput('secrets'); + let secretsParsed = {}; + let envParsed = {}; + if (secretRun) { + try { + var obj = JSON.parse(secretRun); + for (var index in obj) { + var val = obj[index]; + let str = `name : ${val.name}, value : ${val.value}`; + if ((0, util_1.isNullOrUndefined)(val.name)) { + throw new Error(`Invalid secret name at pipeline parameters at ${str}`); + } + secretsParsed[val.name] = { type: 'SECRET_VALUE', value: val.value }; + } + } + catch (error) { + console.log(error); + throw new Error("Invalid format of secrets in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); + } + } + var eRun = core.getInput('env'); + if (eRun) { + try { + var obj = JSON.parse(eRun); + for (var index in obj) { + var val = obj[index]; + let str = `name : ${val.name}, value : ${val.value}`; + if ((0, util_1.isNullOrUndefined)(val.name)) { + throw new Error(`Invalid environment name at pipeline parameters at ${str}`); + } + envParsed[val.name] = val.value; + } + } + catch (error) { + console.log(error); + throw new Error("Invalid format of env in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); + } + } + const runDisplayName = (_a = core.getInput('loadTestRunName')) !== null && _a !== void 0 ? _a : Util.getDefaultTestRunName(); + const runDescription = (_b = core.getInput('loadTestRunDescription')) !== null && _b !== void 0 ? _b : Util.getDefaultRunDescription(); + let runTimeParams = { env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: '' }; + return runTimeParams; + } + getFileName(filepath) { + var filename = pathLib.basename(filepath); + return filename; + } + mergeExistingData(existingData) { + let existingCriteria = existingData.passFailCriteria; + let existingCriteriaIds = Object.keys(existingCriteria); + var numberOfExistingCriteria = existingCriteriaIds.length; + var index = 0; + for (var key in this.failureCriteria) { + var splitted = key.split(" "); + var criteriaId = index < numberOfExistingCriteria ? existingCriteriaIds[index++] : Util.getUniqueId(); + this.passFailApiModel[criteriaId] = { + clientMetric: splitted[0], + aggregate: splitted[1], + condition: splitted[2], + action: splitted[3], + value: this.failureCriteria[key], + requestName: splitted.length > 4 ? splitted.slice(4).join(' ') : null + }; + } + for (; index < numberOfExistingCriteria; index++) { + this.passFailApiModel[existingCriteriaIds[index]] = null; + } + let existingParams = existingCriteria.secrets; + for (var key in existingParams) { + if (!this.secrets.hasOwnProperty(key)) + this.secrets[key] = null; + } + var existingEnv = existingCriteria.env; + for (var key in existingEnv) { + if (!this.env.hasOwnProperty(key)) + this.env[key] = null; + } + } + getCreateTestData(existingData) { + this.mergeExistingData(existingData); + var data = { + testId: this.testId, + description: this.description, + displayName: this.displayName, + loadTestConfiguration: { + engineInstances: this.engineInstances, + splitAllCSVs: this.splitAllCSVs, + regionalLoadTestConfig: this.regionalLoadTestConfig, + }, + secrets: this.secrets, + kind: this.kind, + certificate: this.certificates, + environmentVariables: this.env, + passFailCriteria: { + passFailMetrics: this.passFailApiModel + }, + autoStopCriteria: this.autoStop, + subnetId: this.subnetId, + publicIPDisabled: this.publicIPDisabled, + keyvaultReferenceIdentityType: this.keyVaultReferenceIdentityType, + keyvaultReferenceIdentityId: this.keyVaultReferenceIdentity, + }; + return data; + } + getStartTestData() { + this.runTimeParams.testId = this.testId; + this.runTimeParams.testRunId = Util.getUniqueId(); + return this.runTimeParams; + } + getAutoStopCriteria(autoStopInput) { + let autoStop; + if (autoStopInput == null) { + autoStop = null; + return autoStop; + } + if (typeof autoStopInput == "string") { + if (autoStopInput == "disable") { + let data = { + autoStopDisabled: true, + errorRate: 90, + errorRateTimeWindowInSeconds: 60, + }; + autoStop = data; + } + else { + throw new Error("Invalid value, for disabling auto stop use 'autoStop: disable'"); + } + } + else { + let data = { + autoStopDisabled: false, + errorRate: autoStopInput.errorPercentage, + errorRateTimeWindowInSeconds: autoStopInput.timeWindow, + }; + autoStop = data; + } + return autoStop; + } + parseParameters(obj, type) { + if (type == UtilModels_1.ParamType.secrets) { + let secretsParsed = {}; + for (var index in obj) { + var val = obj[index]; + let str = `name : ${val.name}, value : ${val.value}`; + if ((0, util_1.isNullOrUndefined)(val.name)) { + throw new Error(`Invalid secret name at ${str}`); + } + if (!Util.validateUrl(val.value)) { + throw new Error(`Invalid secret url at ${str}`); + } + secretsParsed[val.name] = { type: 'AKV_SECRET_URI', value: val.value }; + } + return secretsParsed; + } + if (type == UtilModels_1.ParamType.env) { + let envParsed = {}; + for (var index in obj) { + let val = obj[index]; + let str = `name : ${val.name}, value : ${val.value}`; + if ((0, util_1.isNullOrUndefined)(val.name)) { + throw new Error(`Invalid environment name at ${str}`); + } + val = obj[index]; + envParsed[val.name] = val.value; + } + return envParsed; + } + if (type == UtilModels_1.ParamType.cert) { + let cert = null; + if (obj.length > 1) { + throw new Error(`Only one certificate can be added in the load test configuration.`); + } + if (obj.length == 1) { + let val = obj[0]; + let str = `name : ${val.name}, value : ${val.value}`; + if ((0, util_1.isNullOrUndefined)(val.name)) { + throw new Error(`Invalid certificate name at ${str}`); + } + if (!Util.validateUrlcert(val.value)) + throw new Error(`Invalid certificate url at ${str}`); + cert = { name: val.name, type: 'AKV_CERT_URI', value: val.value }; + } + return cert; + } + return null; + } + getMultiRegionLoadTestConfig(multiRegionalConfig) { + let parsedMultiRegionConfiguration = []; + multiRegionalConfig.forEach(regionConfig => { + let data = { + region: regionConfig.region, + engineInstances: regionConfig.engineInstances, + }; + parsedMultiRegionConfiguration.push(data); + }); + return parsedMultiRegionConfiguration; + } +} +exports.YamlConfig = YamlConfig; diff --git a/lib/models/UtilModels.js b/lib/models/UtilModels.js new file mode 100644 index 00000000..596e4b53 --- /dev/null +++ b/lib/models/UtilModels.js @@ -0,0 +1,66 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ManagedIdentityType = exports.ValidConditionList = exports.ValidAggregateList = exports.ApiVersionConstants = exports.correlationHeader = exports.resultZipFileName = exports.reportZipFileName = exports.resultFolder = exports.FileType = exports.ContentTypeMap = exports.CallTypeForDP = exports.TokenScope = exports.ParamType = void 0; +var ParamType; +(function (ParamType) { + ParamType["env"] = "env"; + ParamType["secrets"] = "secrets"; + ParamType["cert"] = "cert"; +})(ParamType || (exports.ParamType = ParamType = {})); +var TokenScope; +(function (TokenScope) { + TokenScope[TokenScope["Dataplane"] = 0] = "Dataplane"; + TokenScope[TokenScope["ControlPlane"] = 1] = "ControlPlane"; +})(TokenScope || (exports.TokenScope = TokenScope = {})); +var CallTypeForDP; +(function (CallTypeForDP) { + CallTypeForDP[CallTypeForDP["get"] = 0] = "get"; + CallTypeForDP[CallTypeForDP["patch"] = 1] = "patch"; + CallTypeForDP[CallTypeForDP["put"] = 2] = "put"; + CallTypeForDP[CallTypeForDP["delete"] = 3] = "delete"; +})(CallTypeForDP || (exports.CallTypeForDP = CallTypeForDP = {})); +exports.ContentTypeMap = { + [CallTypeForDP.get]: null, + [CallTypeForDP.patch]: 'application/merge-patch+json', + [CallTypeForDP.put]: 'application/octet-stream', + [CallTypeForDP.delete]: 'application/json' +}; +var FileType; +(function (FileType) { + FileType["JMX_FILE"] = "JMX_FILE"; + FileType["USER_PROPERTIES"] = "USER_PROPERTIES"; + FileType["ADDITIONAL_ARTIFACTS"] = "ADDITIONAL_ARTIFACTS"; + FileType["ZIPPED_ARTIFACTS"] = "ZIPPED_ARTIFACTS"; + FileType["URL_TEST_CONFIG"] = "URL_TEST_CONFIG"; + FileType["TEST_SCRIPT"] = "TEST_SCRIPT"; +})(FileType || (exports.FileType = FileType = {})); +exports.resultFolder = 'loadTest'; +exports.reportZipFileName = 'report.zip'; +exports.resultZipFileName = 'results.zip'; +exports.correlationHeader = 'x-ms-correlation-request-id'; +var ApiVersionConstants; +(function (ApiVersionConstants) { + ApiVersionConstants.tm2024Version = '2024-05-01-preview'; + ApiVersionConstants.tm2023Version = '2023-04-01-preview'; + ApiVersionConstants.tm2022Version = '2022-11-01'; + ApiVersionConstants.cp2022Version = '2022-12-01'; +})(ApiVersionConstants || (exports.ApiVersionConstants = ApiVersionConstants = {})); +exports.ValidAggregateList = { + 'response_time_ms': ['avg', 'min', 'max', 'p50', 'p75', 'p90', 'p95', 'p96', 'p97', 'p98', 'p99', 'p999', 'p9999'], + 'requests_per_sec': ['avg'], + 'requests': ['count'], + 'latency': ['avg', 'min', 'max', 'p50', 'p75', 'p90', 'p95', 'p96', 'p97', 'p98', 'p99', 'p999', 'p9999'], + 'error': ['percentage'] +}; +exports.ValidConditionList = { + 'response_time_ms': ['>', '<'], + 'requests_per_sec': ['>', '<'], + 'requests': ['>', '<'], + 'latency': ['>', '<'], + 'error': ['>'] +}; +var ManagedIdentityType; +(function (ManagedIdentityType) { + ManagedIdentityType["SystemAssigned"] = "SystemAssigned"; + ManagedIdentityType["UserAssigned"] = "UserAssigned"; +})(ManagedIdentityType || (exports.ManagedIdentityType = ManagedIdentityType = {})); diff --git a/lib/constants.js b/lib/models/constants.js similarity index 100% rename from lib/constants.js rename to lib/models/constants.js diff --git a/lib/engine/BaseLoadTestFrameworkModel.js b/lib/models/engine/BaseLoadTestFrameworkModel.js similarity index 100% rename from lib/engine/BaseLoadTestFrameworkModel.js rename to lib/models/engine/BaseLoadTestFrameworkModel.js diff --git a/lib/engine/JMeterFrameworkModel.js b/lib/models/engine/JMeterFrameworkModel.js similarity index 100% rename from lib/engine/JMeterFrameworkModel.js rename to lib/models/engine/JMeterFrameworkModel.js diff --git a/lib/engine/LocustFrameworkModel.js b/lib/models/engine/LocustFrameworkModel.js similarity index 100% rename from lib/engine/LocustFrameworkModel.js rename to lib/models/engine/LocustFrameworkModel.js diff --git a/lib/engine/TestKind.js b/lib/models/engine/TestKind.js similarity index 100% rename from lib/engine/TestKind.js rename to lib/models/engine/TestKind.js diff --git a/lib/engine/Util.js b/lib/models/engine/Util.js similarity index 93% rename from lib/engine/Util.js rename to lib/models/engine/Util.js index 1b892ebd..b852fa0e 100644 --- a/lib/engine/Util.js +++ b/lib/models/engine/Util.js @@ -1,6 +1,12 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Resources = exports.getLoadTestFrameworkModelFromKind = exports.getLoadTestFrameworkFromKind = exports.isTestKindConvertibleToJMX = exports.getLoadTestFrameworkDisplayName = exports.getLoadTestFrameworkModel = exports.getOrderedLoadTestFrameworks = exports.LoadTestFramework = void 0; +exports.Resources = exports.LoadTestFramework = void 0; +exports.getOrderedLoadTestFrameworks = getOrderedLoadTestFrameworks; +exports.getLoadTestFrameworkModel = getLoadTestFrameworkModel; +exports.getLoadTestFrameworkDisplayName = getLoadTestFrameworkDisplayName; +exports.isTestKindConvertibleToJMX = isTestKindConvertibleToJMX; +exports.getLoadTestFrameworkFromKind = getLoadTestFrameworkFromKind; +exports.getLoadTestFrameworkModelFromKind = getLoadTestFrameworkModelFromKind; const JMeterFrameworkModel_1 = require("./JMeterFrameworkModel"); const LocustFrameworkModel_1 = require("./LocustFrameworkModel"); const TestKind_1 = require("./TestKind"); @@ -21,7 +27,6 @@ var LoadTestFramework; function getOrderedLoadTestFrameworks() { return [LoadTestFramework.JMeter, LoadTestFramework.Locust]; } -exports.getOrderedLoadTestFrameworks = getOrderedLoadTestFrameworks; /** * Returns the corresponding LoadTestFrameworkModel based on the provided LoadTestFramework enum. * If the provided framework is not recognized, it assumes JMeter by default. @@ -39,7 +44,6 @@ function getLoadTestFrameworkModel(framework) { return _jmeterFramework; } } -exports.getLoadTestFrameworkModel = getLoadTestFrameworkModel; /** * Retrieves the display name of a load test framework. * @param framework The load test framework. @@ -48,7 +52,6 @@ exports.getLoadTestFrameworkModel = getLoadTestFrameworkModel; function getLoadTestFrameworkDisplayName(framework) { return getLoadTestFrameworkModel(framework).frameworkDisplayName; } -exports.getLoadTestFrameworkDisplayName = getLoadTestFrameworkDisplayName; /** * Checks if a given test kind is convertible to JMX. * If the kind is not provided, it assumes JMX by default. @@ -62,7 +65,6 @@ function isTestKindConvertibleToJMX(kind) { } return kind === TestKind_1.TestKind.URL; } -exports.isTestKindConvertibleToJMX = isTestKindConvertibleToJMX; /** * Retrieves the load test framework from a given test kind. * If no kind is provided, it assumes JMX by default. @@ -83,7 +85,6 @@ function getLoadTestFrameworkFromKind(kind) { return LoadTestFramework.JMeter; } } -exports.getLoadTestFrameworkFromKind = getLoadTestFrameworkFromKind; /** * Retrieves the load test framework model from a given test kind. * If no kind is provided, it assumes JMX by default. @@ -93,7 +94,6 @@ exports.getLoadTestFrameworkFromKind = getLoadTestFrameworkFromKind; function getLoadTestFrameworkModelFromKind(kind) { return getLoadTestFrameworkModel(getLoadTestFrameworkFromKind(kind)); } -exports.getLoadTestFrameworkModelFromKind = getLoadTestFrameworkModelFromKind; var Resources; (function (Resources) { let Strings; diff --git a/lib/util.js b/lib/models/util.js similarity index 68% rename from lib/util.js rename to lib/models/util.js index abc911d7..f0807531 100644 --- a/lib/util.js +++ b/lib/models/util.js @@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { @@ -32,103 +42,40 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getAllFileErrors = exports.ErrorCorrection = exports.getResultObj = exports.validCriteria = exports.isTerminalTestStatus = exports.removeUnits = exports.indexOfFirstDigit = exports.deleteFile = exports.getReportFolder = exports.getResultFolder = exports.checkValidityYaml = exports.invalidDescription = exports.invalidDisplayName = exports.invalidName = exports.getUniqueId = exports.sleep = exports.printClientMetrics = exports.uploadFileToResultsFolder = exports.printCriteria = exports.printTestDuration = exports.checkFileTypes = exports.checkFileType = exports.httpClientRetries = exports.uploadFileData = exports.ManagedIdentityType = exports.apiConstants = void 0; -const fs = __importStar(require("fs")); -var path = require('path'); -var AdmZip = require("adm-zip"); +exports.checkFileType = checkFileType; +exports.checkFileTypes = checkFileTypes; +exports.printTestDuration = printTestDuration; +exports.printCriteria = printCriteria; +exports.ErrorCorrection = ErrorCorrection; +exports.printClientMetrics = printClientMetrics; +exports.sleep = sleep; +exports.getUniqueId = getUniqueId; +exports.getResultFolder = getResultFolder; +exports.getReportFolder = getReportFolder; +exports.indexOfFirstDigit = indexOfFirstDigit; +exports.removeUnits = removeUnits; +exports.isTerminalTestStatus = isTerminalTestStatus; +exports.isStatusFailed = isStatusFailed; +exports.validCriteria = validCriteria; +exports.getResultObj = getResultObj; +exports.invalidDisplayName = invalidDisplayName; +exports.invalidDescription = invalidDescription; +exports.checkValidityYaml = checkValidityYaml; +exports.getPassFailCriteriaFromString = getPassFailCriteriaFromString; +exports.ValidateCriteriaAndConvertToWorkingStringModel = ValidateCriteriaAndConvertToWorkingStringModel; +exports.validateUrl = validateUrl; +exports.validateUrlcert = validateUrlcert; +exports.getDefaultTestName = getDefaultTestName; +exports.getDefaultTestRunName = getDefaultTestRunName; +exports.getDefaultRunDescription = getDefaultRunDescription; +exports.validateTestRunParamsFromPipeline = validateTestRunParamsFromPipeline; +exports.getAllFileErrors = getAllFileErrors; const { v4: uuidv4 } = require('uuid'); -const core = __importStar(require("@actions/core")); -const httpc = require("typed-rest-client/HttpClient"); -const httpClient = new httpc.HttpClient('MALT-GHACTION'); -const stream_1 = require("stream"); const util_1 = require("util"); const constants_1 = require("./constants"); const EngineUtil = __importStar(require("./engine/Util")); const TestKind_1 = require("./engine/TestKind"); -const validAggregateList = { - 'response_time_ms': ['avg', 'min', 'max', 'p50', 'p75', 'p90', 'p95', 'p96', 'p97', 'p98', 'p99', 'p999', 'p9999'], - 'requests_per_sec': ['avg'], - 'requests': ['count'], - 'latency': ['avg', 'min', 'max', 'p50', 'p75', 'p90', 'p95', 'p96', 'p97', 'p98', 'p99', 'p999', 'p9999'], - 'error': ['percentage'] -}; -const validConditionList = { - 'response_time_ms': ['>', '<'], - 'requests_per_sec': ['>', '<'], - 'requests': ['>', '<'], - 'latency': ['>', '<'], - 'error': ['>'] -}; -var apiConstants; -(function (apiConstants) { - apiConstants.latestVersion = '2024-05-01-preview'; - apiConstants.tm2022Version = '2022-11-01'; - apiConstants.cp2022Version = '2022-12-01'; -})(apiConstants || (exports.apiConstants = apiConstants = {})); -var ManagedIdentityType; -(function (ManagedIdentityType) { - ManagedIdentityType["SystemAssigned"] = "SystemAssigned"; - ManagedIdentityType["UserAssigned"] = "UserAssigned"; -})(ManagedIdentityType || (exports.ManagedIdentityType = ManagedIdentityType = {})); -function uploadFileData(filepath) { - try { - let filedata = fs.readFileSync(filepath); - const readable = new stream_1.Readable(); - readable._read = () => { }; - readable.push(filedata); - readable.push(null); - return readable; - } - catch (err) { - err.message = "File not found " + filepath; - throw new Error(err.message); - } -} -exports.uploadFileData = uploadFileData; -const correlationHeader = 'x-ms-correlation-request-id'; -function httpClientRetries(urlSuffix_1, header_1, method_1) { - return __awaiter(this, arguments, void 0, function* (urlSuffix, header, method, retries = 1, data, isUploadCall = true, log = true) { - let httpResponse; - try { - let correlationId = `gh-actions-${getUniqueId()}`; - header[correlationHeader] = correlationId; // even if we put console.debug its printing along with the logs, so lets just go ahead with the differentiation with GH-actions, so we can search the timeframe for GH-actions in correlationid and resource filter. - if (method == 'get') { - httpResponse = yield httpClient.get(urlSuffix, header); - } - else if (method == 'del') { - httpResponse = yield httpClient.del(urlSuffix, header); - } - else if (method == 'put' && isUploadCall) { - let fileContent = uploadFileData(data); - httpResponse = yield httpClient.request(method, urlSuffix, fileContent, header); - } - else { - httpResponse = yield httpClient.request(method, urlSuffix, data, header); - } - if (httpResponse.message.statusCode != undefined && httpResponse.message.statusCode >= 300) { - core.debug(`correlation id : ${correlationId}`); - } - if (httpResponse.message.statusCode != undefined && [408, 429, 502, 503, 504].includes(httpResponse.message.statusCode)) { - let err = yield getResultObj(httpResponse); - throw { message: (err && err.error && err.error.message) ? err.error.message : ErrorCorrection(httpResponse) }; // throwing as message to catch it as err.message - } - return httpResponse; - } - catch (err) { - if (retries) { - let sleeptime = (5 - retries) * 1000 + Math.floor(Math.random() * 5001); - yield sleep(sleeptime); - if (log) { - console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime / 1000} seconds`); - } - return httpClientRetries(urlSuffix, header, method, retries - 1, data); - } - else - throw new Error(`Operation did not succeed after 3 retries. Pipeline failed with error : ${err.message}`); - } - }); -} -exports.httpClientRetries = httpClientRetries; +const UtilModels_1 = require("./UtilModels"); function checkFileType(filePath, fileExtToValidate) { if ((0, util_1.isNullOrUndefined)(filePath)) { return false; @@ -136,7 +83,6 @@ function checkFileType(filePath, fileExtToValidate) { let split = filePath.split('.'); return split[split.length - 1].toLowerCase() == fileExtToValidate.toLowerCase(); } -exports.checkFileType = checkFileType; function checkFileTypes(filePath, fileExtsToValidate) { var _a; if ((0, util_1.isNullOrUndefined)(filePath)) { @@ -146,30 +92,32 @@ function checkFileTypes(filePath, fileExtsToValidate) { let fileExtsToValidateLower = fileExtsToValidate.map(ext => ext.toLowerCase()); return fileExtsToValidateLower.includes((_a = split[split.length - 1]) === null || _a === void 0 ? void 0 : _a.toLowerCase()); } -exports.checkFileTypes = checkFileTypes; -function printTestDuration(vusers, startTime, endTime, testStatus) { +function printTestDuration(testRunObj) { return __awaiter(this, void 0, void 0, function* () { + var _a, _b; console.log("Summary generation completed\n"); console.log("-------------------Summary ---------------"); - console.log("TestRun start time: " + startTime); - console.log("TestRun end time: " + endTime); - console.log("Virtual Users: " + vusers); - console.log(`TestStatus: ${testStatus} \n`); + console.log("TestRun start time: " + new Date((_a = testRunObj.startDateTime) !== null && _a !== void 0 ? _a : new Date())); + console.log("TestRun end time: " + new Date((_b = testRunObj.endDateTime) !== null && _b !== void 0 ? _b : new Date())); + console.log("Virtual Users: " + testRunObj.virtualUsers); + console.log("TestStatus: " + testRunObj.status + "\n"); return; }); } -exports.printTestDuration = printTestDuration; function printCriteria(criteria) { if (Object.keys(criteria).length == 0) return; printTestResult(criteria); - console.log("Criteria\t\t\t\t\t :Actual Value\t Result"); + console.log("Criteria\t\t\t\t\t :Actual Value\t Result"); for (var key in criteria) { - var metric = criteria[key]; + let metric = criteria[key]; + if ((0, util_1.isNullOrUndefined)(metric)) + continue; var str = metric.aggregate + "(" + metric.clientMetric + ") " + metric.condition + ' ' + metric.value; if (metric.requestName != null) { str = metric.requestName + ": " + str; } + //str += ((metric.clientmetric == "error") ? ", " : "ms, ") + metric.action; var spaceCount = 50 - str.length; while (spaceCount > 0) { str += ' '; @@ -184,55 +132,23 @@ function printCriteria(criteria) { } console.log("\n"); } -exports.printCriteria = printCriteria; +function ErrorCorrection(result) { + return "Unable to fetch the response. Please re-run or contact support if the issue persists. " + "Status code :" + result.message.statusCode; +} function printTestResult(criteria) { + var _a, _b; let pass = 0; let fail = 0; for (var key in criteria) { - if (criteria[key].result == "passed") + if (((_a = criteria[key]) === null || _a === void 0 ? void 0 : _a.result) == "passed") pass++; - else if (criteria[key].result == "failed") + else if (((_b = criteria[key]) === null || _b === void 0 ? void 0 : _b.result) == "failed") fail++; } console.log("-------------------Test Criteria ---------------"); - console.log("Results\t\t\t :" + pass + " Pass " + fail + " Fail\n"); + console.log("Results\t\t\t :" + pass + " Pass " + fail + " Fail\n"); + return { pass, fail }; // returning so that we can use this in the UTs later. } -function uploadFileToResultsFolder(response_1) { - return __awaiter(this, arguments, void 0, function* (response, fileName = 'results.zip') { - try { - const filePath = path.join('loadTest', fileName); - const file = fs.createWriteStream(filePath); - return new Promise((resolve, reject) => { - file.on("error", (err) => reject(err)); - const stream = response.message.pipe(file); - stream.on("close", () => { - try { - resolve(filePath); - } - catch (err) { - reject(err); - } - }); - }); - } - catch (err) { - err.message = "Error in fetching the results of the testRun"; - throw new Error(err); - } - }); -} -exports.uploadFileToResultsFolder = uploadFileToResultsFolder; -function printClientMetrics(obj) { - return __awaiter(this, void 0, void 0, function* () { - if (Object.keys(obj).length == 0) - return; - console.log("------------------Client-side metrics------------\n"); - for (var key in obj) { - printMetrics(obj[key], key); - } - }); -} -exports.printClientMetrics = printClientMetrics; function printMetrics(data, key = null) { var _a; let samplerName = (_a = data.transaction) !== null && _a !== void 0 ? _a : key; @@ -247,6 +163,16 @@ function printMetrics(data, key = null) { console.log("total error rate \t : " + data.errorPct); console.log("\n"); } +function printClientMetrics(obj) { + return __awaiter(this, void 0, void 0, function* () { + if (Object.keys(obj).length == 0) + return; + console.log("------------------Client-side metrics------------\n"); + for (var key in obj) { + printMetrics(obj[key], key); + } + }); +} function getAbsVal(data) { if ((0, util_1.isNullOrUndefined)(data)) { return "undefined"; @@ -260,11 +186,95 @@ function sleep(ms) { setTimeout(resolve, ms); }); } -exports.sleep = sleep; function getUniqueId() { - return uuidv4().toString(); + return uuidv4(); +} +function getResultFolder(testArtifacts) { + if ((0, util_1.isNullOrUndefined)(testArtifacts) || (0, util_1.isNullOrUndefined)(testArtifacts.outputArtifacts)) + return null; + var outputurl = testArtifacts.outputArtifacts; + return !(0, util_1.isNullOrUndefined)(outputurl.resultFileInfo) ? outputurl.resultFileInfo.url : null; +} +function getReportFolder(testArtifacts) { + if ((0, util_1.isNullOrUndefined)(testArtifacts) || (0, util_1.isNullOrUndefined)(testArtifacts.outputArtifacts)) + return null; + var outputurl = testArtifacts.outputArtifacts; + return !(0, util_1.isNullOrUndefined)(outputurl.reportFileInfo) ? outputurl.reportFileInfo.url : null; +} +function indexOfFirstDigit(input) { + let i = 0; + for (; input[i] < '0' || input[i] > '9'; i++) + ; + return i == input.length ? -1 : i; +} +function removeUnits(input) { + let i = 0; + for (; input[i] >= '0' && input[i] <= '9'; i++) + ; + return i == input.length ? input : input.substring(0, i); +} +function isTerminalTestStatus(testStatus) { + if (testStatus == "DONE" || testStatus === "FAILED" || testStatus === "CANCELLED") { + return true; + } + return false; +} +function isStatusFailed(testStatus) { + if (testStatus === "FAILED" || testStatus === "CANCELLED") { + return true; + } + return false; +} +function validCriteria(data) { + switch (data.clientMetric) { + case "response_time_ms": + return validResponseTimeCriteria(data); + case "requests_per_sec": + return validRequestsPerSecondCriteria(data); + case "requests": + return validRequestsCriteria(data); + case "latency": + return validLatencyCriteria(data); + case "error": + return validErrorCriteria(data); + default: + return false; + } +} +function validResponseTimeCriteria(data) { + return !(!UtilModels_1.ValidAggregateList['response_time_ms'].includes(data.aggregate) || !UtilModels_1.ValidConditionList['response_time_ms'].includes(data.condition) + || (data.value).indexOf('.') != -1 || data.action != "continue"); +} +function validRequestsPerSecondCriteria(data) { + return !(!UtilModels_1.ValidAggregateList['requests_per_sec'].includes(data.aggregate) || !UtilModels_1.ValidConditionList['requests_per_sec'].includes(data.condition) + || data.action != "continue"); +} +function validRequestsCriteria(data) { + return !(!UtilModels_1.ValidAggregateList['requests'].includes(data.aggregate) || !UtilModels_1.ValidConditionList['requests'].includes(data.condition) + || (data.value).indexOf('.') != -1 || data.action != "continue"); +} +function validLatencyCriteria(data) { + return !(!UtilModels_1.ValidAggregateList['latency'].includes(data.aggregate) || !UtilModels_1.ValidConditionList['latency'].includes(data.condition) + || (data.value).indexOf('.') != -1 || data.action != "continue"); +} +function validErrorCriteria(data) { + return !(!UtilModels_1.ValidAggregateList['error'].includes(data.aggregate) || !UtilModels_1.ValidConditionList['error'].includes(data.condition) + || Number(data.value) < 0 || Number(data.value) > 100 || data.action != "continue"); +} +function getResultObj(data) { + return __awaiter(this, void 0, void 0, function* () { + let dataString; + let dataJSON; + try { + dataString = yield data.readBody(); + dataJSON = JSON.parse(dataString); + return dataJSON; + } + catch (_a) { + return null; + } + }); } -exports.getUniqueId = getUniqueId; function isDictionary(variable) { return typeof variable === 'object' && variable !== null && !Array.isArray(variable); } @@ -274,19 +284,16 @@ function invalidName(value) { var r = new RegExp(/[^a-z0-9_-]+/); return r.test(value); } -exports.invalidName = invalidName; function invalidDisplayName(value) { if (value.length < 2 || value.length > 50) return true; return false; } -exports.invalidDisplayName = invalidDisplayName; function invalidDescription(value) { if (value.length > 100) return true; return false; } -exports.invalidDescription = invalidDescription; function isInValidSubnet(uri) { const pattern = /^\/subscriptions\/[a-f0-9-]+\/resourceGroups\/[a-zA-Z0-9\u0080-\uFFFF()._-]+\/providers\/Microsoft\.Network\/virtualNetworks\/[a-zA-Z0-9._-]+\/subnets\/[a-zA-Z0-9._-]+$/i; return !(pattern.test(uri)); @@ -299,7 +306,7 @@ function isValidTestKind(value) { return Object.values(TestKind_1.TestKind).includes(value); } function isValidManagedIdentityType(value) { - return Object.values(ManagedIdentityType).includes(value); + return Object.values(UtilModels_1.ManagedIdentityType).includes(value); } function isArrayOfStrings(variable) { return Array.isArray(variable) && variable.every((item) => typeof item === 'string'); @@ -374,10 +381,10 @@ function checkValidityYaml(givenYaml) { if (givenYaml.keyVaultReferenceIdentityType != undefined && givenYaml.keyVaultReferenceIdentityType != null && !isValidManagedIdentityType(givenYaml.keyVaultReferenceIdentityType)) { return { valid: false, error: `The value "${givenYaml.keyVaultReferenceIdentityType}" for keyVaultReferenceIdentityType is invalid. Allowed values are "SystemAssigned" and "UserAssigned".` }; } - if (!(0, util_1.isNullOrUndefined)(givenYaml.keyVaultReferenceIdentity) && givenYaml.keyVaultReferenceIdentityType == ManagedIdentityType.SystemAssigned) { + if (!(0, util_1.isNullOrUndefined)(givenYaml.keyVaultReferenceIdentity) && givenYaml.keyVaultReferenceIdentityType == UtilModels_1.ManagedIdentityType.SystemAssigned) { return { valid: false, error: `The "keyVaultReferenceIdentity" should omitted or set to null when using the "SystemAssigned" identity type.` }; } - if ((0, util_1.isNullOrUndefined)(givenYaml.keyVaultReferenceIdentity) && givenYaml.keyVaultReferenceIdentityType == ManagedIdentityType.UserAssigned) { + if ((0, util_1.isNullOrUndefined)(givenYaml.keyVaultReferenceIdentity) && givenYaml.keyVaultReferenceIdentityType == UtilModels_1.ManagedIdentityType.UserAssigned) { return { valid: false, error: `"The value for 'keyVaultReferenceIdentity' cannot be null when using the 'UserAssigned' identity type. Provide a valid identity reference for 'keyVaultReferenceIdentity'."` }; } if (givenYaml.publicIPDisabled && typeof givenYaml.publicIPDisabled != 'boolean') { @@ -437,125 +444,127 @@ function checkValidityYaml(givenYaml) { } return { valid: true, error: "" }; } -exports.checkValidityYaml = checkValidityYaml; -function getResultFolder(testArtifacts) { - if (testArtifacts == null || testArtifacts.outputArtifacts == null) - return null; - var outputurl = testArtifacts.outputArtifacts; - return (outputurl.resultFileInfo != null) ? outputurl.resultFileInfo.url : null; -} -exports.getResultFolder = getResultFolder; -function getReportFolder(testArtifacts) { - if (testArtifacts == null || testArtifacts.outputArtifacts == null) - return null; - var outputurl = testArtifacts.outputArtifacts; - return (outputurl.reportFileInfo != null) ? outputurl.reportFileInfo.url : null; -} -exports.getReportFolder = getReportFolder; -function deleteFile(foldername) { - if (fs.existsSync(foldername)) { - fs.readdirSync(foldername).forEach((file, index) => { - const curPath = path.join(foldername, file); - if (fs.lstatSync(curPath).isDirectory()) { - deleteFile(curPath); +/* + ado takes the full pf criteria as a string after parsing the string into proper data model, +*/ +function getPassFailCriteriaFromString(passFailCriteria) { + let failureCriteriaValue = {}; + passFailCriteria.forEach(criteria => { + let criteriaString = criteria; + let data = { + aggregate: "", + clientMetric: "", + condition: "", + value: "", + requestName: "", + action: "", + }; + if (typeof criteria !== "string") { + let request = Object.keys(criteria)[0]; + data.requestName = request; + criteriaString = criteria[request]; + } + let tempStr = ""; + for (let i = 0; i < criteriaString.length; i++) { + if (criteriaString[i] == '(') { + data.aggregate = tempStr.trim(); + tempStr = ""; + } + else if (criteriaString[i] == ')') { + data.clientMetric = tempStr; + tempStr = ""; + } + else if (criteriaString[i] == ',') { + data.condition = tempStr.substring(0, indexOfFirstDigit(tempStr)).trim(); + data.value = tempStr.substr(indexOfFirstDigit(tempStr)).trim(); + tempStr = ""; } else { - fs.unlinkSync(curPath); + tempStr += criteriaString[i]; } - }); - fs.rmdirSync(foldername); - } -} -exports.deleteFile = deleteFile; -function indexOfFirstDigit(input) { - let i = 0; - for (; input[i] < '0' || input[i] > '9'; i++) - ; - return i == input.length ? -1 : i; -} -exports.indexOfFirstDigit = indexOfFirstDigit; -function removeUnits(input) { - let i = 0; - for (; input[i] >= '0' && input[i] <= '9'; i++) - ; - return i == input.length ? input : input.substring(0, i); -} -exports.removeUnits = removeUnits; -function isTerminalTestStatus(testStatus) { - if (testStatus === "DONE" || testStatus === "FAILED" || testStatus === "CANCELLED") { - return true; - } - return false; -} -exports.isTerminalTestStatus = isTerminalTestStatus; -function validCriteria(data) { - switch (data.clientMetric) { - case "response_time_ms": - return validResponseTimeCriteria(data); - case "requests_per_sec": - return validRequestsPerSecondCriteria(data); - case "requests": - return validRequestsCriteria(data); - case "latency": - return validLatencyCriteria(data); - case "error": - return validErrorCriteria(data); - default: - return false; - } -} -exports.validCriteria = validCriteria; -function validResponseTimeCriteria(data) { - return !(!validAggregateList['response_time_ms'].includes(data.aggregate) || !validConditionList['response_time_ms'].includes(data.condition) - || (data.value).indexOf('.') != -1 || data.action != "continue"); -} -function validRequestsPerSecondCriteria(data) { - return !(!validAggregateList['requests_per_sec'].includes(data.aggregate) || !validConditionList['requests_per_sec'].includes(data.condition) - || data.action != "continue"); -} -function validRequestsCriteria(data) { - return !(!validAggregateList['requests'].includes(data.aggregate) || !validConditionList['requests'].includes(data.condition) - || (data.value).indexOf('.') != -1 || data.action != "continue"); -} -function validLatencyCriteria(data) { - return !(!validAggregateList['latency'].includes(data.aggregate) || !validConditionList['latency'].includes(data.condition) - || (data.value).indexOf('.') != -1 || data.action != "continue"); -} -function validErrorCriteria(data) { - return !(!validAggregateList['error'].includes(data.aggregate) || !validConditionList['error'].includes(data.condition) - || Number(data.value) < 0 || Number(data.value) > 100 || data.action != "continue"); -} -function getResultObj(data) { - return __awaiter(this, void 0, void 0, function* () { - var dataString; - var dataJSON; - try { - dataString = yield data.readBody(); - dataJSON = JSON.parse(dataString); - return dataJSON; } - catch (_a) { - return null; + if (criteriaString.indexOf(',') != -1) { + data.action = tempStr.trim(); + } + else { + data.condition = tempStr.substring(0, indexOfFirstDigit(tempStr)).trim(); + data.value = tempStr.substr(indexOfFirstDigit(tempStr)).trim(); } + ValidateCriteriaAndConvertToWorkingStringModel(data, failureCriteriaValue); }); + return failureCriteriaValue; +} +/* + ado takes the full pf criteria as a string after parsing the string into proper data model, + this is to avoid duplicates of the data by keeping the full aggrregated metric + as a key and the values will be set in this function to use it further +*/ +function ValidateCriteriaAndConvertToWorkingStringModel(data, failureCriteriaValue) { + if (data.action == "") + data.action = "continue"; + data.value = removeUnits(data.value); + if (!validCriteria(data)) + throw new Error("Invalid Failure Criteria"); + let key = data.clientMetric + ' ' + data.aggregate + ' ' + data.condition + ' ' + data.action; + if (data.requestName != "") { + key = key + ' ' + data.requestName; + } + let val = parseInt(data.value); + let currVal = val; + if (failureCriteriaValue.hasOwnProperty(key)) + currVal = failureCriteriaValue[key]; + if (data.condition == '>') { + failureCriteriaValue[key] = (val < currVal) ? val : currVal; + } + else { + failureCriteriaValue[key] = (val > currVal) ? val : currVal; + } +} +function validateUrl(url) { + var r = new RegExp(/(http|https):\/\/.*\/secrets\/.+$/); + return r.test(url); +} +function validateUrlcert(url) { + var r = new RegExp(/(http|https):\/\/.*\/certificates\/.+$/); + return r.test(url); +} +function getDefaultTestName() { + const a = (new Date(Date.now())).toLocaleString(); + const b = a.split(", "); + const c = a.split(" "); + return "Test_" + b[0] + "_" + c[1] + c[2]; +} +function getDefaultTestRunName() { + const a = (new Date(Date.now())).toLocaleString(); + const b = a.split(", "); + const c = a.split(" "); + return "TestRun_" + b[0] + "_" + c[1] + c[2]; +} +function getDefaultRunDescription() { + return "Started using GitHub Actions"; +} +function validateTestRunParamsFromPipeline(runTimeParams) { + if (runTimeParams.runDisplayName && invalidDisplayName(runTimeParams.runDisplayName)) + throw new Error("Invalid test run name. Test run name must be between 2 to 50 characters."); + if (runTimeParams.runDescription && invalidDescription(runTimeParams.runDescription)) + throw new Error("Invalid test run description. Test run description must be less than 100 characters."); } -exports.getResultObj = getResultObj; -function ErrorCorrection(result) { - return "Unable to fetch the response. Please re-run or contact support if the issue persists. " + "Status code: " + result.message.statusCode; -} -exports.ErrorCorrection = ErrorCorrection; function getAllFileErrors(testObj) { - var allArtifacts = []; - for (var key in testObj.inputArtifacts) { - var artifacts = testObj.inputArtifacts[key]; - if (artifacts instanceof Array) { - allArtifacts = allArtifacts.concat(artifacts.filter((artifact) => artifact !== null && artifact !== undefined)); - } - else if (artifacts !== null && artifacts !== undefined) { - allArtifacts.push(artifacts); - } - } - var fileErrors = {}; + var _a, _b, _c, _d, _e, _f; + let allArtifacts = []; + let additionalArtifacts = (_a = testObj === null || testObj === void 0 ? void 0 : testObj.inputArtifacts) === null || _a === void 0 ? void 0 : _a.additionalFileInfo; + additionalArtifacts && (allArtifacts = allArtifacts.concat(additionalArtifacts.filter((artifact) => artifact !== null && artifact !== undefined))); + let testScript = (_b = testObj === null || testObj === void 0 ? void 0 : testObj.inputArtifacts) === null || _b === void 0 ? void 0 : _b.testScriptFileInfo; + testScript && allArtifacts.push(testScript); + let configFile = (_c = testObj === null || testObj === void 0 ? void 0 : testObj.inputArtifacts) === null || _c === void 0 ? void 0 : _c.configFileInfo; + configFile && allArtifacts.push(configFile); + let userProperties = (_d = testObj === null || testObj === void 0 ? void 0 : testObj.inputArtifacts) === null || _d === void 0 ? void 0 : _d.userPropFileInfo; + userProperties && allArtifacts.push(userProperties); + let zipFile = (_e = testObj === null || testObj === void 0 ? void 0 : testObj.inputArtifacts) === null || _e === void 0 ? void 0 : _e.inputArtifactsZipFileInfo; + zipFile && allArtifacts.push(zipFile); + let urlFile = (_f = testObj === null || testObj === void 0 ? void 0 : testObj.inputArtifacts) === null || _f === void 0 ? void 0 : _f.urlTestConfigFileInfo; + urlFile && allArtifacts.push(urlFile); + let fileErrors = {}; for (const file of allArtifacts) { if (file.validationStatus === "VALIDATION_FAILURE") { fileErrors[file.fileName] = file.validationFailureDetails; @@ -563,4 +572,3 @@ function getAllFileErrors(testObj) { } return fileErrors; } -exports.getAllFileErrors = getAllFileErrors; diff --git a/lib/services/FeatureFlagService.js b/lib/services/FeatureFlagService.js index 2b014428..259d3e84 100644 --- a/lib/services/FeatureFlagService.js +++ b/lib/services/FeatureFlagService.js @@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { @@ -33,21 +43,23 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FeatureFlagService = void 0; -const constants_1 = require("../constants"); -const map = __importStar(require("../mappers")); -const util = __importStar(require("../util")); +const constants_1 = require("../models/constants"); +const util = __importStar(require("../models/util")); +const UtilModels_1 = require("../models/UtilModels"); +const FetchUtil = __importStar(require("./../models/FetchHelper")); class FeatureFlagService { - static getFeatureFlagAsync(flag_1, baseUrl_1) { + constructor(authContext) { + this.featureFlagCache = {}; + this.authContext = authContext; + } + getFeatureFlagAsync(flag_1, baseUrl_1) { return __awaiter(this, arguments, void 0, function* (flag, baseUrl, useCache = true) { if (useCache && flag in this.featureFlagCache) { return { featureFlag: flag, enabled: this.featureFlagCache[flag.toString()] }; } let uri = baseUrl + constants_1.APIRoute.FeatureFlags(flag.toString()); - let headers = { - 'content-type': 'application/json', - 'Authorization': 'Bearer ' + map.getToken() - }; - let flagResponse = yield util.httpClientRetries(uri, headers, 'get', 3, "", false, false); + let headers = this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.get); + let flagResponse = yield FetchUtil.httpClientRetries(uri, headers, 'get', 3, "", false, false); try { let flagObj = (yield util.getResultObj(flagResponse)); this.featureFlagCache[flag.toString()] = flagObj.enabled; @@ -62,7 +74,7 @@ class FeatureFlagService { } }); } - static isFeatureEnabledAsync(flag_1, baseUrl_1) { + isFeatureEnabledAsync(flag_1, baseUrl_1) { return __awaiter(this, arguments, void 0, function* (flag, baseUrl, useCache = true) { let flagObj = yield this.getFeatureFlagAsync(flag, baseUrl, useCache); return flagObj ? flagObj.enabled : false; @@ -70,4 +82,3 @@ class FeatureFlagService { } } exports.FeatureFlagService = FeatureFlagService; -FeatureFlagService.featureFlagCache = {}; diff --git a/src/main.ts b/src/main.ts index b645a893..83d9fc40 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,515 +1,32 @@ -import * as core from '@actions/core' -import httpc = require('typed-rest-client/HttpClient'); -import * as map from "./mappers" -import * as util from './util'; -import * as Util from './engine/Util'; -import { TestKind } from "./engine/TestKind"; +import * as util from './models/FileUtils'; +import { resultFolder } from "./models/UtilModels"; import * as fs from 'fs'; -import { isNullOrUndefined } from 'util'; -import { FeatureFlagService } from './services/FeatureFlagService'; -import { FeatureFlags } from './services/FeatureFlags'; +import * as core from '@actions/core'; +import { AuthenticationUtils } from "./models/AuthenticationUtils"; +import { YamlConfig } from "./models/TaskModels"; +import { APISupport } from "./models/APISupport"; -const resultFolder = 'loadTest'; -const reportZipFileName = 'report.zip'; -const resultZipFileName = 'results.zip'; -let baseURL = ''; -let testId = ''; -let existingCriteria: { [name: string]: map.criteriaObj | null } = {}; -let existingParams: { [name: string]: map.paramObj|null } = {}; -let existingEnv: { [name: string]: string } = {}; -enum FileType{ - JMX_FILE = 'JMX_FILE', - USER_PROPERTIES = 'USER_PROPERTIES', - ADDITIONAL_ARTIFACTS = 'ADDITIONAL_ARTIFACTS', - ZIPPED_ARTIFACTS = "ZIPPED_ARTIFACTS", - URL_TEST_CONFIG = "URL_TEST_CONFIG", - TEST_SCRIPT = "TEST_SCRIPT" -} async function run() { - try { - await map.getInputParams(); - await getLoadTestResource(); - testId = map.getTestId(); - await getTestAPI(false); + try { + + let authContext = new AuthenticationUtils(); + let yamlConfig = new YamlConfig(); + let apiSupport = new APISupport(authContext, yamlConfig); + + await authContext.authorize(); + await apiSupport.getResource(); + await apiSupport.getTestAPI(false); if (fs.existsSync(resultFolder)){ util.deleteFile(resultFolder); } + fs.mkdirSync(resultFolder); - await createTestAPI(); + await apiSupport.createTestAPI(); + } catch (err:any) { core.setFailed(err.message); } } -async function getTestAPI(validate:boolean, returnTestObj:boolean = false) { - var urlSuffix = "tests/"+testId+"?api-version="+util.apiConstants.latestVersion; - urlSuffix = baseURL+urlSuffix; - let header = await map.getTestHeader(); - let testResult = await util.httpClientRetries(urlSuffix,header,'get',3,""); - if(testResult.message.statusCode == 401 || testResult.message.statusCode == 403){ - var message = "Service Principal does not have sufficient permissions. Please assign " - +"the Load Test Contributor role to the service principal. Follow the steps listed at " - +"https://docs.microsoft.com/azure/load-testing/tutorial-cicd-github-actions#configure-the-github-actions-workflow-to-run-a-load-test "; - throw new Error(message); - } - if(testResult.message.statusCode != 200 && testResult.message.statusCode != 201){ - if(validate){ // validate is called, then get should not be false, and this validate had retries because of the conflicts in jmx test, so lets not print in the console, instead put this in the error itself. - let testObj:any=await util.getResultObj(testResult); - let err = testObj?.error?.message ? testObj?.error?.message : util.ErrorCorrection(testResult); - throw new Error(err); - } else if(!validate && testResult.message.statusCode != 404){ // if not validate, then its to check if it is edit or create thats all, so it should not throw the error for 404. - let testObj:any=await util.getResultObj(testResult); - console.log(testObj ? testObj : util.ErrorCorrection(testResult)); - throw new Error("Error in getting the test."); - } - // note : kumarmoh - /// else { - // do nothing if the validate = false and status code is 404, as it is for create test. - // } this is just for comment - } - if(testResult.message.statusCode == 200) { - let testObj:any=await util.getResultObj(testResult); - if(testObj == null){ - throw new Error(util.ErrorCorrection(testResult)); - } - if (testObj.kind == null){ - testObj.kind = testObj.testType; - } - var inputScriptFileInfo = testObj.kind == TestKind.URL ? testObj.inputArtifacts.urlTestConfigFileInfo :testObj.inputArtifacts.testScriptFileInfo; - - if(validate){ - if (returnTestObj) { - return [inputScriptFileInfo.validationStatus, testObj]; - } - return inputScriptFileInfo.validationStatus; - } - else - { - if(!isNullOrUndefined(testObj.passFailCriteria) && !isNullOrUndefined(testObj.passFailCriteria.passFailMetrics)) - existingCriteria = testObj.passFailCriteria.passFailMetrics; - if(testObj.secrets != null) - existingParams = testObj.secrets; - if(testObj.environmentVariables != null) - existingEnv = testObj.environmentVariables; - } - } -} -async function deleteFileAPI(filename:string) { - var urlSuffix = "tests/"+testId+"/files/"+filename+"?api-version="+util.apiConstants.latestVersion; - urlSuffix = baseURL+urlSuffix; - let header = await map.getTestHeader(); - let delFileResult = await util.httpClientRetries(urlSuffix,header,'del',3,""); - if(delFileResult.message.statusCode != 204){ - let delFileObj:any=await util.getResultObj(delFileResult); - let Message: string = delFileObj ? delFileObj.message : util.ErrorCorrection(delFileResult); - throw new Error(Message); - } -} -async function createTestAPI() { - var urlSuffix = "tests/"+testId+"?api-version="+util.apiConstants.latestVersion; - urlSuffix = baseURL+urlSuffix; - var createData = map.createTestData(); - let header = await map.createTestHeader(); - let createTestresult = await util.httpClientRetries(urlSuffix,header,'patch',3,JSON.stringify(createData)); - if(createTestresult.message.statusCode != 200 && createTestresult.message.statusCode != 201) { - let testRunObj:any=await util.getResultObj(createTestresult); - console.log(testRunObj ? testRunObj : util.ErrorCorrection(createTestresult)); - throw new Error("Error in creating test: " + testId); - } - if(createTestresult.message.statusCode == 201) { - console.log("Creating a new load test '"+testId+"' "); - console.log("Successfully created load test "+testId); - } - else{ - console.log("Test '"+ testId +"' already exists"); - // test script will anyway be updated by the GH-actions in later steps, this will be error if the test script is not present in the test. - // this will be error in the url tests when the quick test is getting updated to the url test. so removing this. - let testObj:any=await util.getResultObj(createTestresult); - var testFiles = testObj.inputArtifacts; - if(testFiles.userPropUrl != null){ - console.log(`Deleting the existing UserProperty file.`); - await deleteFileAPI(testFiles.userPropFileInfo.fileName); - } - if(testFiles.testScriptFileInfo != null){ - console.log(`Deleting the existing TestScript file.`); - await deleteFileAPI(testFiles.testScriptFileInfo.fileName); - } - if(testFiles.additionalFileInfo != null){ - // delete existing files which are not present in yaml, the files which are in yaml will anyway be uploaded again. - let existingFiles : string[] = []; - let file : any; - for(file of testFiles.additionalFileInfo){ - existingFiles.push(file.fileName); - } - for(file of map.getConfigFiles()){ - let indexOfFile = existingFiles.indexOf(file) - if(indexOfFile != -1){ - existingFiles.splice(indexOfFile, 1); - } - } - for(file of map.getZipFiles()){ - let indexOfFile = existingFiles.indexOf(file) - if(indexOfFile != -1){ - existingFiles.splice(indexOfFile, 1); - } - } - if(existingFiles.length > 0){ - console.log(`Deleting the ${existingFiles.length} existing test file(s) which is(are) not in the configuration yaml file.`); - } - for(file of existingFiles){ - await deleteFileAPI(file); - } - } - } - - await uploadConfigFile() -} - -async function uploadTestPlan() -{ - let retry = 5; - let filepath = map.getTestFile(); - let filename = map.getFileName(filepath); - var urlSuffix = "tests/"+testId+"/files/"+filename+"?api-version="+util.apiConstants.latestVersion; - - let fileType = FileType.TEST_SCRIPT; - if(map.getTestKind() == TestKind.URL){ - fileType = FileType.URL_TEST_CONFIG; - } - urlSuffix = baseURL + urlSuffix + ("&fileType=" + fileType); - - let headers = await map.UploadAndValidateHeader(); - let uploadresult = await util.httpClientRetries(urlSuffix,headers,'put',3,filepath,true); - if(uploadresult.message.statusCode != 201){ - let uploadObj:any = await util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading TestPlan for the created test"); - } - else { - console.log("Uploaded test plan for the test"); - var minutesToAdd=10; - var startTime = new Date(); - var maxAllowedTime = new Date(startTime.getTime() + minutesToAdd*60000); - var validationStatus = "VALIDATION_INITIATED"; - var testObj; - while(maxAllowedTime>(new Date()) && (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED" || validationStatus == null)) { - try{ - [validationStatus, testObj] = await getTestAPI(true, true); - } - catch(e:any) { - retry--; - if(retry == 0){ - throw new Error("Unable to validate the test plan. Please retry. Failed with error :" + e); - } - } - await util.sleep(5000); - } - console.log("Validation status of the test plan: "+ validationStatus); - if(validationStatus == null || validationStatus == "VALIDATION_SUCCESS" ){ - console.log(`Validated test plan for the test successfully.`); - - // Get errors from all files - var fileErrors = util.getAllFileErrors(testObj); - - if (Object.keys(fileErrors).length > 0) { - console.log("Validation failed for the following files:"); - for (const [file, error] of Object.entries(fileErrors)) { - console.log(`File: ${file}, Error: ${error}`); - } - throw new Error("Validation of one or more files failed. Please correct the errors and try again."); - } - - await createTestRun(); - } - else if(validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED") - throw new Error("TestPlan validation timeout. Please try again.") - else - throw new Error("TestPlan validation Failed."); - } -} -async function uploadConfigFile() -{ - let configFiles = map.getConfigFiles(); - if(configFiles != undefined && configFiles.length > 0) { - for (let filepath of configFiles) { - let filename = map.getFileName(filepath); - var urlSuffix = "tests/"+testId+"/files/"+filename+"?api-version="+util.apiConstants.latestVersion + ("&fileType=" + FileType.ADDITIONAL_ARTIFACTS); - urlSuffix = baseURL+urlSuffix; - let headers = await map.UploadAndValidateHeader(); - let uploadresult = await util.httpClientRetries(urlSuffix,headers,'put',3,filepath, true); - if(uploadresult.message.statusCode != 201){ - let uploadObj:any = await util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading config file for the created test"); - } - } - console.log(`Uploaded ${configFiles.length} configuration file(s) for the test successfully.`); - } - await uploadZipArtifacts(); -} -async function uploadZipArtifacts() -{ - let zipFiles = map.getZipFiles(); - if(zipFiles != undefined && zipFiles.length > 0) { - console.log("Uploading and validating the zip artifacts"); - for (let filepath of zipFiles) { - let filename = map.getFileName(filepath); - var urlSuffix = "tests/"+testId+"/files/"+filename+"?api-version="+util.apiConstants.latestVersion+"&fileType="+FileType.ZIPPED_ARTIFACTS; - urlSuffix = baseURL+urlSuffix; - let headers = await map.UploadAndValidateHeader(); - let uploadresult = await util.httpClientRetries(urlSuffix,headers,'put',3,filepath, true); - if(uploadresult.message.statusCode != 201){ - let uploadObj:any = await util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading config file for the created test"); - } - } - var minutesToAdd=5; - let startTime = new Date(); - var maxAllowedTime = new Date(startTime.getTime() + minutesToAdd*60000); - let flagValidationPending = true; - let zipInvalid = false; - let zipFailureReason = ""; - - // TODO(harshanb): Remove this check once the feature flag is enabled by default. - let isTestScriptFragmentEnabled = await FeatureFlagService.isFeatureEnabledAsync(FeatureFlags.enableTestScriptFragments, baseURL); - let zipValidationTerminateStates = ["VALIDATION_SUCCESS", "VALIDATION_FAILURE"]; - if (isTestScriptFragmentEnabled) { - // NOT_VALIDATED is a terminal state for the file validation and actual validation will be performed during test script upload - zipValidationTerminateStates.push("NOT_VALIDATED"); - } - while(maxAllowedTime>(new Date()) && flagValidationPending) { - var urlSuffix = "tests/"+testId+"?api-version="+util.apiConstants.latestVersion; - urlSuffix = baseURL+urlSuffix; - let header = await map.getTestHeader(); - let testResult = await util.httpClientRetries(urlSuffix,header,'get',3,""); - let testObj = await util.getResultObj(testResult); - if(testResult.message.statusCode != 200 && testResult.message.statusCode != 201){ - console.log(testObj ? testObj : util.ErrorCorrection(testResult)); - throw new Error("Error in getting the test."); - } - flagValidationPending = false; - if (testObj && testObj.inputArtifacts && testObj.inputArtifacts.additionalFileInfo) { - for(const file of testObj.inputArtifacts.additionalFileInfo){ - if (file.fileType == FileType.ZIPPED_ARTIFACTS && (file.validationStatus !in zipValidationTerminateStates)) { - flagValidationPending = true; - break; - } else if(file.fileType == FileType.ZIPPED_ARTIFACTS && file.validationStatus == "VALIDATION_FAILURE"){ - zipInvalid = true; - zipFailureReason = file.validationFailureDetails ?? "Validation failed for the zip artifact with an unknown error."; - break; - } - } - } else { - break; - } - if(zipInvalid){ - break; - } - await util.sleep(3000); - } - if(zipInvalid) { - throw new Error(`Validation of one or more zip artifacts failed with Error : "${zipFailureReason}".`); - } else if(flagValidationPending) { - throw new Error("Validation of one or more zip artifacts timed out. Please retry."); - } - if (isTestScriptFragmentEnabled) { - console.log(`Uploaded ${zipFiles.length} zip artifact(s) for the test successfully.`); - } - else { - console.log(`Uploaded and validated ${zipFiles.length} zip artifact(s) for the test successfully.`); - } - } - var statuscode = await uploadPropertyFile(); - if(statuscode== 201){ - await uploadTestPlan(); - } -} -async function uploadPropertyFile() -{ - let propertyFile = map.getPropertyFile(); - if(propertyFile != undefined) { - let filename = map.getFileName(propertyFile); - var urlSuffix = "tests/"+testId+"/files/"+filename+"?api-version="+util.apiConstants.latestVersion+"&fileType="+FileType.USER_PROPERTIES; - urlSuffix = baseURL + urlSuffix; - let headers = await map.UploadAndValidateHeader(); - let uploadresult = await util.httpClientRetries(urlSuffix,headers,'put',3,propertyFile); - if(uploadresult.message.statusCode != 201){ - let uploadObj:any = await util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading TestPlan for the created test"); - } - console.log(`Uploaded user properties file for the test successfully.`); - } - return 201; -} - -async function createTestRun() { - const tenantId = map.getTenantId(); - const testRunId = util.getUniqueId(); - var urlSuffix = "test-runs/"+testRunId+"?tenantId="+tenantId+"&api-version="+util.apiConstants.latestVersion; - urlSuffix = baseURL+urlSuffix; - const ltres: string = core.getInput('loadTestResource'); - const runDisplayName: string = core.getInput('loadTestRunName'); - const runDescription: string = core.getInput('loadTestRunDescription'); - const subName = await map.getSubName(); - try { - var startData = map.startTestData(testRunId, runDisplayName, runDescription); - console.log("Creating and running a testRun for the test"); - let header = await map.createTestHeader(); - let startTestresult = await util.httpClientRetries(urlSuffix,header,'patch',3,JSON.stringify(startData)); - let testRunDao:any=await util.getResultObj(startTestresult); - if(startTestresult.message.statusCode != 200 && startTestresult.message.statusCode != 201) { - console.log(testRunDao ? testRunDao : util.ErrorCorrection(startTestresult)); - throw new Error("Error in running the test"); - } - let startTime = new Date(); - let testRunName = testRunDao.displayName; - let status = testRunDao.status; - if(status == "ACCEPTED") { - console.log("\nView the load test run in Azure portal by following the steps:") - console.log("1. Go to your Azure Load Testing resource '"+ltres+"' in subscription '"+subName+"'") - console.log("2. On the Tests page, go to test '"+testId+"'") - console.log("3. Go to test run '"+testRunName+"'\n"); - await getTestRunAPI(testRunId, status, startTime); - } - } - catch(err:any) { - if(!err.message) - err.message = "Error in running the test"; - throw new Error(err.message); - } -} -async function getTestRunAPI(testRunId:string, testStatus:string, startTime:Date) -{ - var urlSuffix = "test-runs/"+testRunId+"?api-version="+util.apiConstants.latestVersion; - urlSuffix = baseURL+urlSuffix; - while(!util.isTerminalTestStatus(testStatus)) - { - let header = await map.getTestRunHeader(); - let testRunResult = await util.httpClientRetries(urlSuffix,header,'get',3,""); - let testRunObj:any = await util.getResultObj(testRunResult); - if(testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201) { - console.log(testRunObj ? testRunObj : util.ErrorCorrection(testRunResult)); - throw new Error("Error in getting the test run"); - } - testStatus = testRunObj.status; - if(util.isTerminalTestStatus(testStatus)) { - let vusers = null; - let count = 0; - let reportsAvailable = false; - console.log("Test run completed. Polling for statistics and dashboard report to populate."); - // Polling for max 3 min for statistics and pass fail criteria to populate - while((!reportsAvailable || isNullOrUndefined(vusers)) && count < 18){ - await util.sleep(10000); - let header = await map.getTestRunHeader(); - let testRunResult = await util.httpClientRetries(urlSuffix,header,'get',3,""); - testRunObj = await util.getResultObj(testRunResult); - if(testRunObj == null){ - throw new Error(util.ErrorCorrection(testRunResult)); - } - if(testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201){ - console.log(testRunObj ? testRunObj : util.ErrorCorrection(testRunResult)); - throw new Error("Error in getting the test run"); - } - vusers = testRunObj.virtualUsers; - count++; - let testReport = util.getReportFolder(testRunObj.testArtifacts); - if(testReport) { - reportsAvailable = true; - } - } - if(testRunObj && testRunObj.startDateTime){ - startTime = new Date(testRunObj.startDateTime); - } - let endTime = new Date(); - if(testRunObj && testRunObj.endDateTime){ - endTime = new Date(testRunObj.endDateTime); - } - util.printTestDuration(testRunObj.virtualUsers, startTime, endTime ,testStatus); - if(!isNullOrUndefined(testRunObj.passFailCriteria) && !isNullOrUndefined(testRunObj.passFailCriteria.passFailMetrics)) - util.printCriteria(testRunObj.passFailCriteria.passFailMetrics) - if(testRunObj.testRunStatistics != null) - util.printClientMetrics(testRunObj.testRunStatistics); - let testResultUrl = util.getResultFolder(testRunObj.testArtifacts); - if(testResultUrl != null) { - const response = await util.httpClientRetries(testResultUrl,{},'get',3,""); - if (response.message.statusCode != 200) { - let respObj:any = await util.getResultObj(response); - console.log(respObj ? respObj : util.ErrorCorrection(response)); - throw new Error("Error in fetching results "); - } - else { - await util.uploadFileToResultsFolder(response,resultZipFileName); - } - } - let testReportUrl = util.getReportFolder(testRunObj.testArtifacts); - if(testReportUrl != null) { - const response = await util.httpClientRetries(testReportUrl,{},'get',3,""); - if (response.message.statusCode != 200) { - let respObj:any = await util.getResultObj(response); - console.log(respObj ? respObj : util.ErrorCorrection(response)); - throw new Error("Error in fetching report "); - } - else { - await util.uploadFileToResultsFolder(response, reportZipFileName); - } - } - if(testRunObj.status === "FAILED" || testRunObj.status === "CANCELLED") { - console.log("Please go to the Portal for more error details: "+ testRunObj.portalUrl); - core.setFailed("TestStatus: "+ testRunObj.status); - return; - } - if(testRunObj.testResult === "FAILED" || testRunObj.testResult === "CANCELLED") { - core.setFailed("TestResult: "+ testRunObj.testResult); - return; - } - return; - } - else - { - if(!util.isTerminalTestStatus(testStatus)) - { - if(testStatus === "DEPROVISIONING" || testStatus === "DEPROVISIONED" || testStatus != "EXECUTED" ) - await util.sleep(5000); - else - await util.sleep(20000); - } - } - } -} -async function getLoadTestResource() -{ - let id = map.getResourceId(); - let armUrl = map.getARMEndpoint(); - let armEndpointSuffix = id + "?api-version=" + util.apiConstants.cp2022Version; - let armEndpoint = new URL(armEndpointSuffix, armUrl); - var header = map.armTokenHeader(); - let response = await util.httpClientRetries(armEndpoint.toString(),header,'get',3,""); - var resource_name: string = core.getInput('loadTestResource'); - if(response.message.statusCode == 404) { - var message = "The Azure Load Testing resource "+ resource_name +" does not exist. Please provide an existing resource."; - throw new Error(message); - } - let respObj:any = await util.getResultObj(response); - if(response.message.statusCode != 200){ - console.log(respObj ? respObj : util.ErrorCorrection(response)); - throw new Error("Error fetching resource " + resource_name); - } - let dataPlaneUrl = respObj.properties.dataPlaneURI; - baseURL = 'https://'+dataPlaneUrl+'/'; -} -export function getExistingCriteria() -{ - return existingCriteria; -} -export function getExistingParams() -{ - return existingParams; -} -export function getExistingEnv() -{ - return existingEnv; -} run(); \ No newline at end of file diff --git a/src/mappers.ts b/src/mappers.ts deleted file mode 100644 index db80dee2..00000000 --- a/src/mappers.ts +++ /dev/null @@ -1,666 +0,0 @@ -import { IHeaders } from "typed-rest-client/Interfaces"; -import * as core from "@actions/core"; -const yaml = require("js-yaml"); -import * as jwt_decode from "jwt-decode"; -import * as fs from "fs"; -import { execFile } from "child_process"; -import * as util from "./util"; -import * as index from "./main"; -import { isNullOrUndefined } from "util"; -const pathLib = require("path"); -const { Readable } = require("stream"); -import { TestKind } from "./engine/TestKind"; -import { BaseLoadTestFrameworkModel } from './engine/BaseLoadTestFrameworkModel'; -import * as EngineUtil from './engine/Util'; - -var testId = ""; -var displayName = ""; -var testdesc = "SampleTest"; -var engineInstances = "1"; -var testPlan = ""; -var propertyFile: string | null = null; -var configFiles: string[] = []; -var zipFiles : string[]=[]; -var token = ""; -var resourceId = ""; -var subscriptionID = ""; -var environment="AzureCloud"; -var armTokenScope="https://management.core.windows.net"; -var dataPlaneTokenScope="https://loadtest.azure-dev.com"; -var armEndpoint="https://management.azure.com"; -var tenantId = ""; -var yamlFile = ""; -var passFailCriteria: any[] = []; -var regionalLoadTestConfig: regionConfiguration[] | null = null; -var autoStop: autoStopCriteriaObjOut | null = null; -var kvRefId: string | null = null; -var kvRefType: string | null = null; -var subnetId: string | null = null; -var splitCSVs: boolean | null = null; -var certificate: certObj | null = null; -let kind : TestKind; -let publicIPDisabled : boolean = false; - -export interface certObj { - type: string; - value: string; - name: string; -} -export interface criteriaObj { - aggregate: string; - clientMetric: string; - condition: string; - requestName: string | null; - action: string | null; - value: number; -} -export interface autoStopCriteriaObjIn { - autoStopEnabled? : boolean; - errorPercentage ?: number; - timeWindow ?: number; -} -export interface autoStopCriteriaObjOut { - autoStopDisabled? : boolean; - errorRate ?: number; - errorRateTimeWindowInSeconds ?: number; -} - -export interface regionConfiguration { - region: string; - engineInstances: number; -}; - -export interface paramObj { - type: string; - value: string; -} -enum paramType { - env = "env", - secrets = "secrets", - cert = "cert" -} -let failCriteria: { [name: string]: criteriaObj | null } = {}; -let secretsYaml: { [name: string]: paramObj | null } = {}; -let secretsRun: { [name: string]: paramObj } = {}; -let envYaml: { [name: string]: string | null } = {}; -let envRun: { [name: string]: string } = {}; -let failureCriteriaValue: { [name: string]: number } = {}; - -function getExistingData() { - var existingCriteria: { [name: string]: criteriaObj | null } = - index.getExistingCriteria(); - var existingCriteriaIds: string[] = Object.keys(existingCriteria); - getFailureCriteria(existingCriteriaIds); - - var existingParams: any = index.getExistingParams(); - for (var key in existingParams) { - if (!secretsYaml.hasOwnProperty(key)) secretsYaml[key] = null; - } - var existingEnv: any = index.getExistingEnv(); - for (var key in existingEnv) { - if (!envYaml.hasOwnProperty(key)) envYaml[key] = null; - } -} - -export function getToken() { - return token; -} - -export function createTestData() { - getExistingData(); - var data = { - testId: testId, - description: testdesc, - displayName: displayName, - quickStartTest : false, // always quick test will be false because GH-actions doesnot support it now. - loadTestConfiguration: { - engineInstances: engineInstances, - splitAllCSVs: splitCSVs, - optionalLoadTestConfig : null, - regionalLoadTestConfig : regionalLoadTestConfig, - }, - secrets: secretsYaml, - kind : kind, - certificate: certificate, - environmentVariables: envYaml, - passFailCriteria: { - passFailMetrics: failCriteria, - }, - autoStopCriteria: autoStop, - subnetId: subnetId, - publicIPDisabled : publicIPDisabled, - keyvaultReferenceIdentityType: kvRefType, - keyvaultReferenceIdentityId: kvRefId, - }; - return data; -} - -export async function createTestHeader() { - let headers: IHeaders = { - "content-type": "application/merge-patch+json", - Authorization: "Bearer " + token, - }; - return headers; -} - -export async function UploadAndValidateHeader() { - let headers: IHeaders = { - Authorization: "Bearer " + token, - "content-type": "application/octet-stream", - }; - return headers; -} -export function armTokenHeader() { - let headers: IHeaders = { - Authorization: "Bearer " + token, - }; - return headers; -} -export function startTestData( - testRunName: string, - runDisplayName: string, - runDescription: string -) { - var data = { - testRunId: testRunName, - displayName: runDisplayName ? runDisplayName : getDefaultTestRunName(), - description: runDescription - ? runDescription - : "Started using GitHub Actions", - testId: testId, - secrets: secretsRun, - environmentVariables: envRun, - }; - return data; -} -export async function getTestRunHeader() { - if (!isExpired()) { - await getAccessToken(dataPlaneTokenScope); - } - let headers: IHeaders = { - "content-type": "application/json", - Authorization: "Bearer " + token, - }; - return headers; -} - -function isExpired() { - const header = jwt_decode.jwtDecode(token); - const now = Math.floor(Date.now() / 1000); - return header && header.exp && header.exp > now; -} -export async function getTestHeader() { - await getAccessToken(dataPlaneTokenScope); - let headers: IHeaders = { - "content-type": "application/json", - Authorization: "Bearer " + token, - }; - return headers; -} -export function getResourceId() { - const rg: string = core.getInput("resourceGroup"); - const ltres: string = core.getInput("loadTestResource"); - if(isNullOrUndefined(rg) || rg == ''){ - throw new Error(`The input field "resourceGroup" is empty. Provide an existing resource group name.`); - } - if(isNullOrUndefined(ltres) || ltres == ''){ - throw new Error(`The input field "loadTestResource" is empty. Provide an existing load test resource name.`); - } - resourceId = "/subscriptions/" + subscriptionID + "/resourcegroups/" + rg + "/providers/microsoft.loadtestservice/loadtests/" + ltres; - return resourceId; -} -export async function getInputParams() { - await setEndpointAndScope(); - await getAccessToken(armTokenScope); - yamlFile = core.getInput("loadTestConfigFile"); - if(isNullOrUndefined(yamlFile) || yamlFile == ''){ - throw new Error(`The input field "loadTestConfigFile" is empty. Provide the path to load test yaml file.`); - } - if ( - !( - pathLib.extname(yamlFile) === ".yaml" || - pathLib.extname(yamlFile) === ".yml" - ) - ) - throw new Error( - "The Load Test configuration file should be of type .yaml or .yml" - ); - const config = yaml.load(fs.readFileSync(yamlFile, "utf8")); - - let validConfig : {valid : boolean, error :string} = util.checkValidityYaml(config); - if(!validConfig.valid){ - throw new Error(validConfig.error + ` Refer to the load test YAML syntax at https://learn.microsoft.com/azure/load-testing/reference-test-config-yaml`); - } - testId = config.testId ?? config.testName; - testId = testId.toLowerCase(); - displayName = config.displayName ?? testId; - - testdesc = config.description; - engineInstances = config.engineInstances ?? 1; - - let path = pathLib.dirname(yamlFile); - testPlan = pathLib.join(path, config.testPlan); - - kind = config.testType as TestKind ?? TestKind.JMX; - let framework : BaseLoadTestFrameworkModel = EngineUtil.getLoadTestFrameworkModelFromKind(kind); - - if (config.configurationFiles != null) { - let tempconfigFiles: string[] = []; - tempconfigFiles = config.configurationFiles; - for(let file of tempconfigFiles){ - if(kind == TestKind.URL && !util.checkFileType(file,'csv')){ - throw new Error("Only CSV files are allowed as configuration files for a URL-based test."); - } - file = pathLib.join(path, file); - configFiles.push(file); - } - } - if(config.zipArtifacts != undefined){ - let tempZipFiles: string[]=[]; - tempZipFiles = config.zipArtifacts; - if(kind == TestKind.URL && tempZipFiles.length > 0){ - throw new Error("Zip artifacts are not supported for the URL-based test."); - } - for(let file of tempZipFiles){ - file = pathLib.join(path,file); - zipFiles.push(file); - }; - } - if (config.splitAllCSVs != undefined) { - splitCSVs = config.splitAllCSVs; - } - if (config.failureCriteria != undefined) { - passFailCriteria = config.failureCriteria; - getPassFailCriteria(); - } - if (config.subnetId != undefined) { - subnetId = config.subnetId; - } - if(config.publicIPDisabled != undefined) { - publicIPDisabled = (config.publicIPDisabled) - } - if(config.properties != undefined && config.properties.userPropertyFile != undefined) - { - if(kind == TestKind.URL){ - throw new Error("User property file is not supported for the URL-based test."); - } - if(!util.checkFileTypes(config.properties.userPropertyFile, framework.userPropertyFileExtensions)){ - throw new Error(`User property file with extension other than ${framework.ClientResources.userPropertyFileExtensionsFriendly} is not permitted.`); - } - var propFile = config.properties.userPropertyFile; - propertyFile = pathLib.join(path,propFile); - } - if (config.secrets != undefined) { - kvRefType = "SystemAssigned"; - getParameters(config.secrets, paramType.secrets); - } - if (config.env != undefined) { - getParameters(config.env, paramType.env); - } - if (config.certificates != undefined) { - getParameters(config.certificates, paramType.cert); - } - if (config.autoStop != undefined) { - getAutoStopCriteria(config.autoStop); - } - - if (config.keyVaultReferenceIdentity != undefined) { - kvRefType = "UserAssigned"; - kvRefId = config.keyVaultReferenceIdentity; - } - if(config.regionalLoadTestConfig != undefined) { - regionalLoadTestConfig = getMultiRegionLoadTestConfig(config.regionalLoadTestConfig); - } - getRunTimeParams(); - validateTestRunParams(); - if ( - testId === "" || - isNullOrUndefined(testId) || - testPlan === "" || - isNullOrUndefined(testPlan) - ) { - throw new Error( - "The required fields testName/testPlan are missing in " + yamlFile + "." - ); - } -} - -export async function getSubName() { - try { - const cmdArguments = ["account", "show"]; - var result: any = await execAz(cmdArguments); - let name = result.name; - return name; - } catch (err: any) { - const message = - `An error occurred while getting credentials from ` + - `Azure CLI for getting subscription name: ${err.message}`; - throw new Error(message); - } -} - -async function getAccessToken(aud: string) { - try { - const cmdArguments = ["account", "get-access-token", "--resource"]; - cmdArguments.push(aud); - var result: any = await execAz(cmdArguments); - token = result.accessToken; - subscriptionID = result.subscription; - tenantId = result.tenant; - return token; - } catch (err: any) { - const message = - `An error occurred while getting credentials from ` + - `Azure CLI for getting access token: ${err.message}`; - throw new Error(message); - } -} - -async function setEndpointAndScope() { - try { - const cmdArguments = ["cloud", "show"]; - var result: any = await execAz(cmdArguments); - let env = result ? result.name : null; - environment = env ? env : environment; - let endpointUrl = (result && result.endpoints) ? result.endpoints.resourceManager : null; - armEndpoint = endpointUrl ? endpointUrl : armEndpoint; - - if(environment == 'AzureUSGovernment'){ - dataPlaneTokenScope = 'https://cnt-prod.loadtesting.azure.us'; - armTokenScope = 'https://management.usgovcloudapi.net'; - } - } catch (err: any) { - const message = - `An error occurred while getting credentials from ` + - `Azure CLI for setting endPoint and scope: ${err.message}`; - throw new Error(message); - } -} - -async function execAz(cmdArguments: string[]): Promise { - const azCmd = process.platform === "win32" ? "az.cmd" : "az"; - return new Promise((resolve, reject) => { - execFile( - azCmd, - [...cmdArguments, "--out", "json"], - { encoding: "utf8", shell: true }, - (error: any, stdout: any) => { - if (error) { - return reject(error); - } - try { - return resolve(JSON.parse(stdout)); - } catch (err: any) { - const msg = - `An error occurred while parsing the output "${stdout}", of ` + - `the cmd az "${cmdArguments}": ${err.nmessage}.`; - return reject(new Error(msg)); - } - } - ); - }); -} -export function getDefaultTestName() { - const a = new Date(Date.now()).toLocaleString(); - const b = a.split(", "); - const c = a.split(" "); - return "Test_" + b[0] + "_" + c[1] + c[2]; -} - -export function getDefaultTestRunName() { - const a = new Date(Date.now()).toLocaleString(); - const b = a.split(", "); - const c = a.split(" "); - return "TestRun_" + b[0] + "_" + c[1] + c[2]; -} -function getParameters(obj: any, type: paramType) { - if (type == paramType.secrets) { - for (var index in obj) { - var val = obj[index]; - let str : string = `name : ${val.name}, value : ${val.value}`; - if(isNullOrUndefined(val.name)){ - throw new Error(`Invalid secret name at ${str}`); - } - if(!validateUrl(val.value)){ - throw new Error(`Invalid secret url at ${str}`); - } - secretsYaml[val.name] = { type: "AKV_SECRET_URI", value: val.value }; - } - } else if (type == paramType.env) { - for (var index in obj) { - var val = obj[index]; - let str : string = `name : ${val.name}, value : ${val.value}`; - if(isNullOrUndefined(val.name)){ - throw new Error(`Invalid environment name at ${str}`); - } - envYaml[val.name] = val.value; - } - } else if (type == paramType.cert) { - for (var index in obj) { - var val = obj[index]; - let str : string = `name : ${val.name}, value : ${val.value}`; - if(isNullOrUndefined(val.name)){ - throw new Error(`Invalid certificate name at ${str}`); - } - if(!validateUrl(val.value)) - throw new Error(`Invalid certificate url at ${str}`); - certificate = { name: val.name, type: "AKV_CERT_URI", value: val.value }; - break; - } - } -} -function validateUrl(url: string) { - //var r = new RegExp(/(http|https):\/\/.*\/secrets\/[/a-zA-Z0-9]+$/); - var pattern: any = - /https:\/\/+[a-zA-Z0-9_-]+\.+(?:vault|vault-int)+\.+(?:azure|azure-int|usgovcloudapi|microsoftazure)+\.+(?:net|cn|de)+\/+(?:secrets|certificates|keys|storage)+\/+[a-zA-Z0-9_-]+\/+|[a-zA-Z0-9]+$/; - var r = new RegExp(pattern); - return r.test(url); -} -function getRunTimeParams() { - var secretRun = core.getInput("secrets"); - if (secretRun != "") { - try { - var obj = JSON.parse(secretRun); - for (var index in obj) { - var val = obj[index]; - let str : string = `name : ${val.name}, value : ${val.value}`; - if(isNullOrUndefined(val.name)){ - throw new Error(`Invalid secret name at pipeline params at ${str}`); - } - secretsRun[val.name] = { type: "SECRET_VALUE", value: val.value }; - } - } catch (error) { - console.log(error); - throw new Error("Invalid format of secrets in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); - } - } - var eRun = core.getInput("env"); - if (eRun != "") { - try { - var obj = JSON.parse(eRun); - for (var index in obj) { - var val = obj[index]; - let str : string = `name : ${val.name}, value : ${val.value}`; - if(isNullOrUndefined(val.name)){ - throw new Error(`Invalid environment name at pipeline params at ${str}`); - } - envRun[val.name] = val.value; - } - } catch (error) { - console.log(error); - throw new Error("Invalid format of env in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); - } - } -} -function validateTestRunParams() { - let runDisplayName: string = core.getInput("loadTestRunName"); - let runDescription: string = core.getInput("loadTestRunDescription"); - if (runDisplayName && util.invalidDisplayName(runDisplayName)) - throw new Error( - "Invalid test run name. Test run name must be between 2 to 50 characters." - ); - if (runDescription && util.invalidDescription(runDescription)) - throw new Error( - "Invalid test run description. Test run description must be less than 100 characters." - ); -} -export function getTestKind(){ - return kind; -} -export function getYamlPath() { - return yamlFile; -} -export function getTestFile() { - return testPlan; -} -export function getPropertyFile() { - return propertyFile; -} -export function getConfigFiles() { - return configFiles; -} -export function getZipFiles() { - return zipFiles; -} -export function getTestId() { - return testId; -} - -export function getFileName(filepath: string) { - var filename = pathLib.basename(filepath); - return filename; -} - -export function getTenantId() { - return tenantId; -} -export function getARMEndpoint() { - return armEndpoint; -} -function getPassFailCriteria() { - passFailCriteria.forEach((criteria) => { - let data = { - aggregate: "", - clientMetric: "", - condition: "", - value: "", - requestName: "", - action: "", - }; - if (typeof criteria !== "string") { - var request = Object.keys(criteria)[0]; - data.requestName = request; - criteria = criteria[request]; - } - let tempStr: string = ""; - for (let i = 0; i < criteria.length; i++) { - if (criteria[i] == "(") { - data.aggregate = tempStr.trim(); - tempStr = ""; - } else if (criteria[i] == ")") { - data.clientMetric = tempStr; - tempStr = ""; - } else if (criteria[i] == ",") { - data.condition = tempStr - .substring(0, util.indexOfFirstDigit(tempStr)) - .trim(); - data.value = tempStr.substr(util.indexOfFirstDigit(tempStr)).trim(); - tempStr = ""; - } else { - tempStr += criteria[i]; - } - } - if (criteria.indexOf(",") != -1) { - data.action = tempStr.trim(); - } else { - data.condition = tempStr - .substring(0, util.indexOfFirstDigit(tempStr)) - .trim(); - data.value = tempStr.substr(util.indexOfFirstDigit(tempStr)).trim(); - } - ValidateAndAddCriteria(data); - }); -} -function ValidateAndAddCriteria(data: any) { - if (data.action == "") data.action = "continue"; - data.value = util.removeUnits(data.value); - if (!util.validCriteria(data)) throw new Error("Invalid Failure Criteria"); - var key: string = - data.clientMetric + - " " + - data.aggregate + - " " + - data.condition + - " " + - data.action; - if (data.requestName != "") { - key = key + " " + data.requestName; - } - var val: number = parseInt(data.value); - var currVal = val; - if (failureCriteriaValue.hasOwnProperty(key)) - currVal = failureCriteriaValue[key]; - if (data.condition == ">") { - failureCriteriaValue[key] = val < currVal ? val : currVal; - } else { - failureCriteriaValue[key] = val > currVal ? val : currVal; - } -} -function getFailureCriteria(existingCriteriaIds: string[]) { - var numberOfExistingCriteria = existingCriteriaIds.length; - var index = 0; - for (var key in failureCriteriaValue) { - var splitted = key.split(" "); - var criteriaId = - index < numberOfExistingCriteria - ? existingCriteriaIds[index++] - : util.getUniqueId(); - failCriteria[criteriaId] = { - clientMetric: splitted[0], - aggregate: splitted[1], - condition: splitted[2], - action: splitted[3], - value: failureCriteriaValue[key], - requestName: splitted.length > 4 ? splitted.slice(4).join(" ") : null, - }; - } - for (; index < numberOfExistingCriteria; index++) { - failCriteria[existingCriteriaIds[index]] = null; - } -} -function getAutoStopCriteria(autoStopInput : autoStopCriteriaObjIn | string | null) { - if (autoStopInput == null) {autoStop = null; return;} - if (typeof autoStopInput == "string") { - if (autoStopInput == "disable") { - let data = { - autoStopDisabled : true, - errorRate: 0, - errorRateTimeWindowInSeconds: 60, - }; - autoStop = data; - } else { - throw new Error( - "Invalid value, for disabling auto stop use 'autoStop: disable'" - ); - } - } else { - let data = { - autoStopDisabled : false, - errorRate: autoStopInput.errorPercentage, - errorRateTimeWindowInSeconds: autoStopInput.timeWindow, - }; - autoStop = data; - } -} - -function getMultiRegionLoadTestConfig(multiRegionalConfig : any[]) : regionConfiguration[] { - let parsedMultiRegionConfiguration : regionConfiguration[] = [] - multiRegionalConfig.forEach(regionConfig => { - let data = { - region: regionConfig.region, - engineInstances: regionConfig.engineInstances, - }; - parsedMultiRegionConfiguration.push(data); - }); - return parsedMultiRegionConfiguration; -} diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts new file mode 100644 index 00000000..e666f2d4 --- /dev/null +++ b/src/models/APISupport.ts @@ -0,0 +1,430 @@ +import { isNull, isNullOrUndefined } from "util"; +import { AuthenticationUtils } from "./AuthenticationUtils"; +import { ApiVersionConstants, CallTypeForDP, FileType, reportZipFileName, resultZipFileName } from "./UtilModels"; +import { TestKind } from "./engine/TestKind"; +import * as Util from './util'; +import * as FileUtils from './FileUtils'; +import * as core from '@actions/core'; +import { FileInfo, TestModel, ExistingParams, TestRunModel } from "./PayloadModels"; +import { YamlConfig } from "./TaskModels"; +import * as FetchUtil from './FetchHelper'; + +export class APISupport { + authContext : AuthenticationUtils; + yamlModel: YamlConfig; + baseURL = ''; + existingParams: ExistingParams = {secrets: {}, env: {}, passFailCriteria: {}}; + testId: string; + + constructor(authContext: AuthenticationUtils, yamlModel: YamlConfig) { + this.authContext = authContext; + this.yamlModel = yamlModel; + this.testId = this.yamlModel.testId; + } + + async getResource() { + let id = this.authContext.resourceId; + let armUrl = this.authContext.armEndpoint; + let armEndpointSuffix = id + "?api-version=" + ApiVersionConstants.cp2022Version; + let armEndpoint = new URL(armEndpointSuffix, armUrl); + let header = await this.authContext.armTokenHeader(); + let response = await FetchUtil.httpClientRetries(armEndpoint.toString(),header,'get',3,""); + let resource_name: string | undefined = core.getInput('loadTestResource'); + if(response.message.statusCode == 404) { + var message = `The Azure Load Testing resource ${resource_name} does not exist. Please provide an existing resource.`; + throw new Error(message); + } + let respObj:any = await Util.getResultObj(response); + if(response.message.statusCode != 200){ + console.log(respObj ? respObj : Util.ErrorCorrection(response)); + throw new Error("Error fetching resource " + resource_name); + } + let dataPlaneUrl = respObj.properties.dataPlaneURI; + this.baseURL = 'https://'+dataPlaneUrl+'/'; + } + + async getTestAPI(validate:boolean, returnTestObj:boolean = false) : Promise<[string | undefined, TestModel] | string | undefined> { + var urlSuffix = "tests/"+this.testId+"?api-version="+ ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL+urlSuffix; + let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.get); + let testResult = await FetchUtil.httpClientRetries(urlSuffix,header,'get',3,""); + if(testResult.message.statusCode == 401 || testResult.message.statusCode == 403){ + var message = "Service Principal does not have sufficient permissions. Please assign " + +"the Load Test Contributor role to the service principal. Follow the steps listed at " + +"https://docs.microsoft.com/azure/load-testing/tutorial-cicd-github-actions#configure-the-github-actions-workflow-to-run-a-load-test "; + + throw new Error(message); + } + + if(testResult.message.statusCode != 200 && testResult.message.statusCode != 201){ + if(validate){ // validate is called, then get should not be false, and this validate had retries because of the conflicts in jmx test, so lets not print in the console, instead put this in the error itself. + let testObj:any=await Util.getResultObj(testResult); + let err = testObj?.error?.message ? testObj?.error?.message : Util.ErrorCorrection(testResult); + throw new Error(err); + } else if(!validate && testResult.message.statusCode != 404){ // if not validate, then its to check if it is edit or create thats all, so it should not throw the error for 404. + let testObj:any=await Util.getResultObj(testResult); + console.log(testObj ? testObj : Util.ErrorCorrection(testResult)); + throw new Error("Error in getting the test."); + } + // note : kumarmoh + /// else { + // do nothing if the validate = false and status code is 404, as it is for create test. + // } this is just for comment + } + if(testResult.message.statusCode == 200) { + let testObj: TestModel =await Util.getResultObj(testResult); + if(testObj == null){ + throw new Error(Util.ErrorCorrection(testResult)); + } + let inputScriptFileInfo: FileInfo | undefined = testObj.kind == TestKind.URL ? testObj.inputArtifacts?.urlTestConfigFileInfo :testObj.inputArtifacts?.testScriptFileInfo; + + if(validate) { + if (returnTestObj) { + return [inputScriptFileInfo?.validationStatus, testObj]; + } + return inputScriptFileInfo?.validationStatus; + } + else + { + if(!isNullOrUndefined(testObj.passFailCriteria) && !isNullOrUndefined(testObj.passFailCriteria.passFailMetrics)) + this.existingParams.passFailCriteria = testObj.passFailCriteria.passFailMetrics; + if(testObj.secrets != null){ + this.existingParams.secrets = testObj.secrets; + } + if(testObj.environmentVariables != null){ + this.existingParams.env = testObj.environmentVariables; + } + } + } + } + + async deleteFileAPI(filename:string) { + var urlSuffix = "tests/"+this.testId+"/files/"+filename+"?api-version="+ ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL+urlSuffix; + let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.delete); + let delFileResult = await FetchUtil.httpClientRetries(urlSuffix,header,'del',3,""); + if(delFileResult.message.statusCode != 204) { + let delFileObj:any=await Util.getResultObj(delFileResult); + let Message: string = delFileObj ? delFileObj.message : Util.ErrorCorrection(delFileResult); + throw new Error(Message); + } + } + + async createTestAPI() { + let urlSuffix = "tests/"+this.testId+"?api-version="+ ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL+urlSuffix; + let createData = this.yamlModel.getCreateTestData(this.existingParams); + let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.patch); + let createTestresult = await FetchUtil.httpClientRetries(urlSuffix,header,'patch',3,JSON.stringify(createData)); + if(createTestresult.message.statusCode != 200 && createTestresult.message.statusCode != 201) { + let testRunObj:any=await Util.getResultObj(createTestresult); + console.log(testRunObj ? testRunObj : Util.ErrorCorrection(createTestresult)); + throw new Error("Error in creating test: " + this.testId); + } + if(createTestresult.message.statusCode == 201) { + console.log("Creating a new load test "+this.testId); + console.log("Successfully created load test "+this.testId); + } + else { + console.log("Test '"+ this.testId +"' already exists"); + // test script will anyway be updated by the ado in later steps, this will be error if the test script is not present in the test. + // this will be error in the url tests when the quick test is getting updated to the url test. so removing this. + let testObj:any=await Util.getResultObj(createTestresult); + var testFiles = testObj.inputArtifacts; + if(testFiles.userPropUrl != null){ + console.log(`Deleting the existing UserProperty file.`); + await this.deleteFileAPI(testFiles.userPropFileInfo.fileName); + } + if(testFiles.testScriptFileInfo != null){ + console.log(`Deleting the existing TestScript file.`); + await this.deleteFileAPI(testFiles.testScriptFileInfo.fileName); + } + if(testFiles.additionalFileInfo != null){ + // delete existing files which are not present in yaml, the files which are in yaml will anyway be uploaded again. + let existingFiles : string[] = []; + let file : any; + for(file of testFiles.additionalFileInfo){ + existingFiles.push(file.fileName); + } + for(let file of this.yamlModel.configurationFiles){ + file = this.yamlModel.getFileName(file); + let indexOfFile = existingFiles.indexOf(file); + if(indexOfFile != -1){ + existingFiles.splice(indexOfFile, 1); + } + } + for(let file of this.yamlModel.zipArtifacts){ + file = this.yamlModel.getFileName(file); + let indexOfFile = existingFiles.indexOf(file); + if(indexOfFile != -1){ + existingFiles.splice(indexOfFile, 1); + } + } + if(existingFiles.length > 0){ + console.log(`Deleting the ${existingFiles.length} existing test file(s) which is(are) not in the configuration yaml file.`); + } + for(const file of existingFiles){ + await this.deleteFileAPI(file); + } + } + } + await this.uploadConfigFile(); + } + + async uploadTestPlan() + { + let retry = 5; + let filepath = this.yamlModel.testPlan; + let filename = this.yamlModel.getFileName(filepath); + let urlSuffix = "tests/"+this.testId+"/files/"+filename+"?api-version="+ ApiVersionConstants.tm2024Version; + + let fileType = FileType.TEST_SCRIPT; + if(this.yamlModel.kind == TestKind.URL){ + fileType = FileType.URL_TEST_CONFIG; + } + urlSuffix = this.baseURL + urlSuffix + ("&fileType=" + fileType); + + let headers = await this.authContext.getDataPlaneHeader(CallTypeForDP.put) + let uploadresult = await FetchUtil.httpClientRetries(urlSuffix,headers,'put',3,filepath, true); + if(uploadresult.message.statusCode != 201){ + let uploadObj:any = await Util.getResultObj(uploadresult); + console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + throw new Error("Error in uploading TestPlan for the created test"); + } + else { + console.log("Uploaded test plan for the test"); + let minutesToAdd=10; + let startTime = new Date(); + let maxAllowedTime = new Date(startTime.getTime() + minutesToAdd*60000); + let validationStatus : string | undefined = "VALIDATION_INITIATED"; + let testObj: TestModel | null = null; + while(maxAllowedTime>(new Date()) && (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED" || validationStatus == null)) { + try{ + [validationStatus, testObj] = await this.getTestAPI(true, true) as [string | undefined, TestModel]; + } + catch(e) { + retry--; + if(retry == 0){ + throw new Error("Unable to validate the test plan. Please retry. Failed with error :" + e); + } + } + await Util.sleep(5000); + } + console.log("Validation status of the test plan: "+ validationStatus); + if(validationStatus == null || validationStatus == "VALIDATION_SUCCESS" ){ + console.log(`Validated test plan for the test successfully.`); + + // Get errors from all files + let fileErrors = Util.getAllFileErrors(testObj); + + if (Object.keys(fileErrors).length > 0) { + console.log("Validation failed for the following files:"); + for (const [file, error] of Object.entries(fileErrors)) { + console.log(`File: ${file}, Error: ${error}`); + } + throw new Error("Validation of one or more files failed. Please correct the errors and try again."); + } + + await this.createTestRun(); + } + else if(validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED") + throw new Error("TestPlan validation timeout. Please try again.") + else + throw new Error("TestPlan validation Failed."); + } + } + + async uploadConfigFile() + { + let configFiles = this.yamlModel.configurationFiles; + if(configFiles != undefined && configFiles.length > 0) { + for(let filepath of configFiles){ + let filename = this.yamlModel.getFileName(filepath); + let urlSuffix = "tests/"+ this.testId +"/files/"+filename+"?api-version="+ ApiVersionConstants.tm2024Version + ("&fileType=" + FileType.ADDITIONAL_ARTIFACTS); + urlSuffix = this.baseURL+urlSuffix; + let headers = await this.authContext.getDataPlaneHeader(CallTypeForDP.put); + let uploadresult = await FetchUtil.httpClientRetries(urlSuffix,headers,'put',3,filepath, true); + if(uploadresult.message.statusCode != 201){ + let uploadObj:any = await Util.getResultObj(uploadresult); + console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + throw new Error("Error in uploading config file for the created test"); + } + }; + console.log(`Uploaded ${configFiles.length} configuration file(s) for the test successfully.`); + } + await this.uploadZipArtifacts(); + } + + async uploadZipArtifacts() + { + let zipFiles = this.yamlModel.zipArtifacts; + if(zipFiles != undefined && zipFiles.length > 0) { + console.log("Uploading and validating the zip artifacts"); + for(const filepath of zipFiles){ + let filename = this.yamlModel.getFileName(filepath); + var urlSuffix = "tests/"+this.testId+"/files/"+filename+"?api-version=" + ApiVersionConstants.tm2024Version+"&fileType="+FileType.ZIPPED_ARTIFACTS; + urlSuffix = this.baseURL+urlSuffix; + let headers = await this.authContext.getDataPlaneHeader(CallTypeForDP.put); + let uploadresult = await FetchUtil.httpClientRetries(urlSuffix,headers,'put',3,filepath, true); + if(uploadresult.message.statusCode != 201){ + let uploadObj:any = await Util.getResultObj(uploadresult); + console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + throw new Error("Error in uploading config file for the created test"); + } + } + console.log(`Uploaded ${zipFiles.length} zip artifact(s) for the test successfully.`); + } + let statuscode = await this.uploadPropertyFile(); + if(statuscode== 201) + await this.uploadTestPlan(); + } + + async uploadPropertyFile() + { + let propertyFile = this.yamlModel.propertyFile; + if(propertyFile != undefined && propertyFile!= '') { + let filename = this.yamlModel.getFileName(propertyFile); + let urlSuffix = "tests/"+this.testId+"/files/"+filename+"?api-version="+ ApiVersionConstants.tm2024Version+"&fileType="+FileType.USER_PROPERTIES; + urlSuffix = this.baseURL + urlSuffix; + let headers = await this.authContext.getDataPlaneHeader(CallTypeForDP.put); + let uploadresult = await FetchUtil.httpClientRetries(urlSuffix,headers,'put',3,propertyFile, true); + if(uploadresult.message.statusCode != 201){ + let uploadObj:any = await Util.getResultObj(uploadresult); + console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + throw new Error("Error in uploading TestPlan for the created test"); + } + console.log(`Uploaded user properties file for the test successfully.`); + } + return 201; + } + + async createTestRun() { + const testRunId = Util.getUniqueId(); + let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL+urlSuffix; + try { + var startData = this.yamlModel.getStartTestData(); + console.log("Creating and running a testRun for the test"); + let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.patch); + let startTestresult = await FetchUtil.httpClientRetries(urlSuffix,header,'patch',3,JSON.stringify(startData)); + let testRunDao:any=await Util.getResultObj(startTestresult); + if(startTestresult.message.statusCode != 200 && startTestresult.message.statusCode != 201){ + console.log(testRunDao ? testRunDao : Util.ErrorCorrection(startTestresult)); + throw new Error("Error in running the test"); + } + let startTime = new Date(); + let portalUrl = testRunDao.portalUrl; + let status = testRunDao.status; + if(status == "ACCEPTED") { + console.log("View the load test run in progress at: "+ portalUrl) + await this.getTestRunAPI(testRunId, status, startTime); + } + } + catch(err:any) { + if(!err.message) + err.message = "Error in running the test"; + throw new Error(err.message); + } + } + + async getTestRunAPI(testRunId:string, testStatus:string, startTime : Date) + { + let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL+urlSuffix; + while(!Util.isTerminalTestStatus(testStatus)) + { + let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.get); + let testRunResult = await FetchUtil.httpClientRetries(urlSuffix,header,'get',3,""); + let testRunObj: TestRunModel = await Util.getResultObj(testRunResult); + if (testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201) { + console.log(testRunObj ? testRunObj : Util.ErrorCorrection(testRunResult)); + throw new Error("Error in getting the test run"); + } + testStatus = testRunObj.status; + if(Util.isTerminalTestStatus(testStatus)) { + let vusers = null; + let count = 0; + let reportsAvailable = false; + console.log("Test run completed. Polling for statistics and dashboard report to populate."); + // Polling for max 3 min for statistics and pass fail criteria to populate + while((!reportsAvailable || isNullOrUndefined(vusers)) && count < 18){ + await Util.sleep(10000); + let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.get); + let testRunResult = await FetchUtil.httpClientRetries(urlSuffix,header,'get',3,""); + testRunObj = await Util.getResultObj(testRunResult); + if(testRunObj == null){ + throw new Error(Util.ErrorCorrection(testRunResult)); + } + if(testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201){ + console.log(testRunResult ? testRunResult : Util.ErrorCorrection(testRunResult)); + throw new Error("Error in getting the test run"); + } + vusers = testRunObj.virtualUsers; + count++; + let testReport = Util.getReportFolder(testRunObj.testArtifacts); + if(testReport) { + reportsAvailable = true; + } + } + if(testRunObj && testRunObj.startDateTime){ + startTime = new Date(testRunObj.startDateTime); + } + let endTime = new Date(); + if(testRunObj && testRunObj.endDateTime){ + endTime = new Date(testRunObj.endDateTime); + } + Util.printTestDuration(testRunObj); + if(!isNullOrUndefined(testRunObj.passFailCriteria) && !isNullOrUndefined(testRunObj.passFailCriteria.passFailMetrics)) + Util.printCriteria(testRunObj.passFailCriteria.passFailMetrics) + if(testRunObj.testRunStatistics != null && testRunObj.testRunStatistics != undefined) + Util.printClientMetrics(testRunObj.testRunStatistics); + + let testResultUrl = Util.getResultFolder(testRunObj.testArtifacts); + if(testResultUrl != null) { + const response = await FetchUtil.httpClientRetries(testResultUrl,{},'get',3,""); + if (response.message.statusCode != 200) { + let respObj:any = await Util.getResultObj(response); + console.log(respObj ? respObj : Util.ErrorCorrection(response)); + throw new Error("Error in fetching results "); + } + else { + await FileUtils.uploadFileToResultsFolder(response, resultZipFileName); + } + } + let testReportUrl = Util.getReportFolder(testRunObj.testArtifacts); + if(testReportUrl != null) { + const response = await FetchUtil.httpClientRetries(testReportUrl,{},'get',3,""); + if (response.message.statusCode != 200) { + let respObj:any = await Util.getResultObj(response); + console.log(respObj ? respObj : Util.ErrorCorrection(response)); + throw new Error("Error in fetching report "); + } + else { + await FileUtils.uploadFileToResultsFolder(response, reportZipFileName); + } + } + + if(!isNull(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { + core.setFailed("TestResult: "+ testRunObj.testResult); + return; + } + if(!isNull(testRunObj.status) && Util.isStatusFailed(testRunObj.status)) { + console.log("Please go to the Portal for more error details: "+ testRunObj.portalUrl); + core.setFailed("TestStatus: "+ testRunObj.status); + return; + } + return; + } + else + { + if(!Util.isTerminalTestStatus(testStatus)) + { + if(testStatus === "DEPROVISIONING" || testStatus === "DEPROVISIONED" || testStatus != "EXECUTED" ) + await Util.sleep(5000); + else + await Util.sleep(20000); + } + } + } + } +} \ No newline at end of file diff --git a/src/models/AuthenticationUtils.ts b/src/models/AuthenticationUtils.ts new file mode 100644 index 00000000..7bd631df --- /dev/null +++ b/src/models/AuthenticationUtils.ts @@ -0,0 +1,134 @@ +import { isNullOrUndefined } from "util"; +import * as core from '@actions/core'; +import { execFile } from "child_process"; +import { CallTypeForDP, ContentTypeMap, TokenScope } from "./UtilModels"; +import { jwtDecode, JwtPayload } from "jwt-decode"; +import { IHeaders } from "typed-rest-client/Interfaces"; + +export class AuthenticationUtils { + dataPlanetoken : string = ''; + controlPlaneToken : string = ''; + subscriptionId : string = ''; + env: string = 'AzureCloud'; + armTokenScope='https://management.core.windows.net'; + dataPlaneTokenScope='https://loadtest.azure-dev.com'; + armEndpoint='https://management.azure.com'; + + resourceId : string = ''; + + constructor() {} + + async authorize() { + // NOTE: This will set the subscription id + await this.getTokenAPI(TokenScope.ControlPlane); + + const rg: string | undefined = core.getInput('resourceGroup'); + const ltres: string | undefined = core.getInput('loadTestResource'); + if(isNullOrUndefined(rg) || rg == ''){ + throw new Error(`The input field "resourceGroup" is empty. Provide an existing resource group name.`); + } + if(isNullOrUndefined(ltres) || ltres == ''){ + throw new Error(`The input field "loadTestResource" is empty. Provide an existing load test resource name.`); + } + this.resourceId = "/subscriptions/"+this.subscriptionId+"/resourcegroups/"+rg+"/providers/microsoft.loadtestservice/loadtests/"+ltres; + + await this.setEndpointAndScope(); + } + + async setEndpointAndScope() { + try + { + const cmdArguments = ["cloud", "show"]; + var result: any = await this.execAz(cmdArguments); + let env = result ? result.name : null; + this.env = env ? env : this.env; + let endpointUrl = (result && result.endpoints) ? result.endpoints.resourceManager : null; + this.armEndpoint = endpointUrl ? endpointUrl : this.armEndpoint; + + if(this.env == 'AzureUSGovernment'){ + this.dataPlaneTokenScope = 'https://cnt-prod.loadtesting.azure.us'; + this.armTokenScope = 'https://management.usgovcloudapi.net'; + } + } + catch (err: any) { + const message = + `An error occurred while getting credentials from ` + + `Azure CLI for setting endPoint and scope: ${err.message}`; + throw new Error(message); + } + } + + async getTokenAPI(scope: TokenScope) + { + let tokenScopeDecoded = scope == TokenScope.Dataplane ? this.dataPlaneTokenScope : this.armTokenScope; + try { + const cmdArguments = ["account", "get-access-token", "--resource"]; + cmdArguments.push(tokenScopeDecoded); + var result: any = await this.execAz(cmdArguments); + let token = result.accessToken; + + // NOTE: Setting the subscription id + this.subscriptionId = result.subscription; + scope == TokenScope.ControlPlane ? this.controlPlaneToken = token : this.dataPlanetoken = token; + return token; + } + catch (err:any) { + const message = + `An error occurred while getting credentials from ` + `Azure CLI: ${err.message}`; + throw new Error(message); + } + } + + async execAz(cmdArguments: string[]): Promise { + const azCmd = process.platform === "win32" ? "az.cmd" : "az"; + return new Promise((resolve, reject) => { + execFile(azCmd, [...cmdArguments, "--out", "json"], { encoding: "utf8", shell : true }, (error:any, stdout:any) => { + if (error) { + return reject(error); + } + try { + return resolve(JSON.parse(stdout)); + } catch (err:any) { + const msg = + `An error occurred while parsing the output "${stdout}", of ` + + `the cmd az "${cmdArguments}": ${err.message}.`; + return reject(new Error(msg)); + } + }); + }); + } + + isValid(scope: TokenScope) { + let token = scope == TokenScope.Dataplane ? this.dataPlanetoken : this.controlPlaneToken; + try { + let header = token && jwtDecode(token); + const now = Math.floor(Date.now() / 1000) + return (header && header?.exp && header.exp + 2 > now); + } + catch(error:any) { + console.log("Error in getting the token"); + } + } + + async getDataPlaneHeader(apicallType : CallTypeForDP) : Promise { + if(!this.isValid(TokenScope.Dataplane)) { + let tokenRes:any = await this.getTokenAPI(TokenScope.Dataplane); + this.dataPlanetoken = tokenRes; + } + let headers: IHeaders = { + 'content-type': ContentTypeMap[apicallType] ?? 'application/json', + 'Authorization': 'Bearer '+ this.dataPlanetoken + }; + return headers; + } + + async armTokenHeader() { + // right now only get calls from the GH, so no need of content type for now for the get calls. + var tokenRes:any = await this.getTokenAPI(TokenScope.ControlPlane); + this.controlPlaneToken = tokenRes; + let headers: IHeaders = { + 'Authorization': 'Bearer '+ this.controlPlaneToken, + }; + return headers; + } +} \ No newline at end of file diff --git a/src/models/FetchHelper.ts b/src/models/FetchHelper.ts new file mode 100644 index 00000000..0759dfc1 --- /dev/null +++ b/src/models/FetchHelper.ts @@ -0,0 +1,50 @@ +import { IHeaders, IHttpClientResponse } from 'typed-rest-client/Interfaces'; +import { ErrorCorrection, getResultObj, getUniqueId, sleep } from './util'; +import { correlationHeader } from './UtilModels'; +import * as httpc from 'typed-rest-client/HttpClient'; +import { uploadFileData } from './FileUtils'; +const httpClient: httpc.HttpClient = new httpc.HttpClient('MALT-GHACTION'); +import * as core from '@actions/core' + +// (note mohit): shift to the enum later. +export async function httpClientRetries(urlSuffix : string, header : IHeaders, method : 'get' | 'del' | 'patch' | 'put', retries : number = 1,data : string, isUploadCall : boolean = true, log: boolean = true) : Promise{ + let httpResponse : IHttpClientResponse; + try { + let correlationId = `gh-actions-${getUniqueId()}`; + header[correlationHeader] = correlationId; // even if we put console.debug its printing along with the logs, so lets just go ahead with the differentiation with azdo, so we can search the timeframe for azdo in correlationid and resource filter. + if(method == 'get'){ + httpResponse = await httpClient.get(urlSuffix, header); + } + else if(method == 'del'){ + httpResponse = await httpClient.del(urlSuffix, header); + } + else if(method == 'put' && isUploadCall){ + let fileContent = uploadFileData(data); + httpResponse = await httpClient.request(method,urlSuffix, fileContent, header); + } + else{ + httpResponse = await httpClient.request(method,urlSuffix, data, header); + } + if(httpResponse.message.statusCode!= undefined && httpResponse.message.statusCode >= 300){ + core.debug(`correlation id : ${correlationId}`); + } + if(httpResponse.message.statusCode!=undefined && [408,429,502,503,504].includes(httpResponse.message.statusCode)){ + let err = await getResultObj(httpResponse); + throw {message : (err && err.error && err.error.message) ? err.error.message : ErrorCorrection(httpResponse)}; // throwing as message to catch it as err.message + } + return httpResponse; + } + catch(err:any){ + if(retries){ + let sleeptime = (5-retries)*1000 + Math.floor(Math.random() * 5001); + if (log) { + console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime/1000} seconds`); + } + await sleep(sleeptime); + return await httpClientRetries(urlSuffix,header,method,retries-1,data); + } + else{ + throw new Error(`Operation did not succeed after 3 retries. Pipeline failed with error : ${err.message}`); + } + } +} \ No newline at end of file diff --git a/src/models/FileUtils.ts b/src/models/FileUtils.ts new file mode 100644 index 00000000..4ad844e6 --- /dev/null +++ b/src/models/FileUtils.ts @@ -0,0 +1,57 @@ +import path from 'path'; +import { resultFolder, resultZipFileName } from './UtilModels'; +import * as fs from 'fs'; +import { Readable } from 'stream'; + +export async function uploadFileToResultsFolder(response:any, fileName : string = resultZipFileName) { + try { + const filePath = path.join(resultFolder, fileName); + const file: NodeJS.WritableStream = fs.createWriteStream(filePath); + + return new Promise((resolve, reject) => { + file.on("error", (err) => reject(err)); + const stream = response.message.pipe(file); + stream.on("close", () => { + try { resolve(filePath); } catch (err) { + reject(err); + } + }); + }); + } + catch(err:any) { + err.message = "Error in fetching the results of the testRun"; + throw new Error(err); + } +} + +export function deleteFile(foldername:string) +{ + if (fs.existsSync(foldername)) + { + fs.readdirSync(foldername).forEach((file, index) => { + const curPath = path.join(foldername, file); + if (fs.lstatSync(curPath).isDirectory()) { + deleteFile(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(foldername); + } +} + +export function uploadFileData(filepath: string) { + try + { + let filedata : Buffer = fs.readFileSync(filepath); + const readable = new Readable(); + readable._read = () => {}; + readable.push(filedata); + readable.push(null); + return readable; + } + catch(err:any) { + err.message = "File not found "+ filepath; + throw new Error(err.message); + } +} \ No newline at end of file diff --git a/src/models/PayloadModels.ts b/src/models/PayloadModels.ts new file mode 100644 index 00000000..029a4430 --- /dev/null +++ b/src/models/PayloadModels.ts @@ -0,0 +1,142 @@ +import { TestKind } from "./engine/TestKind"; + +export class CertificateMetadata { + type?: string; + value?: string; + name?: string; +}; + +export interface RegionConfiguration { + region: string; + engineInstances: number; +}; + +export interface SecretMetadata { + type: string; + value: string; +}; + +export interface PassFailMetric { + aggregate: string; + clientMetric: string; + condition?: string; + action?: string | null; + requestName?: string | null; + value?: number; + actualValue?: number; + result?: string | null; +}; + +export interface TestModel { + testId?: string; + description?: string; + displayName?: string; + loadTestConfiguration?: LoadTestConfiguration; + passFailCriteria?: PassFailCriteria; + autoStopCriteria?: AutoStopCriteria | null; + createdDateTime?: string; + createdBy?: string; + lastModifiedDateTime?: string; + lastModifiedBy?: string; + inputArtifacts?: InputArtifacts; + secrets?: { [key: string]: SecretMetadata | null }; + certificate?: CertificateMetadata | null; + environmentVariables?: { [key: string]: string | null }; + subnetId?: string; + publicIPDisabled?: boolean; + keyvaultReferenceIdentityType?: string; + keyvaultReferenceIdentityId?: string| null; + metricsReferenceIdentityType?: string; + metricsReferenceIdentityId?: string; + engineBuiltinIdentityType?: string; + engineBuiltinIdentityIds?: string[]; + baselineTestRunId?: string; + kind?: TestKind; +}; + +export interface TestRunArtifacts { + inputArtifacts: InputArtifacts; + outputArtifacts: OutputArtifacts; +} + +export interface TestRunModel extends TestModel { + testRunId? : string; + errorDetails? : any; + testArtifacts?: TestRunArtifacts; + testResult: string; + status: string; + testRunStatistics : { [ key: string ] : Statistics }; + virtualUserHours?: number; + virtualUsers?: number; + startDateTime?: string; + endDateTime?: string; + portalUrl?: string; +} + +export interface Statistics { + errorCount?: number; + errorPct?: number; + minResTime?: number; + maxResTime?: number; + meanResTime?: number; + medianResTime?: number; + pct1ResTime?: number; + pct2ResTime?: number; + pct3ResTime?: number; + pct75ResTime?: number; + pct96ResTime?: number; + pct98ResTime?: number; + pct999ResTime?: number; + pct9999ResTime?: number; + sampleCount?: number; + throughput?: number; + transaction?: string; +} + +export interface AutoStopCriteria { + autoStopDisabled? : boolean; + errorRate ?: number; + errorRateTimeWindowInSeconds ?: number; +} + +export interface LoadTestConfiguration { + engineInstances?: number; + splitAllCSVs?: boolean; + quickStartTest?: boolean; + regionalLoadTestConfig?: RegionConfiguration[] | null; +}; + +export interface PassFailCriteria { + passFailMetrics?: { [key: string]: PassFailMetric | null }; +}; + +export interface InputArtifacts { + configFileInfo?: FileInfo; + testScriptFileInfo?: FileInfo; + additionalFileInfo?: FileInfo[]; + inputArtifactsZipFileInfo? : FileInfo; + userPropFileInfo?: FileInfo; + urlTestConfigFileInfo? : FileInfo; +}; + +export interface OutputArtifacts { + reportFileInfo?: FileInfo; + resultFileInfo?: FileInfo; + logsFileInfo?: FileInfo; + artifactsContainerInfo?: FileInfo; +} + +export interface FileInfo { + url?: string; + fileName?: string; + expireDateTime?: string; + fileType?: string; + validationStatus?: string; + validationFailureDetails?: string; +}; + +export interface ExistingParams { + secrets: { [key: string]: SecretMetadata | null }; + env: { [key: string]: string | null }; + passFailCriteria: { [key: string]: PassFailMetric | null }; +} \ No newline at end of file diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts new file mode 100644 index 00000000..7f059310 --- /dev/null +++ b/src/models/TaskModels.ts @@ -0,0 +1,350 @@ +import { isNullOrUndefined } from "util"; +const pathLib = require('path'); +import * as Util from './util'; +import * as EngineUtil from './engine/Util'; +import { TestKind } from "./engine/TestKind"; +import { BaseLoadTestFrameworkModel } from "./engine/BaseLoadTestFrameworkModel"; +const yaml = require('js-yaml'); +import * as fs from 'fs'; +import { AutoStopCriteria, AutoStopCriteria as autoStopCriteriaObjOut } from "./PayloadModels"; +import { AutoStopCriteriaObjYaml, ManagedIdentityType, ParamType, RunTimeParams } from "./UtilModels"; +import * as core from '@actions/core'; +import { PassFailMetric, ExistingParams, TestModel, CertificateMetadata, SecretMetadata, RegionConfiguration } from "./PayloadModels"; + +export class YamlConfig { + testId:string = ''; + displayName:string = ''; + description:string = ''; + testPlan: string = ''; + kind?: TestKind = TestKind.JMX; + engineInstances: number = 1; + subnetId?: string; + publicIPDisabled: boolean = false; + configurationFiles: string[] = []; + zipArtifacts: string[] = []; + splitAllCSVs: boolean = false; + propertyFile: string| null = null; + env: { [key: string]: string | null } = {}; + certificates: CertificateMetadata| null = null; + secrets: { [key: string] : SecretMetadata | null} = {}; + failureCriteria: { [key: string]: number } = {}; // this is yaml model. + passFailApiModel : { [key: string]: PassFailMetric | null } = {}; // this is api model. + autoStop: autoStopCriteriaObjOut | null = null; + keyVaultReferenceIdentity: string| null = null; + keyVaultReferenceIdentityType: ManagedIdentityType = ManagedIdentityType.SystemAssigned; + regionalLoadTestConfig: RegionConfiguration[] | null = null; + runTimeParams: RunTimeParams = {env: {}, secrets: {}, runDisplayName: '', runDescription: '', testId: '', testRunId: ''}; + + constructor() { + let yamlFile = core.getInput('loadTestConfigFile') ?? ''; + if(isNullOrUndefined(yamlFile) || yamlFile == ''){ + throw new Error(`The input field "loadTestConfigFile" is empty. Provide the path to load test yaml file.`); + } + + let yamlPath = yamlFile; + if(!(pathLib.extname(yamlPath) === ".yaml" || pathLib.extname(yamlPath) === ".yml")) + throw new Error("The Load Test configuration file should be of type .yaml or .yml"); + const config: any = yaml.load(fs.readFileSync(yamlPath, 'utf8')); + let validConfig : {valid : boolean, error :string} = Util.checkValidityYaml(config); + if(!validConfig.valid){ + throw new Error(validConfig.error + ` Refer to the load test YAML syntax at https://learn.microsoft.com/azure/load-testing/reference-test-config-yaml`); + } + this.testId = config.testId ?? config.testName; + this.testId = this.testId.toLowerCase(); + this.displayName = config.displayName ?? this.testId; + this.description = config.description; + this.engineInstances = config.engineInstances ?? 1; + let path = pathLib.dirname(yamlPath); + this.testPlan = pathLib.join(path,config.testPlan); + + this.kind = config.testType as TestKind ?? TestKind.JMX; + let framework : BaseLoadTestFrameworkModel = EngineUtil.getLoadTestFrameworkModelFromKind(this.kind); + + if(config.configurationFiles != undefined) { + var tempconfigFiles: string[]=[]; + tempconfigFiles = config.configurationFiles; + for(let file of tempconfigFiles){ + if(this.kind == TestKind.URL && !Util.checkFileType(file,'csv')){ + throw new Error("Only CSV files are allowed as configuration files for a URL-based test."); + } + file = pathLib.join(path,file); + this.configurationFiles.push(file); + }; + } + if(config.zipArtifacts != undefined){ + var tempconfigFiles: string[]=[]; + tempconfigFiles = config.zipArtifacts; + if(this.kind == TestKind.URL && tempconfigFiles.length > 0){ + throw new Error("Zip artifacts are not supported for the URL-based test."); + } + for(let file of tempconfigFiles){ + file = pathLib.join(path,file); + this.zipArtifacts.push(file); + }; + } + if(config.splitAllCSVs !=undefined){ + this.splitAllCSVs = config.splitAllCSVs; + } + if(config.failureCriteria != undefined) { + this.failureCriteria = Util.getPassFailCriteriaFromString(config.failureCriteria); + } + if (config.autoStop != undefined) { + this.autoStop = this.getAutoStopCriteria(config.autoStop); + } + if(config.subnetId != undefined) { + this.subnetId = (config.subnetId); + } + if(config.publicIPDisabled != undefined) { + this.publicIPDisabled = (config.publicIPDisabled) + } + if(config.properties != undefined && config.properties.userPropertyFile != undefined) + { + if(this.kind == TestKind.URL){ + throw new Error("User property file is not supported for the URL-based test."); + } + let propFile = config.properties.userPropertyFile; + this.propertyFile = pathLib.join(path,propFile); + if(!Util.checkFileTypes(config.properties.userPropertyFile, framework.userPropertyFileExtensions)){ + throw new Error(`User property file with extension other than ${framework.ClientResources.userPropertyFileExtensionsFriendly} is not permitted.`); + } + } + if(config.secrets != undefined) { + this.keyVaultReferenceIdentityType = ManagedIdentityType.SystemAssigned; + this.secrets = this.parseParameters(config.secrets, ParamType.secrets) as { [key: string]: SecretMetadata }; + } + if(config.env != undefined) { + this.env = this.parseParameters(config.env, ParamType.env) as { [key: string]: string }; + } + if(config.certificates != undefined){ + this.certificates = this.parseParameters(config.certificates, ParamType.cert) as CertificateMetadata | null; + } + if(config.keyVaultReferenceIdentity != undefined) { + this.keyVaultReferenceIdentityType = ManagedIdentityType.UserAssigned; + this.keyVaultReferenceIdentity = config.keyVaultReferenceIdentity; + } + if(config.regionalLoadTestConfig != undefined) { + this.regionalLoadTestConfig = this.getMultiRegionLoadTestConfig(config.regionalLoadTestConfig); + } + // commenting out for now, will re-write this logic with the changed options. + // if(config.engineBuiltInIdentityType != undefined) { + // engineBuiltInIdentityType = config.engineBuiltInIdentityType; + // } + // if(config.engineBuiltInIdentityIds != undefined) { + // engineBuiltInIdentityIds = config.engineBuiltInIdentityIds; + // } + if(this.testId === '' || isNullOrUndefined(this.testId) || this.testPlan === '' || isNullOrUndefined(this.testPlan)) { + throw new Error("The required fields testId/testPlan are missing in "+yamlPath+"."); + } + this.runTimeParams = this.getRunTimeParams(); + Util.validateTestRunParamsFromPipeline(this.runTimeParams); + } + + getRunTimeParams() { + var secretRun = core.getInput('secrets'); + let secretsParsed : {[key: string] : SecretMetadata} = {}; + let envParsed : {[key: string] : string} = {}; + if(secretRun) { + try { + var obj = JSON.parse(secretRun); + for (var index in obj) { + var val = obj[index]; + let str : string = `name : ${val.name}, value : ${val.value}`; + if(isNullOrUndefined(val.name)){ + throw new Error(`Invalid secret name at pipeline parameters at ${str}`); + } + secretsParsed[val.name] = {type: 'SECRET_VALUE',value: val.value}; + } + } + catch (error) { + console.log(error); + throw new Error("Invalid format of secrets in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); + } + } + var eRun = core.getInput('env'); + if(eRun) { + try { + var obj = JSON.parse(eRun); + for (var index in obj) { + var val = obj[index]; + let str : string = `name : ${val.name}, value : ${val.value}`; + if(isNullOrUndefined(val.name)){ + throw new Error(`Invalid environment name at pipeline parameters at ${str}`); + } + envParsed[val.name] = val.value; + } + } + catch (error) { + console.log(error); + throw new Error("Invalid format of env in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); + } + } + const runDisplayName = core.getInput('loadTestRunName') ?? Util.getDefaultTestRunName(); + const runDescription = core.getInput('loadTestRunDescription') ?? Util.getDefaultRunDescription(); + + let runTimeParams : RunTimeParams = {env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: ''}; + return runTimeParams; + } + + getFileName(filepath:string) { + var filename = pathLib.basename(filepath); + return filename; + } + + mergeExistingData(existingData:ExistingParams) { + let existingCriteria = existingData.passFailCriteria; + let existingCriteriaIds = Object.keys(existingCriteria); + var numberOfExistingCriteria = existingCriteriaIds.length; + var index = 0; + + for(var key in this.failureCriteria) { + var splitted = key.split(" "); + var criteriaId = index < numberOfExistingCriteria ? existingCriteriaIds[index++] : Util.getUniqueId(); + this.passFailApiModel[criteriaId] = { + clientMetric: splitted[0], + aggregate: splitted[1], + condition: splitted[2], + action : splitted[3], + value: this.failureCriteria[key], + requestName: splitted.length > 4 ? splitted.slice(4).join(' ') : null + }; + } + + for (; index < numberOfExistingCriteria; index++) { + this.passFailApiModel[existingCriteriaIds[index]] = null; + } + + let existingParams = existingCriteria.secrets; + for(var key in existingParams) { + if(!this.secrets.hasOwnProperty(key)) + this.secrets[key] = null; + } + var existingEnv = existingCriteria.env; + for(var key in existingEnv) { + if(!this.env.hasOwnProperty(key)) + this.env[key] = null; + } + } + + getCreateTestData(existingData:ExistingParams) { + this.mergeExistingData(existingData); + var data : TestModel = { + testId: this.testId, + description: this.description, + displayName: this.displayName, + loadTestConfiguration: { + engineInstances: this.engineInstances, + splitAllCSVs: this.splitAllCSVs, + regionalLoadTestConfig : this.regionalLoadTestConfig, + }, + secrets: this.secrets, + kind : this.kind, + certificate: this.certificates, + environmentVariables: this.env, + passFailCriteria:{ + passFailMetrics: this.passFailApiModel + }, + autoStopCriteria: this.autoStop, + subnetId: this.subnetId, + publicIPDisabled : this.publicIPDisabled, + keyvaultReferenceIdentityType: this.keyVaultReferenceIdentityType, + keyvaultReferenceIdentityId: this.keyVaultReferenceIdentity, + }; + return data; + } + + getStartTestData() { + this.runTimeParams.testId = this.testId; + this.runTimeParams.testRunId = Util.getUniqueId(); + return this.runTimeParams; + } + + getAutoStopCriteria(autoStopInput : AutoStopCriteriaObjYaml | string | null): AutoStopCriteria | null { + let autoStop: AutoStopCriteria | null; + if (autoStopInput == null) {autoStop = null; return autoStop;} + if (typeof autoStopInput == "string") { + if (autoStopInput == "disable") { + let data = { + autoStopDisabled : true, + errorRate: 90, + errorRateTimeWindowInSeconds: 60, + }; + autoStop = data; + } else { + throw new Error( + "Invalid value, for disabling auto stop use 'autoStop: disable'" + ); + } + } else { + let data = { + autoStopDisabled : false, + errorRate: autoStopInput.errorPercentage, + errorRateTimeWindowInSeconds: autoStopInput.timeWindow, + }; + autoStop = data; + } + return autoStop; + } + + parseParameters(obj:{name: string, value: string}[], type:ParamType) : {[key: string]: SecretMetadata | string}| CertificateMetadata | null{ + if(type == ParamType.secrets) { + let secretsParsed : {[key: string] : SecretMetadata} = {}; + for (var index in obj) { + var val = obj[index]; + let str : string = `name : ${val.name}, value : ${val.value}`; + if(isNullOrUndefined(val.name)){ + throw new Error(`Invalid secret name at ${str}`); + } + if(!Util.validateUrl(val.value)){ + throw new Error(`Invalid secret url at ${str}`); + } + secretsParsed[val.name] = {type: 'AKV_SECRET_URI',value: val.value}; + } + return secretsParsed; + } + + if(type == ParamType.env) { + let envParsed : {[key: string] : string} = {}; + for(var index in obj) { + let val = obj[index]; + let str : string = `name : ${val.name}, value : ${val.value}`; + if(isNullOrUndefined(val.name)){ + throw new Error(`Invalid environment name at ${str}`); + } + val = obj[index]; + envParsed[val.name] = val.value; + } + return envParsed; + } + + if(type == ParamType.cert){ + let cert : CertificateMetadata| null = null; + if(obj.length > 1){ + throw new Error(`Only one certificate can be added in the load test configuration.`); + } + if(obj.length == 1) { + let val = obj[0]; + let str : string = `name : ${val.name}, value : ${val.value}`; + if(isNullOrUndefined(val.name)){ + throw new Error(`Invalid certificate name at ${str}`); + } + if(!Util.validateUrlcert(val.value)) + throw new Error(`Invalid certificate url at ${str}`); + cert = {name: val.name ,type: 'AKV_CERT_URI',value: val.value}; + } + return cert; + } + return null; + } + + getMultiRegionLoadTestConfig(multiRegionalConfig : any[]) : RegionConfiguration[] { + let parsedMultiRegionConfiguration : RegionConfiguration[] = [] + multiRegionalConfig.forEach(regionConfig => { + let data = { + region: regionConfig.region, + engineInstances: regionConfig.engineInstances, + }; + parsedMultiRegionConfiguration.push(data); + }); + return parsedMultiRegionConfiguration; + } +} \ No newline at end of file diff --git a/src/models/UtilModels.ts b/src/models/UtilModels.ts new file mode 100644 index 00000000..5a0fe058 --- /dev/null +++ b/src/models/UtilModels.ts @@ -0,0 +1,88 @@ +import { SecretMetadata } from "./PayloadModels"; + +export interface AutoStopCriteriaObjYaml { + autoStopEnabled? : boolean; + errorPercentage ?: number; + timeWindow ?: number; +} + +export enum ParamType { + env = "env", + secrets = "secrets", + cert = "cert" +} + +export interface RunTimeParams { + env: { [key: string]: string }; + secrets: { [key: string] : SecretMetadata }; + runDisplayName: string; + runDescription: string; + testRunId: string; + testId: string; +} + +export enum TokenScope { + Dataplane, + ControlPlane +} + +export enum CallTypeForDP { + get, + patch, + put, + delete +} + +export interface PassFailCount { + pass: number; + fail: number; +} + +export const ContentTypeMap : { [key in CallTypeForDP]: string | null } = { + [CallTypeForDP.get]: null, + [CallTypeForDP.patch]: 'application/merge-patch+json', + [CallTypeForDP.put]: 'application/octet-stream', + [CallTypeForDP.delete]: 'application/json' +} + +export enum FileType{ + JMX_FILE = 'JMX_FILE', + USER_PROPERTIES = 'USER_PROPERTIES', + ADDITIONAL_ARTIFACTS = 'ADDITIONAL_ARTIFACTS', + ZIPPED_ARTIFACTS = "ZIPPED_ARTIFACTS", + URL_TEST_CONFIG = "URL_TEST_CONFIG", + TEST_SCRIPT = 'TEST_SCRIPT', +} + +export const resultFolder = 'loadTest'; +export const reportZipFileName = 'report.zip'; +export const resultZipFileName = 'results.zip'; +export const correlationHeader = 'x-ms-correlation-request-id'; + +export module ApiVersionConstants { + export const tm2024Version = '2024-05-01-preview'; + export const tm2023Version = '2023-04-01-preview'; + export const tm2022Version = '2022-11-01'; + export const cp2022Version = '2022-12-01' +} + +export const ValidAggregateList = { + 'response_time_ms': ['avg', 'min', 'max', 'p50', 'p75', 'p90', 'p95', 'p96', 'p97', 'p98', 'p99', 'p999', 'p9999'], + 'requests_per_sec': ['avg'], + 'requests': ['count'], + 'latency': ['avg', 'min', 'max', 'p50', 'p75', 'p90', 'p95', 'p96', 'p97', 'p98', 'p99', 'p999', 'p9999'], + 'error': ['percentage'] +} + +export const ValidConditionList = { + 'response_time_ms': ['>', '<'], + 'requests_per_sec': ['>', '<'], + 'requests': ['>', '<'], + 'latency': ['>', '<'], + 'error': ['>'] +} + +export enum ManagedIdentityType { + SystemAssigned = "SystemAssigned", + UserAssigned = "UserAssigned", +} \ No newline at end of file diff --git a/src/constants.ts b/src/models/constants.ts similarity index 100% rename from src/constants.ts rename to src/models/constants.ts diff --git a/src/engine/BaseLoadTestFrameworkModel.ts b/src/models/engine/BaseLoadTestFrameworkModel.ts similarity index 100% rename from src/engine/BaseLoadTestFrameworkModel.ts rename to src/models/engine/BaseLoadTestFrameworkModel.ts diff --git a/src/engine/JMeterFrameworkModel.ts b/src/models/engine/JMeterFrameworkModel.ts similarity index 100% rename from src/engine/JMeterFrameworkModel.ts rename to src/models/engine/JMeterFrameworkModel.ts diff --git a/src/engine/LocustFrameworkModel.ts b/src/models/engine/LocustFrameworkModel.ts similarity index 100% rename from src/engine/LocustFrameworkModel.ts rename to src/models/engine/LocustFrameworkModel.ts diff --git a/src/engine/TestKind.ts b/src/models/engine/TestKind.ts similarity index 100% rename from src/engine/TestKind.ts rename to src/models/engine/TestKind.ts diff --git a/src/engine/Util.ts b/src/models/engine/Util.ts similarity index 100% rename from src/engine/Util.ts rename to src/models/engine/Util.ts diff --git a/src/util.ts b/src/models/util.ts similarity index 67% rename from src/util.ts rename to src/models/util.ts index d6422688..111fbcdd 100644 --- a/src/util.ts +++ b/src/models/util.ts @@ -1,99 +1,13 @@ -import * as fs from 'fs'; -var path = require('path'); -var AdmZip = require("adm-zip"); +import { IHttpClientResponse } from 'typed-rest-client/Interfaces'; const { v4: uuidv4 } = require('uuid'); -import * as core from '@actions/core' -import httpc = require('typed-rest-client/HttpClient'); -import internal = require('stream'); -const httpClient: httpc.HttpClient = new httpc.HttpClient('MALT-GHACTION'); -import { IHttpClientResponse, IHeaders } from 'typed-rest-client/Interfaces'; -import { Readable } from 'stream'; -import { isNull, isUndefined, isNullOrUndefined } from 'util'; +import { isNull, isNullOrUndefined } from 'util'; import { defaultYaml } from './constants'; import * as EngineUtil from './engine/Util'; import { BaseLoadTestFrameworkModel } from './engine/BaseLoadTestFrameworkModel'; import { TestKind } from "./engine/TestKind"; +import { PassFailMetric, Statistics, TestRunArtifacts, TestRunModel, TestModel } from './PayloadModels'; +import { RunTimeParams, ValidAggregateList, ValidConditionList, ManagedIdentityType, PassFailCount } from './UtilModels'; -const validAggregateList = { - 'response_time_ms': ['avg', 'min', 'max', 'p50', 'p75', 'p90', 'p95', 'p96', 'p97', 'p98', 'p99', 'p999', 'p9999'], - 'requests_per_sec': ['avg'], - 'requests': ['count'], - 'latency': ['avg', 'min', 'max', 'p50', 'p75', 'p90', 'p95', 'p96', 'p97', 'p98', 'p99', 'p999', 'p9999'], - 'error': ['percentage'] -} - -const validConditionList = { - 'response_time_ms': ['>', '<'], - 'requests_per_sec': ['>', '<'], - 'requests': ['>', '<'], - 'latency': ['>', '<'], - 'error': ['>'] -} -export module apiConstants { - export const latestVersion = '2024-05-01-preview'; - export const tm2022Version = '2022-11-01'; - export const cp2022Version = '2022-12-01'; -} -export enum ManagedIdentityType { - SystemAssigned = "SystemAssigned", - UserAssigned = "UserAssigned", -} - -export function uploadFileData(filepath: string) { - try { - let filedata: Buffer = fs.readFileSync(filepath); - const readable = new Readable(); - readable._read = () => {}; - readable.push(filedata); - readable.push(null); - return readable; - } catch (err: any) { - err.message = "File not found " + filepath; - throw new Error(err.message); - } -} - -const correlationHeader = 'x-ms-correlation-request-id' -export async function httpClientRetries(urlSuffix : string, header : IHeaders, method : 'get' | 'del' | 'patch' | 'put', retries : number = 1, data : string, isUploadCall : boolean = true, log: boolean = true) : Promise{ - let httpResponse : IHttpClientResponse; - try { - let correlationId = `gh-actions-${getUniqueId()}`; - header[correlationHeader] = correlationId; // even if we put console.debug its printing along with the logs, so lets just go ahead with the differentiation with GH-actions, so we can search the timeframe for GH-actions in correlationid and resource filter. - if(method == 'get'){ - httpResponse = await httpClient.get(urlSuffix, header); - } - else if(method == 'del'){ - httpResponse = await httpClient.del(urlSuffix, header); - } - else if(method == 'put' && isUploadCall){ - let fileContent = uploadFileData(data); - httpResponse = await httpClient.request(method,urlSuffix, fileContent, header); - } - else{ - httpResponse = await httpClient.request(method,urlSuffix, data, header); - } - if(httpResponse.message.statusCode!= undefined && httpResponse.message.statusCode >= 300){ - core.debug(`correlation id : ${correlationId}`); - } - if(httpResponse.message.statusCode!=undefined && [408,429,502,503,504].includes(httpResponse.message.statusCode)){ - let err = await getResultObj(httpResponse); - throw {message : (err && err.error && err.error.message) ? err.error.message : ErrorCorrection(httpResponse)}; // throwing as message to catch it as err.message - } - return httpResponse; - } - catch(err:any){ - if(retries){ - let sleeptime = (5-retries)*1000 + Math.floor(Math.random() * 5001); - await sleep(sleeptime); - if (log) { - console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime/1000} seconds`); - } - return httpClientRetries(urlSuffix,header,method,retries-1,data); - } - else - throw new Error(`Operation did not succeed after 3 retries. Pipeline failed with error : ${err.message}`); - } -} export function checkFileType(filePath: string, fileExtToValidate: string): boolean{ if(isNullOrUndefined(filePath)){ return false; @@ -111,27 +25,30 @@ export function checkFileTypes(filePath: string, fileExtsToValidate: string[]): return fileExtsToValidateLower.includes(split[split.length-1]?.toLowerCase()); } -export async function printTestDuration(vusers:string, startTime:Date, endTime : Date, testStatus : string) -{ +export async function printTestDuration(testRunObj: TestRunModel) { console.log("Summary generation completed\n"); console.log("-------------------Summary ---------------"); - console.log("TestRun start time: "+ startTime); - console.log("TestRun end time: "+ endTime); - console.log("Virtual Users: "+ vusers); - console.log(`TestStatus: ${testStatus} \n`); + console.log("TestRun start time: "+ new Date(testRunObj.startDateTime ?? new Date())); + console.log("TestRun end time: "+ new Date(testRunObj.endDateTime ?? new Date())); + console.log("Virtual Users: "+ testRunObj.virtualUsers); + console.log("TestStatus: "+ testRunObj.status + "\n"); return; } -export function printCriteria(criteria:any) { + +export function printCriteria(criteria:{ [key: string]: PassFailMetric | null }) { if(Object.keys(criteria).length == 0) return; printTestResult(criteria); - console.log("Criteria\t\t\t\t\t :Actual Value\t Result"); + console.log("Criteria\t\t\t\t\t :Actual Value\t Result"); for(var key in criteria) { - var metric = criteria[key]; + let metric = criteria[key]; + if(isNullOrUndefined(metric)) continue; + var str = metric.aggregate+"("+metric.clientMetric+") "+ metric.condition+ ' '+metric.value; if(metric.requestName != null){ str = metric.requestName + ": " + str; } + //str += ((metric.clientmetric == "error") ? ", " : "ms, ") + metric.action; var spaceCount = 50 - str.length; while(spaceCount > 0){ str+=' '; @@ -146,50 +63,29 @@ export function printCriteria(criteria:any) { } console.log("\n"); } -function printTestResult(criteria:any) { + +export function ErrorCorrection(result : IHttpClientResponse){ + return "Unable to fetch the response. Please re-run or contact support if the issue persists. " + "Status code :" + result.message.statusCode ; +} + +function printTestResult(criteria:{ [key: string] :PassFailMetric | null}) : PassFailCount { let pass = 0; let fail = 0; for(var key in criteria) { - if(criteria[key].result == "passed") + if(criteria[key]?.result == "passed") pass++; - else if(criteria[key].result == "failed") + else if(criteria[key]?.result == "failed") fail++; } console.log("-------------------Test Criteria ---------------"); - console.log("Results\t\t\t :"+pass+" Pass "+fail+" Fail\n"); -} -export async function uploadFileToResultsFolder(response:any,fileName : string = 'results.zip') -{ - try { - const filePath = path.join('loadTest',fileName); - const file: NodeJS.WritableStream = fs.createWriteStream(filePath); - - return new Promise((resolve, reject) => { - file.on("error", (err) => reject(err)); - const stream = response.message.pipe(file); - stream.on("close", () => { - try { resolve(filePath); } catch (err) { - reject(err); - } - }); - }); - } - catch(err:any) { - err.message = "Error in fetching the results of the testRun"; - throw new Error(err); - } -} -export async function printClientMetrics(obj:any) { - if(Object.keys(obj).length == 0) - return; - console.log("------------------Client-side metrics------------\n"); - for(var key in obj) { - printMetrics(obj[key], key); - } + console.log("Results\t\t\t :"+pass+" Pass "+fail+" Fail\n"); + return {pass, fail}; // returning so that we can use this in the UTs later. } -function printMetrics(data:any, key : string | null = null) { - let samplerName : string = data.transaction ?? key; + + +function printMetrics(data: Statistics, key : string | null = null) { + let samplerName : string | null = data.transaction ?? key; if(samplerName == 'Total'){ samplerName = "Aggregate"; } @@ -202,7 +98,16 @@ function printMetrics(data:any, key : string | null = null) { console.log("\n"); } -function getAbsVal(data:any) { +export async function printClientMetrics( obj:{ [key: string]: Statistics }) { + if(Object.keys(obj).length == 0) + return; + console.log("------------------Client-side metrics------------\n"); + for(var key in obj) { + printMetrics(obj[key], key); + } +} + +function getAbsVal(data: number| undefined | null) { if(isNullOrUndefined(data)) { return "undefined"; } @@ -211,50 +116,151 @@ function getAbsVal(data:any) { return dataArray[0]; } -export function sleep(ms:any) { +export function sleep(ms:number) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } export function getUniqueId() { - return uuidv4().toString(); + return uuidv4(); +} + +export function getResultFolder(testArtifacts:TestRunArtifacts | undefined) { + if(isNullOrUndefined(testArtifacts) || isNullOrUndefined(testArtifacts.outputArtifacts)) + return null; + var outputurl = testArtifacts.outputArtifacts; + return !isNullOrUndefined(outputurl.resultFileInfo) ? outputurl.resultFileInfo.url: null; } + +export function getReportFolder(testArtifacts:TestRunArtifacts | undefined) { + if(isNullOrUndefined(testArtifacts) || isNullOrUndefined(testArtifacts.outputArtifacts)) + return null; + var outputurl = testArtifacts.outputArtifacts; + return !isNullOrUndefined(outputurl.reportFileInfo) ? outputurl.reportFileInfo.url: null; +} + +export function indexOfFirstDigit(input: string) { + let i = 0; + for (; input[i] < '0' || input[i] > '9'; i++); + return i == input.length ? -1 : i; +} +export function removeUnits(input:string) +{ + let i = 0; + for (; input[i] >= '0' && input[i] <= '9'; i++); + return i == input.length ? input : input.substring(0,i); +} + +export function isTerminalTestStatus(testStatus: string){ + if(testStatus == "DONE" || testStatus === "FAILED" || testStatus === "CANCELLED"){ + return true; + } + return false; +} + +export function isStatusFailed(testStatus: string){ + if(testStatus === "FAILED" || testStatus === "CANCELLED"){ + return true; + } + return false; +} + +export function validCriteria(data:any) { + switch(data.clientMetric) { + case "response_time_ms": + return validResponseTimeCriteria(data); + case "requests_per_sec": + return validRequestsPerSecondCriteria(data); + case "requests": + return validRequestsCriteria(data); + case "latency": + return validLatencyCriteria(data); + case "error": + return validErrorCriteria(data); + default: + return false; + } +} + +function validResponseTimeCriteria(data:any) { + return !(!ValidAggregateList['response_time_ms'].includes(data.aggregate) || !ValidConditionList['response_time_ms'].includes(data.condition) + || (data.value).indexOf('.')!=-1 || data.action!= "continue"); +} +function validRequestsPerSecondCriteria(data:any) { + return !(!ValidAggregateList['requests_per_sec'].includes(data.aggregate) || !ValidConditionList['requests_per_sec'].includes(data.condition) + || data.action!= "continue"); +} +function validRequestsCriteria(data:any) { + return !(!ValidAggregateList['requests'].includes(data.aggregate) || !ValidConditionList['requests'].includes(data.condition) + || (data.value).indexOf('.')!=-1 || data.action!= "continue"); +} +function validLatencyCriteria(data:any) { + return !(!ValidAggregateList['latency'].includes(data.aggregate) || !ValidConditionList['latency'].includes(data.condition) + || (data.value).indexOf('.')!=-1 || data.action!= "continue"); +} +function validErrorCriteria(data:any) { + return !(!ValidAggregateList['error'].includes(data.aggregate) || !ValidConditionList['error'].includes(data.condition) + || Number(data.value)<0 || Number(data.value)>100 || data.action!= "continue"); +} + +export async function getResultObj(data:IHttpClientResponse) { + let dataString ; + let dataJSON ; + try{ + dataString = await data.readBody(); + dataJSON = JSON.parse(dataString); + return dataJSON; + } + catch{ + return null; + } +} + function isDictionary(variable: any): variable is { [key: string]: any } { return typeof variable === 'object' && variable !== null && !Array.isArray(variable); } -export function invalidName(value:string) + +function invalidName(value:string) { if(value.length < 2 || value.length > 50) return true; var r = new RegExp(/[^a-z0-9_-]+/); return r.test(value); } + export function invalidDisplayName(value : string){ if(value.length < 2 || value.length > 50) return true; return false; } + export function invalidDescription(value : string){ if(value.length > 100) return true; return false; } + function isInValidSubnet(uri: string): boolean { const pattern = /^\/subscriptions\/[a-f0-9-]+\/resourceGroups\/[a-zA-Z0-9\u0080-\uFFFF()._-]+\/providers\/Microsoft\.Network\/virtualNetworks\/[a-zA-Z0-9._-]+\/subnets\/[a-zA-Z0-9._-]+$/i; return !(pattern.test(uri)); } + function isInValidKVId(uri: string): boolean { const pattern = /^\/subscriptions\/[a-f0-9-]+\/resourceGroups\/[a-zA-Z0-9\u0080-\uFFFF()._-]+\/providers\/Microsoft\.ManagedIdentity\/userAssignedIdentities\/[a-zA-Z0-9._-]+$/i; return !(pattern.test(uri)); } + function isValidTestKind(value: string): value is TestKind { return Object.values(TestKind).includes(value as TestKind); } + function isValidManagedIdentityType(value: string): value is ManagedIdentityType { return Object.values(ManagedIdentityType).includes(value as ManagedIdentityType); } + function isArrayOfStrings(variable: any): variable is string[] { return Array.isArray(variable) && variable.every((item) => typeof item === 'string'); } + function inValidEngineInstances(engines : number) : boolean{ if(engines > 400 || engines < 1){ return true; @@ -303,13 +309,13 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s if(givenYaml.engineInstances && (isNaN(givenYaml.engineInstances) || inValidEngineInstances(givenYaml.engineInstances))){ return {valid : false, error : `The value "${givenYaml.engineInstances}" for engineInstances is invalid. The value should be an integer between 1 and 400.`}; } - + let kind : TestKind = givenYaml.testType ?? TestKind.JMX; - + if(!isValidTestKind(kind)){ return {valid : false, error : `The value "${kind}" for testType is invalid. Acceptable values are ${EngineUtil.Resources.Strings.allFrameworksFriendly}.`}; } - + let framework : BaseLoadTestFrameworkModel = EngineUtil.getLoadTestFrameworkModelFromKind(kind); if(givenYaml.testType as TestKind == TestKind.URL){ if(!checkFileType(givenYaml.testPlan,'json')) { @@ -375,7 +381,7 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s if(givenYaml.regionalLoadTestConfig.length < 2){ return {valid : false, error : `Multi-region load tests should contain a minimum of 2 geographic regions in the configuration.`}; } - + var totalEngineCount = 0; for(let i = 0; i < givenYaml.regionalLoadTestConfig.length; i++){ if(isNullOrUndefined(givenYaml.regionalLoadTestConfig[i].region) || typeof givenYaml.regionalLoadTestConfig[i].region != 'string' || givenYaml.regionalLoadTestConfig[i].region == ""){ @@ -394,123 +400,150 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s return {valid : true, error : ""}; } -export function getResultFolder(testArtifacts:any) { - if(testArtifacts == null || testArtifacts.outputArtifacts == null) - return null; - var outputurl = testArtifacts.outputArtifacts; - return (outputurl.resultFileInfo != null)? outputurl.resultFileInfo.url: null; -} +/* + ado takes the full pf criteria as a string after parsing the string into proper data model, +*/ +export function getPassFailCriteriaFromString(passFailCriteria: (string | {[key: string]: string})[]): { [key: string]: number } { + let failureCriteriaValue : {[key: string] : number} = {}; + passFailCriteria.forEach(criteria => { + let criteriaString = criteria as string; + let data = { + aggregate: "", + clientMetric: "", + condition: "", + value: "", + requestName: "", + action: "", + } + if(typeof criteria !== "string"){ + let request = Object.keys(criteria)[0] + data.requestName = request; + criteriaString = criteria[request] + } + let tempStr: string = ""; + for(let i=0; i'){ + failureCriteriaValue[key] = (valcurrVal) ? val : currVal; + } } -export function deleteFile(foldername:string) +export function validateUrl(url:string) { - if (fs.existsSync(foldername)) - { - fs.readdirSync(foldername).forEach((file, index) => { - const curPath = path.join(foldername, file); - if (fs.lstatSync(curPath).isDirectory()) { - deleteFile(curPath); - } else { - fs.unlinkSync(curPath); - } - }); - fs.rmdirSync(foldername); - } + var r = new RegExp(/(http|https):\/\/.*\/secrets\/.+$/); + return r.test(url); } -export function indexOfFirstDigit(input: string) { - let i = 0; - for (; input[i] < '0' || input[i] > '9'; i++); - return i == input.length ? -1 : i; - } -export function removeUnits(input:string) +export function validateUrlcert(url:string) { - let i = 0; - for (; input[i] >= '0' && input[i] <= '9'; i++); - return i == input.length ? input : input.substring(0,i); -} -export function isTerminalTestStatus(testStatus: string){ - if(testStatus === "DONE" || testStatus === "FAILED" || testStatus === "CANCELLED"){ - return true; - } - return false; -} -export function validCriteria(data:any) { - switch(data.clientMetric) { - case "response_time_ms": - return validResponseTimeCriteria(data); - case "requests_per_sec": - return validRequestsPerSecondCriteria(data); - case "requests": - return validRequestsCriteria(data); - case "latency": - return validLatencyCriteria(data); - case "error": - return validErrorCriteria(data); - default: - return false; - } + var r = new RegExp(/(http|https):\/\/.*\/certificates\/.+$/); + return r.test(url); } -function validResponseTimeCriteria(data:any) { - return !(!validAggregateList['response_time_ms'].includes(data.aggregate) || !validConditionList['response_time_ms'].includes(data.condition) - || (data.value).indexOf('.')!=-1 || data.action!= "continue"); +export function getDefaultTestName() +{ + const a = (new Date(Date.now())).toLocaleString() + const b = a.split(", ") + const c = a.split(" ") + return "Test_"+b[0]+"_"+c[1]+c[2] } -function validRequestsPerSecondCriteria(data:any) { - return !(!validAggregateList['requests_per_sec'].includes(data.aggregate) || !validConditionList['requests_per_sec'].includes(data.condition) - || data.action!= "continue"); -} -function validRequestsCriteria(data:any) { - return !(!validAggregateList['requests'].includes(data.aggregate) || !validConditionList['requests'].includes(data.condition) - || (data.value).indexOf('.')!=-1 || data.action!= "continue"); -} -function validLatencyCriteria(data:any) { - return !(!validAggregateList['latency'].includes(data.aggregate) || !validConditionList['latency'].includes(data.condition) - || (data.value).indexOf('.')!=-1 || data.action!= "continue"); -} -function validErrorCriteria(data:any) { - return !(!validAggregateList['error'].includes(data.aggregate) || !validConditionList['error'].includes(data.condition) - || Number(data.value)<0 || Number(data.value)>100 || data.action!= "continue"); +export function getDefaultTestRunName() +{ + const a = (new Date(Date.now())).toLocaleString() + const b = a.split(", ") + const c = a.split(" ") + return "TestRun_"+b[0]+"_"+c[1]+c[2] } -export async function getResultObj(data:any) { - var dataString ; - var dataJSON ; - try{ - dataString = await data.readBody(); - dataJSON = JSON.parse(dataString); - return dataJSON; - } - catch{ - return null; - } + +export function getDefaultRunDescription() +{ + return "Started using GitHub Actions" } -export function ErrorCorrection(result : IHttpClientResponse){ - return "Unable to fetch the response. Please re-run or contact support if the issue persists. " + "Status code: " + result.message.statusCode ; + +export function validateTestRunParamsFromPipeline(runTimeParams: RunTimeParams){ + if(runTimeParams.runDisplayName && invalidDisplayName(runTimeParams.runDisplayName)) + throw new Error("Invalid test run name. Test run name must be between 2 to 50 characters."); + if(runTimeParams.runDescription && invalidDescription(runTimeParams.runDescription)) + throw new Error("Invalid test run description. Test run description must be less than 100 characters.") } -export function getAllFileErrors(testObj:any): { [key: string]: string } { - var allArtifacts:any[] = []; - for (var key in testObj.inputArtifacts) { - var artifacts = testObj.inputArtifacts[key]; - if (artifacts instanceof Array ) { - allArtifacts = allArtifacts.concat(artifacts.filter((artifact:any) => artifact !== null && artifact !== undefined)); - } - else if (artifacts !== null && artifacts !== undefined) { - allArtifacts.push(artifacts); - } - } +export function getAllFileErrors(testObj:TestModel | null): { [key: string]: string } { + let allArtifacts:any[] = []; + let additionalArtifacts = testObj?.inputArtifacts?.additionalFileInfo; + additionalArtifacts && (allArtifacts = allArtifacts.concat(additionalArtifacts.filter((artifact:any) => artifact !== null && artifact !== undefined))); - var fileErrors: { [key: string]: string } = {}; + let testScript = testObj?.inputArtifacts?.testScriptFileInfo; + testScript && allArtifacts.push(testScript); + + let configFile = testObj?.inputArtifacts?.configFileInfo; + configFile && allArtifacts.push(configFile); + + let userProperties = testObj?.inputArtifacts?.userPropFileInfo; + userProperties && allArtifacts.push(userProperties); + + let zipFile = testObj?.inputArtifacts?.inputArtifactsZipFileInfo; + zipFile && allArtifacts.push(zipFile); + + let urlFile = testObj?.inputArtifacts?.urlTestConfigFileInfo; + urlFile && allArtifacts.push(urlFile); + + let fileErrors: { [key: string]: string } = {}; for (const file of allArtifacts) { if (file.validationStatus === "VALIDATION_FAILURE") { fileErrors[file.fileName] = file.validationFailureDetails; } } + return fileErrors; } diff --git a/src/services/FeatureFlagService.ts b/src/services/FeatureFlagService.ts index 125b8933..b9fd047b 100644 --- a/src/services/FeatureFlagService.ts +++ b/src/services/FeatureFlagService.ts @@ -1,24 +1,27 @@ import { FeatureFlags } from "./FeatureFlags"; import { Definitions } from "../models/APIResponseModel"; -import { APIRoute } from "../constants"; -import * as map from '../mappers'; -import * as util from '../util'; -import { IHeaders } from "typed-rest-client/Interfaces"; +import { APIRoute } from "../models/constants"; +import * as util from '../models/util'; +import { AuthenticationUtils } from "../models/AuthenticationUtils"; +import { CallTypeForDP } from "../models/UtilModels"; +import * as FetchUtil from './../models/FetchHelper'; export class FeatureFlagService { - private static featureFlagCache: { [key: string]: boolean } = {}; + featureFlagCache: { [key: string]: boolean } = {}; + authContext: AuthenticationUtils; - public static async getFeatureFlagAsync(flag: FeatureFlags, baseUrl: string, useCache: boolean = true): Promise { + constructor(authContext: AuthenticationUtils) { + this.authContext = authContext; + } + + async getFeatureFlagAsync(flag: FeatureFlags, baseUrl: string, useCache: boolean = true): Promise { if (useCache && flag in this.featureFlagCache) { return {featureFlag: flag, enabled: this.featureFlagCache[flag.toString()]}; } let uri: string = baseUrl + APIRoute.FeatureFlags(flag.toString()); - let headers: IHeaders = { - 'content-type': 'application/json', - 'Authorization': 'Bearer '+ map.getToken() - }; - let flagResponse = await util.httpClientRetries(uri, headers, 'get', 3, "", false, false); + let headers = this.authContext.getDataPlaneHeader(CallTypeForDP.get); + let flagResponse = await FetchUtil.httpClientRetries(uri, headers, 'get', 3, "", false, false); try { let flagObj = (await util.getResultObj(flagResponse)) as Definitions["FeatureFlagResponse"]; this.featureFlagCache[flag.toString()] = flagObj.enabled; @@ -33,7 +36,7 @@ export class FeatureFlagService { } } - public static async isFeatureEnabledAsync(flag: FeatureFlags, baseUrl: string, useCache: boolean = true): Promise { + async isFeatureEnabledAsync(flag: FeatureFlags, baseUrl: string, useCache: boolean = true): Promise { let flagObj = await this.getFeatureFlagAsync(flag, baseUrl, useCache); return flagObj ? flagObj.enabled : false; } diff --git a/test/Utils/checkForValidationOfYaml.test.ts b/test/Utils/checkForValidationOfYaml.test.ts index 2ae7369f..a3d75272 100644 --- a/test/Utils/checkForValidationOfYaml.test.ts +++ b/test/Utils/checkForValidationOfYaml.test.ts @@ -1,4 +1,4 @@ -import { checkValidityYaml, getAllFileErrors } from '../../src/util' +import { checkValidityYaml, getAllFileErrors } from '../../src/models/util' import * as constants from './testYamls'; describe('invalid Yaml tests', () =>{