diff --git a/.github/workflows/pr_check_load_test.yml b/.github/workflows/pr_check_load_test.yml index 2a1973dd..c750259a 100644 --- a/.github/workflows/pr_check_load_test.yml +++ b/.github/workflows/pr_check_load_test.yml @@ -53,9 +53,7 @@ jobs: run: | npm install --include=dev -f npm ci - npm install typescript -g - tsc - # (note:mohit) - generally typescript is ignored somehow in the installation and now we want to ignore the js files for main branch hence we need tsc and npm install seperately. + - name: Azure authentication uses: azure/login@v1 continue-on-error: false @@ -73,23 +71,21 @@ jobs: echo "::set-output name=GUID::$(uuidgen)" fi shell : bash - - - name: Set up testName - shell: bash - run: | - # copying the files to a new file and editing the testId for each file in the copied file. - # replacing 'testId'(the thing present in the file.) with the new testId ('${{ steps.guid.outputs.GUID }}') in the copied file. - cp ${{ matrix.configFile }} test-config-${{ steps.guid.outputs.GUID }}.yaml - sed -i 's/testId/${{ steps.guid.outputs.GUID }}/g' test-config-${{ steps.guid.outputs.GUID }}.yaml - + - name: 'Azure Load Testing' uses: ./ + id: alt with: - loadTestConfigFile: test-config-${{ steps.guid.outputs.GUID }}.yaml + loadTestConfigFile: ${{ matrix.configFile }} loadTestResource: ${{ env.LOAD_TEST_RESOURCE }} resourceGroup: ${{ env.LOAD_TEST_RESOURCE_GROUP }} + overRideParameters: "{\"testId\":\"${{ steps.guid.outputs.GUID }}\"}" + outputVariableName: 'loadTestRunId' continue-on-error: true + - name: Print the Output + run: echo "The Test ID is ${{ steps.alt.outputs['loadTestRunId.testRunId'] }}" + - name: Check for results and report files run: | if [[ -d "./loadTest" ]]; then diff --git a/.gitignore b/.gitignore index 7ce7a18b..71397229 100644 --- a/.gitignore +++ b/.gitignore @@ -334,4 +334,3 @@ node_modules/ .vscode TmpFiles loadTest -lib diff --git a/action.yml b/action.yml index 6f01bfb5..fdcaf7a6 100644 --- a/action.yml +++ b/action.yml @@ -22,9 +22,17 @@ inputs: env: description: 'Enter env in JSON' required: false + overrideParameters: + description: 'Override parameters in the YAML config file using the JSON format with testId, displayName, description, engineInstances, autoStop supported.' + required: false + outputVariableName: + description: 'Name of the output variable that stores the test run ID for use in subsequent tasks.' + required: false + branding: icon: 'extension-icon.svg' color: 'blue' runs: using: 'node20' main: 'lib/main.js' + post: 'lib/postProcessJob.js' diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 00000000..956d4c83 --- /dev/null +++ b/lib/main.js @@ -0,0 +1,67 @@ +"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 }); +const util = __importStar(require("./models/FileUtils")); +const UtilModels_1 = require("./models/UtilModels"); +const fs = __importStar(require("fs")); +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 { + 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(); + core.exportVariable(UtilModels_1.PostTaskParameters.baseUri, apiSupport.baseURL); + yield apiSupport.getTestAPI(false); + if (fs.existsSync(UtilModels_1.resultFolder)) { + util.deleteFile(UtilModels_1.resultFolder); + } + fs.mkdirSync(UtilModels_1.resultFolder); + yield apiSupport.createTestAPI(); + let outputVar = { + testRunId: yamlConfig.runTimeParams.testRunId + }; + core.setOutput(`${yamlConfig.outputVariableName}.${UtilModels_1.OutPutVariablesConstants.testRunId}`, outputVar.testRunId); + } + catch (err) { + core.setFailed(err.message); + } + }); +} +run(); diff --git a/lib/models/APIResponseModel.js b/lib/models/APIResponseModel.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/lib/models/APIResponseModel.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/models/APISupport.js b/lib/models/APISupport.js new file mode 100644 index 00000000..c49053d4 --- /dev/null +++ b/lib/models/APISupport.js @@ -0,0 +1,546 @@ +"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.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")); +const InputConstants = __importStar(require("./InputConstants")); +class APISupport { + constructor(authContext, yamlModel) { + this.baseURL = ''; + this.existingParams = { secrets: {}, env: {}, passFailCriteria: {}, appComponents: new Map() }; + 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, UtilModels_1.FetchCallType.get, 3, ""); + let resource_name = core.getInput(InputConstants.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, returnTestObj = false) { + var _a, _b, _c, _d; + return __awaiter(this, void 0, void 0, function* () { + var urlSuffix = "tests/" + this.testId + "?api-version=" + UtilModels_1.ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL + urlSuffix; + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.get); + let testResult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.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 errorObj = yield Util.getResultObj(testResult); + let err = ((_a = errorObj === null || errorObj === void 0 ? void 0 : errorObj.error) === null || _a === void 0 ? void 0 : _a.message) ? (_b = errorObj === null || errorObj === void 0 ? void 0 : errorObj.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; + } + } + } + }); + } + getAppComponents() { + var _a, _b, _c; + return __awaiter(this, void 0, void 0, function* () { + let urlSuffix = "tests/" + this.testId + "/app-components/" + "?api-version=" + UtilModels_1.ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL + urlSuffix; + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.get); + let appComponentsResult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.get, 3, ""); + if (appComponentsResult.message.statusCode == 200) { + let appComponentsObj = yield Util.getResultObj(appComponentsResult); + for (let guid in appComponentsObj.components) { + let resourceId = (_b = (_a = appComponentsObj.components[guid]) === null || _a === void 0 ? void 0 : _a.resourceId) !== null && _b !== void 0 ? _b : ""; + if (this.existingParams.appComponents.has(resourceId === null || resourceId === void 0 ? void 0 : resourceId.toLowerCase())) { + let existingGuids = (_c = this.existingParams.appComponents.get(resourceId === null || resourceId === void 0 ? void 0 : resourceId.toLowerCase())) !== null && _c !== void 0 ? _c : []; + existingGuids.push(guid); + this.existingParams.appComponents.set(resourceId.toLowerCase(), existingGuids); + } + else { + this.existingParams.appComponents.set(resourceId.toLowerCase(), [guid]); + } + } + } + }); + } + getServerMetricsConfig() { + return __awaiter(this, void 0, void 0, function* () { + let urlSuffix = "tests/" + this.testId + "/server-metrics-config/" + "?api-version=" + UtilModels_1.ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL + urlSuffix; + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.get); + let serverComponentsResult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.get, 3, ""); + if (serverComponentsResult.message.statusCode == 200) { + let serverComponentsObj = yield Util.getResultObj(serverComponentsResult); + this.yamlModel.mergeExistingServerCriteria(serverComponentsObj); + } + }); + } + deleteFileAPI(filename) { + return __awaiter(this, void 0, void 0, function* () { + var urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL + urlSuffix; + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.delete); + let delFileResult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.delete, 3, ""); + if (delFileResult.message.statusCode != 204) { + let errorObj = yield Util.getResultObj(delFileResult); + let Message = errorObj ? errorObj.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.latestVersion; + urlSuffix = this.baseURL + urlSuffix; + let createData = this.yamlModel.getCreateTestData(this.existingParams); + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.patch); + let createTestresult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.patch, 3, JSON.stringify(createData)); + if (createTestresult.message.statusCode != 200 && createTestresult.message.statusCode != 201) { + let errorObj = yield Util.getResultObj(createTestresult); + console.log(errorObj ? errorObj : 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(); + }); + } + patchAppComponents() { + return __awaiter(this, void 0, void 0, function* () { + let urlSuffix = "tests/" + this.testId + "/app-components/" + "?api-version=" + UtilModels_1.ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL + urlSuffix; + let appComponentsData = this.yamlModel.getAppComponentsData(); + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.patch); + let appComponentsResult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.patch, 3, JSON.stringify(appComponentsData)); + if (!(0, util_1.isNullOrUndefined)(appComponentsData === null || appComponentsData === void 0 ? void 0 : appComponentsData.components) && Object.keys(appComponentsData.components).length == 0) { + return; + } + if (appComponentsResult.message.statusCode != 200 && appComponentsResult.message.statusCode != 201) { + let errorObj = yield Util.getResultObj(appComponentsResult); + console.log(errorObj ? errorObj : Util.ErrorCorrection(appComponentsResult)); + throw new Error("Error in updating app components"); + } + else { + console.log("Updated app components successfully"); + yield this.getServerMetricsConfig(); + yield this.patchServerMetrics(); + } + }); + } + patchServerMetrics() { + return __awaiter(this, void 0, void 0, function* () { + let urlSuffix = "tests/" + this.testId + "/server-metrics-config/" + "?api-version=" + UtilModels_1.ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL + urlSuffix; + let serverMetricsData = { + metrics: this.yamlModel.serverMetricsConfig + }; + if (!(0, util_1.isNullOrUndefined)(serverMetricsData === null || serverMetricsData === void 0 ? void 0 : serverMetricsData.metrics) && Object.keys(serverMetricsData.metrics).length == 0) { + return; + } + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.patch); + let serverMetricsResult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.patch, 3, JSON.stringify(serverMetricsData)); + if (serverMetricsResult.message.statusCode != 200 && serverMetricsResult.message.statusCode != 201) { + let errorObj = yield Util.getResultObj(serverMetricsResult); + console.log(errorObj ? errorObj : Util.ErrorCorrection(serverMetricsResult)); + throw new Error("Error in updating server metrics"); + } + else { + console.log("Updated server metrics successfully"); + } + }); + } + 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.latestVersion; + 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.FetchCallType.put); + let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, UtilModels_1.FetchCallType.put, 3, filepath, true); + if (uploadresult.message.statusCode != 201) { + let errorObj = yield Util.getResultObj(uploadresult); + console.log(errorObj ? errorObj : 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.patchAppComponents(); + 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.latestVersion + ("&fileType=" + UtilModels_1.FileType.ADDITIONAL_ARTIFACTS); + urlSuffix = this.baseURL + urlSuffix; + let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.put); + let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, UtilModels_1.FetchCallType.put, 3, filepath, true); + if (uploadresult.message.statusCode != 201) { + let errorObj = yield Util.getResultObj(uploadresult); + console.log(errorObj ? errorObj : 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.latestVersion + "&fileType=" + UtilModels_1.FileType.ZIPPED_ARTIFACTS; + urlSuffix = this.baseURL + urlSuffix; + let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.put); + let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, UtilModels_1.FetchCallType.put, 3, filepath, true); + if (uploadresult.message.statusCode != 201) { + let errorObj = yield Util.getResultObj(uploadresult); + console.log(errorObj ? errorObj : 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.latestVersion + "&fileType=" + UtilModels_1.FileType.USER_PROPERTIES; + urlSuffix = this.baseURL + urlSuffix; + let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.put); + let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, UtilModels_1.FetchCallType.put, 3, propertyFile, true); + if (uploadresult.message.statusCode != 201) { + let errorObj = yield Util.getResultObj(uploadresult); + console.log(errorObj ? errorObj : 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* () { + try { + var startData = this.yamlModel.getStartTestData(); + const testRunId = this.yamlModel.runTimeParams.testRunId; + let urlSuffix = "test-runs/" + testRunId + "?api-version=" + UtilModels_1.ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL + urlSuffix; + core.exportVariable(UtilModels_1.PostTaskParameters.runId, testRunId); + console.log("Creating and running a testRun for the test"); + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.patch); + let startTestresult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.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 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 '" + Util.getResourceNameFromResourceId(this.authContext.resourceId) + "' in subscription '" + this.authContext.subscriptionName + "'"); + console.log("2. On the Tests page, go to test '" + this.yamlModel.displayName + "'"); + console.log("3. Go to test run '" + testRunDao.displayName + "'\n"); + 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) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + let urlSuffix = "test-runs/" + testRunId + "?api-version=" + UtilModels_1.ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL + urlSuffix; + while (!Util.isTerminalTestStatus(testStatus)) { + let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.get); + let testRunResult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.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 = (_a = testRunObj.status) !== null && _a !== void 0 ? _a : testStatus; + 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.FetchCallType.get); + let testRunResult = yield FetchUtil.httpClientRetries(urlSuffix, header, UtilModels_1.FetchCallType.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); + core.exportVariable(UtilModels_1.PostTaskParameters.isRunCompleted, 'true'); + let testResultUrl = Util.getResultFolder(testRunObj.testArtifacts); + if (testResultUrl != null) { + const response = yield FetchUtil.httpClientRetries(testResultUrl, {}, UtilModels_1.FetchCallType.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, {}, UtilModels_1.FetchCallType.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.isNullOrUndefined)(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { + core.setFailed("TestResult: " + testRunObj.testResult); + return; + } + if (!(0, util_1.isNullOrUndefined)(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); + } + } + } + }); + } + // this api is special case and doesnot use the yamlModels, instead uses the task variables for the same, this doesnot have the initialisation too. + stopTestRunPostProcess(baseUri, runId) { + return __awaiter(this, void 0, void 0, function* () { + let urlSuffix = baseUri + "test-runs/" + runId + ":stop?api-version=" + UtilModels_1.ApiVersionConstants.latestVersion; + let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.post); + yield FetchUtil.httpClientRetries(urlSuffix, headers, UtilModels_1.FetchCallType.post, 3, ''); + }); + } +} +exports.APISupport = APISupport; diff --git a/lib/models/AuthenticationUtils.js b/lib/models/AuthenticationUtils.js new file mode 100644 index 00000000..df3be57b --- /dev/null +++ b/lib/models/AuthenticationUtils.js @@ -0,0 +1,183 @@ +"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.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"); +const InputConstants = __importStar(require("./InputConstants")); +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 = ''; + this.subscriptionName = ''; + } + authorize() { + return __awaiter(this, void 0, void 0, function* () { + // NOTE: This will set the subscription id + yield this.getTokenAPI(UtilModels_1.TokenScope.ControlPlane); + this.subscriptionName = yield this.getSubName(); + const rg = core.getInput(InputConstants.resourceGroup); + const ltres = core.getInput(InputConstants.loadTestResource); + if ((0, util_1.isNullOrUndefined)(rg) || rg == '') { + throw new Error(`The input field "${InputConstants.resourceGroupLabel}" is empty. Provide an existing resource group name.`); + } + if ((0, util_1.isNullOrUndefined)(ltres) || ltres == '') { + throw new Error(`The input field "${InputConstants.loadTestResourceLabel}" 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) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + 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; + }); + } + getSubName() { + return __awaiter(this, void 0, void 0, function* () { + try { + const cmdArguments = ["account", "show"]; + var result = yield this.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); + } + }); + } + 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..7c8c9f72 --- /dev/null +++ b/lib/models/FetchHelper.js @@ -0,0 +1,103 @@ +"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.httpClientRetries = void 0; +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")); +const methodEnumToString = { + [UtilModels_1.FetchCallType.get]: "get", + [UtilModels_1.FetchCallType.post]: "post", + [UtilModels_1.FetchCallType.put]: "put", + [UtilModels_1.FetchCallType.delete]: "del", + [UtilModels_1.FetchCallType.patch]: "patch" +}; +// (note mohit): shift to the enum later. +function httpClientRetries(urlSuffix, header, method, retries = 1, data, isUploadCall = true, log = true) { + return __awaiter(this, void 0, void 0, function* () { + 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 == UtilModels_1.FetchCallType.get) { + httpResponse = yield httpClient.get(urlSuffix, header); + } + else if (method == UtilModels_1.FetchCallType.delete) { + httpResponse = yield httpClient.del(urlSuffix, header); + } + else if (method == UtilModels_1.FetchCallType.post) { + httpResponse = yield httpClient.post(urlSuffix, data, header); + } + else if (method == UtilModels_1.FetchCallType.put && isUploadCall) { + let fileContent = (0, FileUtils_1.uploadFileData)(data); + httpResponse = yield httpClient.request(methodEnumToString[method], urlSuffix, fileContent, header); + } + else { + const githubBaseUrl = process.env.GITHUB_SERVER_URL; + const repository = process.env.GITHUB_REPOSITORY; + const runId = process.env.GITHUB_RUN_ID; + const pipelineName = process.env.GITHUB_WORKFLOW || "Unknown Pipeline"; + const pipelineUrl = `${githubBaseUrl}/${repository}/actions/runs/${runId}`; + header['x-ms-pipelineUrl'] = pipelineUrl; + header['x-ms-pipelineName'] = pipelineName; // setting these for patch calls. + httpResponse = yield httpClient.request(methodEnumToString[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}`); + } + } + }); +} +exports.httpClientRetries = httpClientRetries; diff --git a/lib/models/FileUtils.js b/lib/models/FileUtils.js new file mode 100644 index 00000000..5b7c65a2 --- /dev/null +++ b/lib/models/FileUtils.js @@ -0,0 +1,97 @@ +"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()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.uploadFileData = exports.deleteFile = exports.uploadFileToResultsFolder = void 0; +const path_1 = __importDefault(require("path")); +const UtilModels_1 = require("./UtilModels"); +const fs = __importStar(require("fs")); +const stream_1 = require("stream"); +function uploadFileToResultsFolder(response, fileName = UtilModels_1.resultZipFileName) { + return __awaiter(this, void 0, void 0, function* () { + 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); + } + }); +} +exports.uploadFileToResultsFolder = uploadFileToResultsFolder; +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); + } +} +exports.deleteFile = deleteFile; +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; diff --git a/lib/models/InputConstants.js b/lib/models/InputConstants.js new file mode 100644 index 00000000..80866b46 --- /dev/null +++ b/lib/models/InputConstants.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.loadTestConfigFileLabel = exports.loadTestResourceLabel = exports.resourceGroupLabel = exports.serviceConnectionNameLabel = exports.secretsLabel = exports.envVarsLabel = exports.outputVariableNameLabel = exports.overRideParametersLabel = exports.runDescriptionLabel = exports.testRunNameLabel = exports.loadTestConfigFile = exports.loadTestResource = exports.resourceGroup = exports.serviceConnectionName = exports.secrets = exports.envVars = exports.outputVariableName = exports.overRideParameters = exports.runDescription = exports.testRunName = void 0; +exports.testRunName = 'loadTestRunName'; +exports.runDescription = 'loadTestRunDescription'; +exports.overRideParameters = 'overrideParameters'; +exports.outputVariableName = 'outputVariableName'; +exports.envVars = 'env'; +exports.secrets = 'secrets'; +exports.serviceConnectionName = 'connectedServiceNameARM'; +exports.resourceGroup = 'resourceGroup'; +exports.loadTestResource = 'loadTestResource'; +exports.loadTestConfigFile = 'loadTestConfigFile'; +// labels user visible strings +exports.testRunNameLabel = 'Load Test Run Name'; +exports.runDescriptionLabel = 'Load Test Run Description'; +exports.overRideParametersLabel = 'Override Parameters'; +exports.outputVariableNameLabel = 'Output Variable Name'; +exports.envVarsLabel = 'env'; +exports.secretsLabel = 'Secrets'; +exports.serviceConnectionNameLabel = 'Azure subscription'; +exports.resourceGroupLabel = 'Resource Group'; +exports.loadTestResourceLabel = 'Load Test Resource'; +exports.loadTestConfigFileLabel = 'Load Test Config File'; diff --git a/lib/models/PayloadModels.js b/lib/models/PayloadModels.js new file mode 100644 index 00000000..905c2aea --- /dev/null +++ b/lib/models/PayloadModels.js @@ -0,0 +1,21 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ManagedIdentityTypeForAPI = exports.CertificateMetadata = void 0; +class CertificateMetadata { +} +exports.CertificateMetadata = CertificateMetadata; +; +; +; +; +; +; +; +; +; +var ManagedIdentityTypeForAPI; +(function (ManagedIdentityTypeForAPI) { + ManagedIdentityTypeForAPI["SystemAssigned"] = "SystemAssigned"; + ManagedIdentityTypeForAPI["UserAssigned"] = "UserAssigned"; + ManagedIdentityTypeForAPI["None"] = "None"; +})(ManagedIdentityTypeForAPI = exports.ManagedIdentityTypeForAPI || (exports.ManagedIdentityTypeForAPI = {})); diff --git a/lib/models/TaskModels.js b/lib/models/TaskModels.js new file mode 100644 index 00000000..0dce855c --- /dev/null +++ b/lib/models/TaskModels.js @@ -0,0 +1,514 @@ +"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; +}; +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 PayloadModels_1 = require("./PayloadModels"); +const UtilModels_1 = require("./UtilModels"); +const core = __importStar(require("@actions/core")); +const constants_1 = require("./constants"); +const InputConstants = __importStar(require("./InputConstants")); +class YamlConfig { + constructor(isPostProcess = false) { + var _a, _b, _c, _d, _e, _f; + 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.keyVaultReferenceIdentityType = PayloadModels_1.ManagedIdentityTypeForAPI.SystemAssigned; + this.metricsReferenceIdentityType = PayloadModels_1.ManagedIdentityTypeForAPI.SystemAssigned; + this.engineReferenceIdentityType = PayloadModels_1.ManagedIdentityTypeForAPI.None; + this.keyVaultReferenceIdentity = null; + this.metricsReferenceIdentity = null; + this.engineReferenceIdentities = null; + this.autoStop = null; + this.regionalLoadTestConfig = null; + this.runTimeParams = { env: {}, secrets: {}, runDisplayName: '', runDescription: '', testId: '', testRunId: '' }; + this.appComponents = {}; + this.serverMetricsConfig = {}; + this.addDefaultsForAppComponents = {}; // when server components are not given for few app components, we need to add the defaults for this. + this.outputVariableName = constants_1.OutputVariableName; + if (isPostProcess) { + return; + } + let yamlFile = (_a = core.getInput(InputConstants.loadTestConfigFile)) !== null && _a !== void 0 ? _a : ''; + if ((0, util_1.isNullOrUndefined)(yamlFile) || yamlFile == '') { + throw new Error(`The input field "${InputConstants.loadTestConfigFileLabel}" 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.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.appComponents != undefined) { + let appcomponents = config.appComponents; + this.getAppComponentsAndServerMetricsConfig(appcomponents); + } + if (config.keyVaultReferenceIdentity != undefined || config.keyVaultReferenceIdentityType != undefined) { + this.keyVaultReferenceIdentityType = config.keyVaultReferenceIdentity ? PayloadModels_1.ManagedIdentityTypeForAPI.UserAssigned : PayloadModels_1.ManagedIdentityTypeForAPI.SystemAssigned; + this.keyVaultReferenceIdentity = (_f = config.keyVaultReferenceIdentity) !== null && _f !== void 0 ? _f : null; + } + if (config.referenceIdentities != undefined) { + this.getReferenceIdentities(config.referenceIdentities); + } + if (config.regionalLoadTestConfig != undefined) { + this.regionalLoadTestConfig = this.getMultiRegionLoadTestConfig(config.regionalLoadTestConfig); + } + 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); + } + getAppComponentsAndServerMetricsConfig(appComponents) { + var _a, _b, _c, _d, _e, _f, _g, _h; + for (let value of appComponents) { + let resourceId = value.resourceId.toLowerCase(); + this.appComponents[resourceId] = { + resourceName: (value.resourceName || Util.getResourceNameFromResourceId(resourceId)), + kind: (_a = value.kind) !== null && _a !== void 0 ? _a : null, + resourceType: (_b = Util.getResourceTypeFromResourceId(resourceId)) !== null && _b !== void 0 ? _b : '', + resourceId: resourceId, + subscriptionId: (_c = Util.getSubscriptionIdFromResourceId(resourceId)) !== null && _c !== void 0 ? _c : '', + resourceGroup: (_d = Util.getResourceGroupFromResourceId(resourceId)) !== null && _d !== void 0 ? _d : '' + }; + let metrics = ((_e = value.metrics) !== null && _e !== void 0 ? _e : []); + if (this.addDefaultsForAppComponents[resourceId] == undefined) { + this.addDefaultsForAppComponents[resourceId] = metrics.length == 0; + } + else { + this.addDefaultsForAppComponents[resourceId] = this.addDefaultsForAppComponents[resourceId] && metrics.length == 0; + // when the same resource has metrics at one place, but not at other, we dont need defaults anymore. + } + for (let serverComponent of metrics) { + let key = resourceId.toLowerCase() + '/' + ((_f = serverComponent.namespace) !== null && _f !== void 0 ? _f : Util.getResourceTypeFromResourceId(resourceId)) + '/' + serverComponent.name; + if (!this.serverMetricsConfig.hasOwnProperty(key) || (0, util_1.isNullOrUndefined)(this.serverMetricsConfig[key])) { + this.serverMetricsConfig[key] = { + name: serverComponent.name, + aggregation: serverComponent.aggregation, + metricNamespace: (_g = serverComponent.namespace) !== null && _g !== void 0 ? _g : Util.getResourceTypeFromResourceId(resourceId), + resourceId: resourceId, + resourceType: (_h = Util.getResourceTypeFromResourceId(resourceId)) !== null && _h !== void 0 ? _h : '', + id: key + }; + } + else { + this.serverMetricsConfig[key].aggregation = this.serverMetricsConfig[key].aggregation + "," + serverComponent.aggregation; + } + } + } + } + getReferenceIdentities(referenceIdentities) { + let segregatedManagedIdentities = Util.validateAndGetSegregatedManagedIdentities(referenceIdentities); + this.keyVaultReferenceIdentity = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.KeyVault].length > 0 ? segregatedManagedIdentities.referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.KeyVault][0] : null; + this.keyVaultReferenceIdentityType = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.KeyVault].length > 0 ? PayloadModels_1.ManagedIdentityTypeForAPI.UserAssigned : PayloadModels_1.ManagedIdentityTypeForAPI.SystemAssigned; + this.metricsReferenceIdentity = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.Metrics].length > 0 ? segregatedManagedIdentities.referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.Metrics][0] : null; + this.metricsReferenceIdentityType = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.Metrics].length > 0 ? PayloadModels_1.ManagedIdentityTypeForAPI.UserAssigned : PayloadModels_1.ManagedIdentityTypeForAPI.SystemAssigned; + if (segregatedManagedIdentities.referenceIdentiesSystemAssignedCount[UtilModels_1.ReferenceIdentityKinds.Engine] > 0) { + this.engineReferenceIdentityType = PayloadModels_1.ManagedIdentityTypeForAPI.SystemAssigned; + } + else if (segregatedManagedIdentities.referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.Engine].length > 0) { + this.engineReferenceIdentityType = PayloadModels_1.ManagedIdentityTypeForAPI.UserAssigned; + this.engineReferenceIdentities = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.Engine]; + } + else { + this.engineReferenceIdentityType = PayloadModels_1.ManagedIdentityTypeForAPI.None; + } + } + getOverRideParams() { + let overRideParams = core.getInput(InputConstants.overRideParameters); + if (overRideParams) { + let overRideParamsObj = JSON.parse(overRideParams); + if (overRideParamsObj.testId != undefined) { + this.testId = overRideParamsObj.testId; + } + if (overRideParamsObj.displayName != undefined) { + this.displayName = overRideParamsObj.displayName; + } + if (overRideParamsObj.description != undefined) { + this.description = overRideParamsObj.description; + } + if (overRideParamsObj.engineInstances != undefined) { + this.engineInstances = overRideParamsObj.engineInstances; + } + if (overRideParamsObj.autoStop != undefined) { + this.autoStop = this.getAutoStopCriteria(overRideParamsObj.autoStop); + } + } + } + getOutPutVarName() { + var _a; + let outputVarName = (_a = core.getInput(InputConstants.outputVariableName)) !== null && _a !== void 0 ? _a : constants_1.OutputVariableName; + this.outputVariableName = outputVarName; + } + getRunTimeParams() { + var secretRun = core.getInput(InputConstants.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 ${InputConstants.secretsLabel} in the pipeline file. Refer to the pipeline syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-configure-load-test-cicd?tabs=pipelines#update-the-azure-pipelines-workflow`); + } + } + var eRun = core.getInput(InputConstants.envVars); + 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 ${InputConstants.envVarsLabel} in the pipeline file. Refer to the pipeline syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-configure-load-test-cicd?tabs=pipelines#update-the-azure-pipelines-workflow`); + } + } + let runDisplayNameInput = core.getInput(InputConstants.testRunName); + const runDisplayName = !(0, util_1.isNullOrUndefined)(runDisplayNameInput) && runDisplayNameInput != '' ? runDisplayNameInput : Util.getDefaultTestRunName(); + let runDescriptionInput = core.getInput(InputConstants.runDescription); + const runDescription = !(0, util_1.isNullOrUndefined)(runDescriptionInput) && runDescriptionInput != '' ? runDescriptionInput : Util.getDefaultRunDescription(); + let runTimeParams = { env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: '' }; + this.runTimeParams = runTimeParams; + let overRideParamsInput = core.getInput(InputConstants.overRideParameters); + let outputVariableNameInput = core.getInput(InputConstants.outputVariableName); + let overRideParams = !(0, util_1.isNullOrUndefined)(overRideParamsInput) && overRideParamsInput != '' ? overRideParamsInput : undefined; + let outputVarName = !(0, util_1.isNullOrUndefined)(outputVariableNameInput) && outputVariableNameInput != '' ? outputVariableNameInput : constants_1.OutputVariableName; + console.log(`overRideParams: ${overRideParams}`, `outputVarName: ${outputVarName}`); + let validation = Util.validateOverRideParameters(overRideParams); + if (validation.valid == false) { + console.log(validation.error); + throw new Error(`Invalid ${InputConstants.overRideParametersLabel}. Refer to the pipeline syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-configure-load-test-cicd?tabs=pipelines#update-the-azure-pipelines-workflow`); + } + validation = Util.validateOutputParametervariableName(outputVarName); + if (validation.valid == false) { + console.log(validation.error); + throw new Error(`Invalid ${InputConstants.outputVariableNameLabel}. Refer to the pipeline syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-configure-load-test-cicd?tabs=pipelines#update-the-azure-pipelines-workflow`); + } + this.getOverRideParams(); + this.getOutPutVarName(); + 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; + } + for (let [resourceId, keys] of existingData.appComponents) { + if (!this.appComponents.hasOwnProperty(resourceId.toLowerCase())) { + for (let key of keys) { + this.appComponents[key] = null; + } + } + else { + for (let key of keys) { + if (key != null && key != resourceId.toLowerCase()) { + this.appComponents[key] = null; + } + } + } + } + } + mergeExistingServerCriteria(existingServerCriteria) { + var _a, _b, _c; + for (let key in existingServerCriteria.metrics) { + let resourceId = (_c = (_b = (_a = existingServerCriteria.metrics[key]) === null || _a === void 0 ? void 0 : _a.resourceId) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : ""; + if (this.addDefaultsForAppComponents.hasOwnProperty(resourceId) && !this.addDefaultsForAppComponents[resourceId] && !this.serverMetricsConfig.hasOwnProperty(key)) { + this.serverMetricsConfig[key] = null; + } + } + } + getAppComponentsData() { + let appComponentsApiModel = { + components: this.appComponents + }; + return appComponentsApiModel; + } + 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, + engineBuiltinIdentityIds: this.engineReferenceIdentities, + engineBuiltinIdentityType: this.engineReferenceIdentityType, + metricsReferenceIdentityType: this.metricsReferenceIdentityType, + metricsReferenceIdentityId: this.metricsReferenceIdentity + }; + return data; + } + getStartTestData() { + this.runTimeParams.testId = this.testId; + this.runTimeParams.testRunId = Util.getUniqueId(); + let startData = { + testId: this.testId, + testRunId: this.runTimeParams.testRunId, + environmentVariables: this.runTimeParams.env, + secrets: this.runTimeParams.secrets, + displayName: this.runTimeParams.runDisplayName, + description: this.runTimeParams.runDescription + }; + return startData; + } + getAutoStopCriteria(autoStopInput) { + let autoStop; + if (autoStopInput == null) { + autoStop = null; + return autoStop; + } + if (typeof autoStopInput == "string") { + if (autoStopInput == constants_1.autoStopDisable) { + 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..14158d48 --- /dev/null +++ b/lib/models/UtilModels.js @@ -0,0 +1,83 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OutPutVariablesConstants = exports.PostTaskParameters = exports.ManagedIdentityType = exports.ValidConditionList = exports.ValidAggregateList = exports.ApiVersionConstants = exports.correlationHeader = exports.resultZipFileName = exports.reportZipFileName = exports.resultFolder = exports.FileType = exports.ContentTypeMap = exports.FetchCallType = exports.TokenScope = exports.ReferenceIdentityKinds = exports.ParamType = void 0; +var ParamType; +(function (ParamType) { + ParamType["env"] = "env"; + ParamType["secrets"] = "secrets"; + ParamType["cert"] = "cert"; +})(ParamType = exports.ParamType || (exports.ParamType = {})); +var ReferenceIdentityKinds; +(function (ReferenceIdentityKinds) { + ReferenceIdentityKinds["KeyVault"] = "KeyVault"; + ReferenceIdentityKinds["Metrics"] = "Metrics"; + ReferenceIdentityKinds["Engine"] = "Engine"; +})(ReferenceIdentityKinds = exports.ReferenceIdentityKinds || (exports.ReferenceIdentityKinds = {})); +var TokenScope; +(function (TokenScope) { + TokenScope[TokenScope["Dataplane"] = 0] = "Dataplane"; + TokenScope[TokenScope["ControlPlane"] = 1] = "ControlPlane"; +})(TokenScope = exports.TokenScope || (exports.TokenScope = {})); +var FetchCallType; +(function (FetchCallType) { + FetchCallType[FetchCallType["get"] = 0] = "get"; + FetchCallType[FetchCallType["patch"] = 1] = "patch"; + FetchCallType[FetchCallType["put"] = 2] = "put"; + FetchCallType[FetchCallType["delete"] = 3] = "delete"; + FetchCallType[FetchCallType["post"] = 4] = "post"; +})(FetchCallType = exports.FetchCallType || (exports.FetchCallType = {})); +exports.ContentTypeMap = { + [FetchCallType.get]: null, + [FetchCallType.patch]: 'application/merge-patch+json', + [FetchCallType.put]: 'application/octet-stream', + [FetchCallType.delete]: 'application/json', + [FetchCallType.post]: '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 || (exports.FileType = {})); +exports.resultFolder = 'loadTest'; +exports.reportZipFileName = 'report.zip'; +exports.resultZipFileName = 'results.zip'; +exports.correlationHeader = 'x-ms-correlation-request-id'; +var ApiVersionConstants; +(function (ApiVersionConstants) { + ApiVersionConstants.latestVersion = '2024-12-01-preview'; + ApiVersionConstants.tm2022Version = '2022-11-01'; + ApiVersionConstants.cp2022Version = '2022-12-01'; +})(ApiVersionConstants = exports.ApiVersionConstants || (exports.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 || (exports.ManagedIdentityType = {})); +var PostTaskParameters; +(function (PostTaskParameters) { + PostTaskParameters.runId = 'LOADTEST_RUNID'; + PostTaskParameters.baseUri = 'LOADTEST_RESOURCE_URI'; + PostTaskParameters.isRunCompleted = 'LOADTEST_RUN_COMPLETED'; // this is set when the task is completed, to avoid get calls for the test again. +})(PostTaskParameters = exports.PostTaskParameters || (exports.PostTaskParameters = {})); +var OutPutVariablesConstants; +(function (OutPutVariablesConstants) { + OutPutVariablesConstants.testRunId = 'testRunId'; +})(OutPutVariablesConstants = exports.OutPutVariablesConstants || (exports.OutPutVariablesConstants = {})); diff --git a/lib/models/constants.js b/lib/models/constants.js new file mode 100644 index 00000000..daad98fd --- /dev/null +++ b/lib/models/constants.js @@ -0,0 +1,61 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OutputVariableName = exports.APIRoute = exports.autoStopDisable = exports.testmanagerApiVersion = exports.OverRideParametersModel = exports.overRideParamsJSON = exports.DefaultYamlModel = void 0; +class DefaultYamlModel { + constructor() { + this.version = ''; + this.testId = ''; + this.testName = ''; + this.displayName = ''; + this.description = ''; + this.testPlan = ''; + this.testType = ''; + this.engineInstances = 0; + this.subnetId = ''; + this.publicIPDisabled = false; + this.configurationFiles = []; + this.zipArtifacts = []; + this.splitAllCSVs = false; + this.properties = { userPropertyFile: '' }; + this.env = []; + this.certificates = []; + this.secrets = []; + this.failureCriteria = []; + this.appComponents = []; + this.autoStop = { errorPercentage: 0, timeWindow: 0 }; + this.keyVaultReferenceIdentity = ''; + this.keyVaultReferenceIdentityType = ''; + this.regionalLoadTestConfig = []; + this.referenceIdentities = []; + } +} +exports.DefaultYamlModel = DefaultYamlModel; +exports.overRideParamsJSON = { + testId: 'SampleTest', + displayName: 'SampleTest', + description: 'Load test website home page', + engineInstances: 1, + autoStop: { errorPercentage: 80, timeWindow: 60 }, +}; +class OverRideParametersModel { + constructor() { + this.testId = ''; + this.displayName = ''; + this.description = ''; + this.engineInstances = 0; + this.autoStop = { errorPercentage: 0, timeWindow: 0 }; + } +} +exports.OverRideParametersModel = OverRideParametersModel; +exports.testmanagerApiVersion = "2024-07-01-preview"; +exports.autoStopDisable = "disable"; +var BaseAPIRoute; +(function (BaseAPIRoute) { + BaseAPIRoute.featureFlag = "featureFlags"; +})(BaseAPIRoute || (BaseAPIRoute = {})); +var APIRoute; +(function (APIRoute) { + const latestVersion = "api-version=" + exports.testmanagerApiVersion; + APIRoute.FeatureFlags = (flag) => `${BaseAPIRoute.featureFlag}/${flag}?${latestVersion}`; +})(APIRoute = exports.APIRoute || (exports.APIRoute = {})); +exports.OutputVariableName = 'ALTOutputVar'; diff --git a/lib/models/engine/BaseLoadTestFrameworkModel.js b/lib/models/engine/BaseLoadTestFrameworkModel.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/lib/models/engine/BaseLoadTestFrameworkModel.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/models/engine/JMeterFrameworkModel.js b/lib/models/engine/JMeterFrameworkModel.js new file mode 100644 index 00000000..26b71211 --- /dev/null +++ b/lib/models/engine/JMeterFrameworkModel.js @@ -0,0 +1,40 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.JMeterFrameworkModel = void 0; +const TestKind_1 = require("./TestKind"); +/** + * JMeter load test framework. + */ +class JMeterFrameworkModel { + constructor() { + // Constants + /** + * The kind of the load test framework. + */ + this.kind = TestKind_1.TestKind.JMX; + /** + * The display name of the load test framework. + */ + this.frameworkDisplayName = "JMeter"; + /** + * The file extension for the test script file. + */ + this.testScriptFileExtension = "jmx"; + /** + * The file extensions for the configuration files. + */ + this.userPropertyFileExtensions = ["properties"]; + /** + * Strings for the client resources. + */ + this.ClientResources = { + /** + * Friendly string of the user property extensions. + */ + userPropertyFileExtensionsFriendly: "\".properties\"", + }; + // Data related to the framework + // Methods + } +} +exports.JMeterFrameworkModel = JMeterFrameworkModel; diff --git a/lib/models/engine/LocustFrameworkModel.js b/lib/models/engine/LocustFrameworkModel.js new file mode 100644 index 00000000..6f4cd4df --- /dev/null +++ b/lib/models/engine/LocustFrameworkModel.js @@ -0,0 +1,40 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LocustFrameworkModel = void 0; +const TestKind_1 = require("./TestKind"); +/** + * Locust load test framework. + */ +class LocustFrameworkModel { + constructor() { + // Constants + /** + * The kind of the load test framework. + */ + this.kind = TestKind_1.TestKind.Locust; + /** + * The display name of the load test framework. + */ + this.frameworkDisplayName = "Locust (preview)"; + /** + * The file extension for the test script file. + */ + this.testScriptFileExtension = "py"; + /** + * The file extensions for the configuration files. + */ + this.userPropertyFileExtensions = ["conf", "ini", "toml"]; + /** + * Strings for the client resources. + */ + this.ClientResources = { + /** + * Friendly string of the user property extensions. + */ + userPropertyFileExtensionsFriendly: "\".conf\", \".toml\" or \".ini\"", + }; + // Data related to the framework + // Methods + } +} +exports.LocustFrameworkModel = LocustFrameworkModel; diff --git a/lib/models/engine/TestKind.js b/lib/models/engine/TestKind.js new file mode 100644 index 00000000..0b773c6b --- /dev/null +++ b/lib/models/engine/TestKind.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TestKind = void 0; +/** + * Enumeration representing the available test kinds. + */ +var TestKind; +(function (TestKind) { + TestKind["URL"] = "URL"; + TestKind["JMX"] = "JMX"; + TestKind["Locust"] = "Locust"; +})(TestKind = exports.TestKind || (exports.TestKind = {})); diff --git a/lib/models/engine/Util.js b/lib/models/engine/Util.js new file mode 100644 index 00000000..4a9cd9bb --- /dev/null +++ b/lib/models/engine/Util.js @@ -0,0 +1,103 @@ +"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; +const JMeterFrameworkModel_1 = require("./JMeterFrameworkModel"); +const LocustFrameworkModel_1 = require("./LocustFrameworkModel"); +const TestKind_1 = require("./TestKind"); +var _jmeterFramework = new JMeterFrameworkModel_1.JMeterFrameworkModel(); +var _locustFramework = new LocustFrameworkModel_1.LocustFrameworkModel(); +/** + * Enumeration representing the available load test frameworks. + */ +var LoadTestFramework; +(function (LoadTestFramework) { + LoadTestFramework["JMeter"] = "JMeter"; + LoadTestFramework["Locust"] = "Locust"; +})(LoadTestFramework = exports.LoadTestFramework || (exports.LoadTestFramework = {})); +/** + * Retrieves an array of load test frameworks in a specific order. + * @returns An array of load test frameworks. + */ +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. + * @param framework The LoadTestFramework to get the corresponding LoadTestFrameworkModel for. + * @returns The corresponding LoadTestFrameworkModel. + */ +function getLoadTestFrameworkModel(framework) { + switch (framework) { + case LoadTestFramework.JMeter: + return _jmeterFramework; + case LoadTestFramework.Locust: + return _locustFramework; + default: + // Assume JMeter by default + return _jmeterFramework; + } +} +exports.getLoadTestFrameworkModel = getLoadTestFrameworkModel; +/** + * Retrieves the display name of a load test framework. + * @param framework The load test framework. + * @returns The display name of the load test framework. + */ +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. + * @param kind The test kind to check. + * @returns True if the test kind is convertible to JMX, false otherwise. + */ +function isTestKindConvertibleToJMX(kind) { + if (!kind) { + // Assume JMX by default + return false; + } + 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. + * @param kind The test kind. + * @returns The load test framework for the test kind. + */ +function getLoadTestFrameworkFromKind(kind) { + if (!kind) { + // Assume JMX by default + return LoadTestFramework.JMeter; + } + switch (kind) { + case TestKind_1.TestKind.JMX: + return LoadTestFramework.JMeter; + case TestKind_1.TestKind.Locust: + return LoadTestFramework.Locust; + default: + 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. + * @param kind The test kind. + * @returns The load test framework model for the test kind. + */ +function getLoadTestFrameworkModelFromKind(kind) { + return getLoadTestFrameworkModel(getLoadTestFrameworkFromKind(kind)); +} +exports.getLoadTestFrameworkModelFromKind = getLoadTestFrameworkModelFromKind; +var Resources; +(function (Resources) { + let Strings; + (function (Strings) { + Strings.allFrameworksFriendly = "URL, JMX and Locust"; + })(Strings = Resources.Strings || (Resources.Strings = {})); +})(Resources = exports.Resources || (exports.Resources = {})); diff --git a/lib/models/util.js b/lib/models/util.js new file mode 100644 index 00000000..3185a82b --- /dev/null +++ b/lib/models/util.js @@ -0,0 +1,835 @@ +"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.getAllFileErrors = exports.validateTestRunParamsFromPipeline = exports.getDefaultRunDescription = exports.getDefaultTestRunName = exports.getDefaultTestName = exports.validateUrlcert = exports.validateUrl = exports.ValidateCriteriaAndConvertToWorkingStringModel = exports.getPassFailCriteriaFromString = exports.validateOutputParametervariableName = exports.validateOverRideParameters = exports.validateAndGetSegregatedManagedIdentities = exports.validateAutoStop = exports.checkValidityYaml = exports.getSubscriptionIdFromResourceId = exports.getResourceGroupFromResourceId = exports.getResourceNameFromResourceId = exports.getResourceTypeFromResourceId = exports.invalidDescription = exports.invalidDisplayName = exports.getResultObj = exports.validCriteria = exports.isStatusFailed = exports.isTerminalTestStatus = exports.removeUnits = exports.indexOfFirstDigit = exports.getReportFolder = exports.getResultFolder = exports.getUniqueId = exports.sleep = exports.printClientMetrics = exports.ErrorCorrection = exports.printCriteria = exports.printTestDuration = exports.checkFileTypes = exports.checkFileType = void 0; +const { v4: uuidv4 } = require('uuid'); +const util_1 = require("util"); +const constants_1 = require("./constants"); +const EngineUtil = __importStar(require("./engine/Util")); +const TestKind_1 = require("./engine/TestKind"); +const UtilModels_1 = require("./UtilModels"); +const InputConstants = __importStar(require("./InputConstants")); +function checkFileType(filePath, fileExtToValidate) { + if ((0, util_1.isNullOrUndefined)(filePath)) { + return false; + } + 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)) { + return false; + } + let split = filePath.split('.'); + 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(testRunObj) { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + console.log("Summary generation completed\n"); + console.log("-------------------Summary ---------------"); + 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"); + for (var key in criteria) { + 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 += ' '; + spaceCount--; + } + var actualValue = metric.actualValue ? metric.actualValue.toString() : ''; + spaceCount = 10 - (actualValue).length; + while (spaceCount--) + actualValue = actualValue + ' '; + metric.result = metric.result ? metric.result.toUpperCase() : ''; + console.log(str + actualValue + " " + metric.result); + } + 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; +} +exports.ErrorCorrection = ErrorCorrection; +function printTestResult(criteria) { + var _a, _b; + let pass = 0; + let fail = 0; + for (var key in criteria) { + if (((_a = criteria[key]) === null || _a === void 0 ? void 0 : _a.result) == "passed") + pass++; + 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"); + return { pass, fail }; // returning so that we can use this in the UTs later. +} +function printMetrics(data, key = null) { + var _a; + let samplerName = (_a = data.transaction) !== null && _a !== void 0 ? _a : key; + if (samplerName == 'Total') { + samplerName = "Aggregate"; + } + console.log("Sampler name \t\t : ", samplerName, "\n"); + console.log("response time \t\t : avg=" + getAbsVal(data.meanResTime) + " ms, min=" + getAbsVal(data.minResTime) + " ms, med=" + getAbsVal(data.medianResTime) + " ms, max=" + getAbsVal(data.maxResTime) + " ms, p(75)=" + getAbsVal(data.pct75ResTime) + " ms, p(90)=" + getAbsVal(data.pct1ResTime) + " ms, p(95)=" + getAbsVal(data.pct2ResTime) + " ms, p(96)=" + getAbsVal(data.pct96ResTime) + " ms, p(98)=" + getAbsVal(data.pct98ResTime) + " ms, p(99)=" + getAbsVal(data.pct3ResTime) + " ms, p(99.9)=" + getAbsVal(data.pct999ResTime) + " ms, p(99.99)=" + getAbsVal(data.pct9999ResTime)); + console.log("requests per sec \t : avg=" + getAbsVal(data.throughput)); + console.log("total requests \t\t : " + data.sampleCount); + console.log("total errors \t\t : " + data.errorCount); + 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); + } + }); +} +exports.printClientMetrics = printClientMetrics; +function getAbsVal(data) { + if ((0, util_1.isNullOrUndefined)(data)) { + return "undefined"; + } + let dataString = data.toString(); + let dataArray = dataString.split('.'); + return dataArray[0]; +} +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} +exports.sleep = sleep; +function getUniqueId() { + return uuidv4(); +} +exports.getUniqueId = getUniqueId; +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; +} +exports.getResultFolder = getResultFolder; +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; +} +exports.getReportFolder = getReportFolder; +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 isStatusFailed(testStatus) { + if (testStatus === "FAILED" || testStatus === "CANCELLED") { + return true; + } + return false; +} +exports.isStatusFailed = isStatusFailed; +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 !(!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.getResultObj = getResultObj; +function isDictionary(variable) { + return typeof variable === 'object' && variable !== null && !Array.isArray(variable); +} +function invalidName(value) { + if (value.length < 2 || value.length > 50) + return true; + var r = new RegExp(/[^a-z0-9_-]+/); + return r.test(value); +} +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)); +} +function isInvalidManagedIdentityId(uri) { + 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 isValidReferenceIdentityKind(value) { + return Object.values(UtilModels_1.ReferenceIdentityKinds).includes(value); +} +function isValidTestKind(value) { + return Object.values(TestKind_1.TestKind).includes(value); +} +function isValidManagedIdentityType(value) { + return Object.values(UtilModels_1.ManagedIdentityType).includes(value); +} +function isArrayOfStrings(variable) { + return Array.isArray(variable) && variable.every((item) => typeof item === 'string'); +} +function isInvalidString(variable, allowNull = false) { + if (allowNull) { + return !(0, util_1.isNullOrUndefined)(variable) && (typeof variable != 'string' || variable == ""); + } + return (0, util_1.isNullOrUndefined)(variable) || typeof variable != 'string' || variable == ""; +} +function inValidEngineInstances(engines) { + if (engines > 400 || engines < 1) { + return true; + } + return false; +} +function getResourceTypeFromResourceId(resourceId) { + return resourceId && resourceId.split("/").length > 7 ? resourceId.split("/")[6] + "/" + resourceId.split("/")[7] : null; +} +exports.getResourceTypeFromResourceId = getResourceTypeFromResourceId; +function getResourceNameFromResourceId(resourceId) { + return resourceId && resourceId.split("/").length > 8 ? resourceId.split("/")[8] : null; +} +exports.getResourceNameFromResourceId = getResourceNameFromResourceId; +function getResourceGroupFromResourceId(resourceId) { + return resourceId && resourceId.split("/").length > 4 ? resourceId.split("/")[4] : null; +} +exports.getResourceGroupFromResourceId = getResourceGroupFromResourceId; +function getSubscriptionIdFromResourceId(resourceId) { + return resourceId && resourceId.split("/").length > 2 ? resourceId.split("/")[2] : null; +} +exports.getSubscriptionIdFromResourceId = getSubscriptionIdFromResourceId; +function isValidGUID(guid) { + const guidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + return guidRegex.test(guid); +} +function checkValidityYaml(givenYaml) { + var _a, _b; + if (!isDictionary(givenYaml)) { + return { valid: false, error: `Invalid YAML syntax.` }; + } + let unSupportedKeys = []; + let supportedKeys = Object.keys(new constants_1.DefaultYamlModel()); + Object.keys(givenYaml).forEach(element => { + if (supportedKeys.indexOf(element) == -1) { + unSupportedKeys.push(element); + } + }); + if (unSupportedKeys.length) { + const result = unSupportedKeys.map(element => `${element}`).join(", "); + return { valid: false, error: `The YAML file provided has unsupported field(s) "${result}".` }; + } + if ((0, util_1.isNullOrUndefined)(givenYaml.testName) && (0, util_1.isNullOrUndefined)(givenYaml.testId)) { + return { valid: false, error: "The required field testId is missing in the load test YAML file." }; + } + let testId = ''; + if (!(0, util_1.isNullOrUndefined)(givenYaml.testName)) { + testId = givenYaml.testName; + } + if (!(0, util_1.isNullOrUndefined)(givenYaml.testId)) { + testId = givenYaml.testId; + } + testId = testId.toLowerCase(); + if (typeof (testId) != "string" || invalidName(testId)) { + return { valid: false, error: `The value "${testId}" for testId is not a valid string. Allowed characters are [a-zA-Z0-9-_] and the length must be between 2 to 50 characters.` }; + } + if (givenYaml.displayName && (typeof givenYaml.displayName != 'string' || invalidDisplayName(givenYaml.displayName))) { + return { valid: false, error: `The value "${givenYaml.displayName}" for displayName is invalid. Display name must be a string of length between 2 to 50.` }; + } + if (givenYaml.description && (typeof givenYaml.description != 'string' || invalidDescription(givenYaml.description))) { + return { valid: false, error: `The value "${givenYaml.description}" for description is invalid. Description must be a string of length less than 100.` }; + } + if ((0, util_1.isNullOrUndefined)(givenYaml.testPlan)) { + return { valid: false, error: "The required field testPlan is missing in the load test YAML file." }; + } + 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 = (_a = givenYaml.testType) !== null && _a !== void 0 ? _a : TestKind_1.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 = EngineUtil.getLoadTestFrameworkModelFromKind(kind); + if (givenYaml.testType == TestKind_1.TestKind.URL) { + if (!checkFileType(givenYaml.testPlan, 'json')) { + return { valid: false, error: "The testPlan for a URL test should of type \"json\"." }; + } + } + else if (!checkFileType(givenYaml.testPlan, framework.testScriptFileExtension)) { + return { valid: false, error: `The testPlan for a ${kind} test should of type "${framework.testScriptFileExtension}".` }; + } + if (givenYaml.subnetId && (typeof givenYaml.subnetId != 'string' || isInValidSubnet(givenYaml.subnetId))) { + return { valid: false, error: `The value "${givenYaml.subnetId}" for subnetId is invalid. The value should be a string of the format: "/subscriptions/{subscriptionId}/resourceGroups/{rgName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}".` }; + } + if (givenYaml.keyVaultReferenceIdentity && (typeof givenYaml.keyVaultReferenceIdentity != 'string' || isInvalidManagedIdentityId(givenYaml.keyVaultReferenceIdentity))) { + return { valid: false, error: `The value "${givenYaml.keyVaultReferenceIdentity}" for keyVaultReferenceIdentity is invalid. The value should be a string of the format: "/subscriptions/{subsId}/resourceGroups/{rgName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}".` }; + } + 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.referenceIdentities)) { + if (!Array.isArray(givenYaml.referenceIdentities)) { + return { valid: false, error: `The value "${givenYaml.referenceIdentities.toString()}" for referenceIdentities is invalid. Provide a valid list of reference identities.` }; + } + let result = validateReferenceIdentities(givenYaml.referenceIdentities); + if ((result === null || result === void 0 ? void 0 : result.valid) == false) { + return result; + } + try { + if (givenYaml.keyVaultReferenceIdentityType || givenYaml.keyVaultReferenceIdentity) { + validateAndGetSegregatedManagedIdentities(givenYaml.referenceIdentities, true); + } + else { + validateAndGetSegregatedManagedIdentities(givenYaml.referenceIdentities); + } + } + catch (error) { + return { valid: false, error: error.message }; + } + } + 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 == 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') { + return { valid: false, error: `The value "${givenYaml.publicIPDisabled}" for publicIPDisabled is invalid. The value should be either true or false.` }; + } + if (givenYaml.publicIPDisabled && (0, util_1.isNullOrUndefined)(givenYaml.subnetId)) { + return { valid: false, error: `Public IP deployment can only be disabled for tests against private endpoints. For public endpoints, set publicIPDisabled to False.` }; + } + if (givenYaml.configurationFiles && !isArrayOfStrings(givenYaml.configurationFiles)) { + return { valid: false, error: `The value "${givenYaml.configurationFiles}" for configurationFiles is invalid. Provide a valid list of strings.` }; + } + if (givenYaml.zipArtifacts && !isArrayOfStrings(givenYaml.zipArtifacts)) { + return { valid: false, error: `The value "${givenYaml.zipArtifacts}" for zipArtifacts is invalid. Provide a valid list of strings.` }; + } + if (givenYaml.splitAllCSVs && typeof givenYaml.splitAllCSVs != 'boolean') { + return { valid: false, error: `The value "${givenYaml.splitAllCSVs}" for splitAllCSVs is invalid. The value should be either true or false` }; + } + if (givenYaml.properties != undefined && givenYaml.properties.userPropertyFile != undefined) { + if ((0, util_1.isNull)(givenYaml.properties.userPropertyFile) || typeof givenYaml.properties.userPropertyFile != 'string' || !checkFileTypes(givenYaml.properties.userPropertyFile, framework.userPropertyFileExtensions)) { + return { valid: false, error: `The value "${givenYaml.properties.userPropertyFile}" for userPropertyFile is invalid. Provide a valid file path of type ${framework.ClientResources.userPropertyFileExtensionsFriendly}. Refer to the YAML syntax at https://learn.microsoft.com/azure/load-testing/reference-test-config-yaml#properties-configuration.` }; + } + } + if (givenYaml.appComponents) { + if (!Array.isArray(givenYaml.appComponents)) { + return { valid: false, error: `The value "${givenYaml.appComponents}" for appComponents is invalid. Provide a valid list of application components.` }; + } + let validationAppComponents = validateAppComponentAndServerMetricsConfig(givenYaml.appComponents); + if (validationAppComponents.valid == false) { + return validationAppComponents; + } + } + if (givenYaml.autoStop) { + let validation = validateAutoStop(givenYaml.autoStop); + if (validation.valid == false) { + return validation; + } + } + if (givenYaml.regionalLoadTestConfig) { + if (!Array.isArray(givenYaml.regionalLoadTestConfig)) { + return { valid: false, error: `The value "${givenYaml.regionalLoadTestConfig}" for regionalLoadTestConfig is invalid. Provide a valid list of region configuration for Multi-region load test.` }; + } + 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 ((0, util_1.isNullOrUndefined)(givenYaml.regionalLoadTestConfig[i].region) || typeof givenYaml.regionalLoadTestConfig[i].region != 'string' || givenYaml.regionalLoadTestConfig[i].region == "") { + return { valid: false, error: `The value "${givenYaml.regionalLoadTestConfig[i].region}" for region in regionalLoadTestConfig is invalid. Provide a valid string.` }; + } + if ((0, util_1.isNullOrUndefined)(givenYaml.regionalLoadTestConfig[i].engineInstances) || isNaN(givenYaml.regionalLoadTestConfig[i].engineInstances) || inValidEngineInstances(givenYaml.regionalLoadTestConfig[i].engineInstances)) { + return { valid: false, error: `The value "${givenYaml.regionalLoadTestConfig[i].engineInstances}" for engineInstances in regionalLoadTestConfig is invalid. The value should be an integer between 1 and 400.` }; + } + totalEngineCount += givenYaml.regionalLoadTestConfig[i].engineInstances; + } + let engineInstances = (_b = givenYaml.engineInstances) !== null && _b !== void 0 ? _b : 1; + if (totalEngineCount != givenYaml.engineInstances) { + return { valid: false, error: `The sum of engineInstances in regionalLoadTestConfig should be equal to the value of totalEngineInstances "${engineInstances}" in the test configuration.` }; + } + } + return { valid: true, error: "" }; +} +exports.checkValidityYaml = checkValidityYaml; +function validateAutoStop(autoStop, isPipelineParam = false) { + if (typeof autoStop != 'string') { + if ((0, util_1.isNullOrUndefined)(autoStop.errorPercentage) || isNaN(autoStop.errorPercentage) || autoStop.errorPercentage > 100 || autoStop.errorPercentage < 0) { + let errorMessage = isPipelineParam + ? `The value "${autoStop.errorPercentage}" for errorPercentage of auto-stop criteria is invalid in the overrideParameters provided. The value should be valid decimal number from 0 to 100.` + : `The value "${autoStop.errorPercentage}" for errorPercentage of auto-stop criteria is invalid. The value should be valid decimal number from 0 to 100.`; + return { valid: false, error: errorMessage }; + } + if ((0, util_1.isNullOrUndefined)(autoStop.timeWindow) || isNaN(autoStop.timeWindow) || autoStop.timeWindow <= 0 || !Number.isInteger(autoStop.timeWindow)) { + let errorMessage = isPipelineParam + ? `The value "${autoStop.timeWindow}" for timeWindow of auto-stop criteria is invalid in the overrideParameters provided. The value should be valid integer greater than 0.` + : `The value "${autoStop.timeWindow}" for timeWindow of auto-stop criteria is invalid. The value should be valid integer greater than 0.`; + return { valid: false, error: errorMessage }; + } + } + else if (autoStop != constants_1.autoStopDisable) { + let errorMessage = isPipelineParam + ? 'Invalid value for "autoStop" in the overrideParameters provided, for disabling auto stop use "autoStop: disable"' + : 'Invalid value for "autoStop", for disabling auto stop use "autoStop: disable"'; + return { valid: false, error: errorMessage }; + } + return { valid: true, error: "" }; +} +exports.validateAutoStop = validateAutoStop; +function validateAndGetSegregatedManagedIdentities(referenceIdentities, keyVaultGivenOutOfReferenceIdentities = false) { + let referenceIdentityValuesUAMIMap = { + [UtilModels_1.ReferenceIdentityKinds.KeyVault]: [], + [UtilModels_1.ReferenceIdentityKinds.Metrics]: [], + [UtilModels_1.ReferenceIdentityKinds.Engine]: [] + }; + let referenceIdentiesSystemAssignedCount = { + [UtilModels_1.ReferenceIdentityKinds.KeyVault]: 0, + [UtilModels_1.ReferenceIdentityKinds.Metrics]: 0, + [UtilModels_1.ReferenceIdentityKinds.Engine]: 0 + }; + for (let referenceIdentity of referenceIdentities) { + // the value has check proper check in the utils, so we can decide the Type based on the value. + if (referenceIdentity.value) { + referenceIdentityValuesUAMIMap[referenceIdentity.kind].push(referenceIdentity.value); + } + else { + referenceIdentiesSystemAssignedCount[referenceIdentity.kind]++; + } + } + // key-vault which needs back-compat. + if (keyVaultGivenOutOfReferenceIdentities) { + if (referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.KeyVault].length > 0 || referenceIdentiesSystemAssignedCount[UtilModels_1.ReferenceIdentityKinds.KeyVault] > 0) { + throw new Error("Two KeyVault references are defined in the YAML config file. Use either the keyVaultReferenceIdentity field or the referenceIdentities section to specify the KeyVault reference identity."); + } + // this will be assigned above if the given is outside the refIds so no need to assign again. + } + for (let key in UtilModels_1.ReferenceIdentityKinds) { + if (key != UtilModels_1.ReferenceIdentityKinds.Engine) { + if (referenceIdentityValuesUAMIMap[key].length > 1 || referenceIdentiesSystemAssignedCount[key] > 1) { + throw new Error(`Only one ${key} reference identity should be provided in the referenceIdentities array.`); + } + else if (referenceIdentityValuesUAMIMap[key].length == 1 && referenceIdentiesSystemAssignedCount[key] > 0) { + throw new Error(`${key} reference identity should be either SystemAssigned or UserAssigned but not both.`); + } + } + } + // engines check, this can have multiple values too check is completely different. + if (referenceIdentityValuesUAMIMap[UtilModels_1.ReferenceIdentityKinds.Engine].length > 0 && referenceIdentiesSystemAssignedCount[UtilModels_1.ReferenceIdentityKinds.Engine] > 0) { + throw new Error("Engine reference identity should be either SystemAssigned or UserAssigned but not both."); + } + else if (referenceIdentiesSystemAssignedCount[UtilModels_1.ReferenceIdentityKinds.Engine] > 1) { + throw new Error("Only one Engine reference identity with SystemAssigned should be provided in the referenceIdentities array."); + } + return { referenceIdentityValuesUAMIMap, referenceIdentiesSystemAssignedCount }; +} +exports.validateAndGetSegregatedManagedIdentities = validateAndGetSegregatedManagedIdentities; +function validateAppComponentAndServerMetricsConfig(appComponents) { + var _a, _b, _c, _d, _e; + let appComponentsParsed = appComponents; + for (let i = 0; i < appComponentsParsed.length; i++) { + if (!isDictionary(appComponentsParsed[i])) { + return { valid: false, error: `The value "${appComponentsParsed[i].toString()}" for AppComponents in the index "${i}" is invalid. Provide a valid dictionary.` }; + } + let resourceId = appComponentsParsed[i].resourceId; + if (isInvalidString(resourceId)) { + return { valid: false, error: `The value "${appComponentsParsed[i].resourceId}" for resourceId in appComponents is invalid. Provide a valid resourceId.` }; + } + resourceId = resourceId.toLowerCase(); + let subscriptionId = getSubscriptionIdFromResourceId(resourceId); + let resourceType = getResourceTypeFromResourceId(resourceId); + let name = getResourceNameFromResourceId(resourceId); + let resourceGroup = getResourceGroupFromResourceId(resourceId); + if ((0, util_1.isNullOrUndefined)(resourceGroup) || (0, util_1.isNullOrUndefined)(subscriptionId) + || (0, util_1.isNullOrUndefined)(resourceType) || (0, util_1.isNullOrUndefined)(name) + || !isValidGUID(subscriptionId)) { + return { valid: false, error: `The value "${resourceId}" for resourceId in appComponents is invalid. Provide a valid resourceId.` }; + } + if (isInvalidString(appComponentsParsed[i].kind, true)) { + return { valid: false, error: `The value "${(_a = appComponentsParsed[i].kind) === null || _a === void 0 ? void 0 : _a.toString()}" for kind in appComponents is invalid. Provide a valid string.` }; + } + if (isInvalidString(appComponentsParsed[i].resourceName, true)) { + return { valid: false, error: `The value "${(_b = appComponentsParsed[i].resourceName) === null || _b === void 0 ? void 0 : _b.toString()}" for resourceName in appComponents is invalid. Provide a valid string.` }; + } + let resourceName = appComponentsParsed[i].resourceName || name; + if (!(0, util_1.isNullOrUndefined)(appComponentsParsed[i].metrics)) { + let metrics = appComponentsParsed[i].metrics; + if (!Array.isArray(metrics)) { + return { valid: false, error: `The value "${metrics === null || metrics === void 0 ? void 0 : metrics.toString()}" for metrics in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid list of metrics.` }; + } + for (let metric of metrics) { + if (!isDictionary(metric)) { + return { valid: false, error: `The value "${metric === null || metric === void 0 ? void 0 : metric.toString()}" for metrics in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid dictionary.` }; + } + if (metric && isInvalidString(metric.name)) { + return { valid: false, error: `The value "${(_c = metric.name) === null || _c === void 0 ? void 0 : _c.toString()}" for name in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid string.` }; + } + if (isInvalidString(metric.aggregation)) { + return { valid: false, error: `The value "${(_d = metric.aggregation) === null || _d === void 0 ? void 0 : _d.toString()}" for aggregation in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid string.` }; + } + if (isInvalidString(metric.namespace, true)) { + return { valid: false, error: `The value "${(_e = metric.namespace) === null || _e === void 0 ? void 0 : _e.toString()}" for namespace in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid string.` }; + } + } + } + else { + console.log(`Metrics not provided for the appComponent "${resourceName}", default metrics will be enabled for the same.`); + } + } + return { valid: true, error: "" }; +} +function validateReferenceIdentities(referenceIdentities) { + for (let referenceIdentity of referenceIdentities) { + if (!isDictionary(referenceIdentity)) { + return { valid: false, error: `The value "${referenceIdentity.toString()}" for referenceIdentities is invalid. Provide a valid dictionary with kind, value and type.` }; + } + if (referenceIdentity.value != undefined && typeof referenceIdentity.value != 'string') { + return { valid: false, error: `The value "${referenceIdentity.value.toString()}" for id in referenceIdentities is invalid. Provide a valid string.` }; + } + if (referenceIdentity.type != undefined && typeof referenceIdentity.type != 'string') { + return { valid: false, error: `The value "${referenceIdentity.type.toString()}" for type in referenceIdentities is invalid. Allowed values are "SystemAssigned" and "UserAssigned".` }; + } + if (!isValidReferenceIdentityKind(referenceIdentity.kind)) { + return { valid: false, error: `The value "${referenceIdentity.kind}" for kind in referenceIdentity is invalid. Allowed values are 'Metrics', 'Keyvault' and 'Engine'.` }; + } + if (referenceIdentity.type && !isValidManagedIdentityType(referenceIdentity.type)) { + return { valid: false, error: `The value "${referenceIdentity.type}" for type in referenceIdentities is invalid. Allowed values are "SystemAssigned" and "UserAssigned".` }; + } + if (!(0, util_1.isNullOrUndefined)(referenceIdentity.value) && referenceIdentity.type == UtilModels_1.ManagedIdentityType.SystemAssigned) { + return { valid: false, error: `The "reference identity value" should omitted or set to null when using the "SystemAssigned" identity type.` }; + } + if ((0, util_1.isNullOrUndefined)(referenceIdentity.value) && referenceIdentity.type == UtilModels_1.ManagedIdentityType.UserAssigned) { + return { valid: false, error: `The value for 'referenceIdentity value' cannot be null when using the 'UserAssigned' identity type. Provide a valid identity reference for 'reference identity value'.` }; + } + if (referenceIdentity.value && isInvalidManagedIdentityId(referenceIdentity.value)) { + return { valid: false, error: `The value "${referenceIdentity.value}" for reference identity is invalid. The value should be a string of the format: "/subscriptions/{subsId}/resourceGroups/{rgName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}".` }; + } + } + return { valid: true, error: "" }; +} +function validateOverRideParameters(overRideParams) { + try { + if (!(0, util_1.isNullOrUndefined)(overRideParams)) { + let overRideParamsObj; + try { + overRideParamsObj = JSON.parse(overRideParams); + } + catch (error) { + return { valid: false, error: `Invalid format provided in the ${InputConstants.overRideParametersLabel} field in pipeline, provide a valid json string.` }; + } + ; + let unSupportedKeys = []; + let supportedKeys = Object.keys(new constants_1.OverRideParametersModel()); + Object.keys(overRideParamsObj).forEach(element => { + if (supportedKeys.indexOf(element) == -1) { + unSupportedKeys.push(element); + } + }); + if (unSupportedKeys.length) { + const result = unSupportedKeys.map(element => `${element}`).join(", "); + return { valid: false, error: `The ${InputConstants.overRideParametersLabel} provided has unsupported field(s) "${result}".` }; + } + if (overRideParamsObj.testId != undefined) { + if (typeof overRideParamsObj.testId != 'string') { + return { valid: false, error: `The testId provided in the overrideParameters is not a string.` }; + } + } + if (overRideParamsObj.displayName != undefined) { + if (typeof overRideParamsObj.displayName != 'string') { + return { valid: false, error: `The displayName provided in the overrideParameters is not a string.` }; + } + } + if (overRideParamsObj.description != undefined) { + if (typeof overRideParamsObj.description != 'string') { + return { valid: false, error: `The description provided in the overrideParameters is not a string.` }; + } + } + if (overRideParamsObj.engineInstances != undefined) { + if (typeof overRideParamsObj.engineInstances != 'number') { + return { valid: false, error: `The engineInstances provided in the overrideParameters is not a number.` }; + } + } + if (!(0, util_1.isNullOrUndefined)(overRideParamsObj.autoStop)) { + let validation = validateAutoStop(overRideParamsObj.autoStop, true); + if (validation.valid == false) { + return validation; + } + } + } + } + catch (error) { + return { valid: false, error: (error !== null && error !== void 0 ? error : '').toString() }; + } + return { valid: true, error: "" }; +} +exports.validateOverRideParameters = validateOverRideParameters; +function validateOutputParametervariableName(outputVarName) { + if ((0, util_1.isNullOrUndefined)(outputVarName) || typeof outputVarName != 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(outputVarName)) { + return { valid: false, error: `Invalid output variable name '${outputVarName}'. Use only letters, numbers, and underscores.` }; + } + return { valid: true, error: "" }; +} +exports.validateOutputParametervariableName = validateOutputParametervariableName; +/* + 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 { + tempStr += criteriaString[i]; + } + } + 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; +} +exports.getPassFailCriteriaFromString = getPassFailCriteriaFromString; +/* + 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; + } +} +exports.ValidateCriteriaAndConvertToWorkingStringModel = ValidateCriteriaAndConvertToWorkingStringModel; +function validateUrl(url) { + var r = new RegExp(/(http|https):\/\/.*\/secrets\/.+$/); + return r.test(url); +} +exports.validateUrl = validateUrl; +function validateUrlcert(url) { + var r = new RegExp(/(http|https):\/\/.*\/certificates\/.+$/); + return r.test(url); +} +exports.validateUrlcert = validateUrlcert; +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 getDefaultRunDescription() { + const pipelineName = process.env.GITHUB_WORKFLOW || "Unknown Pipeline"; + return "Started using GH workflows" + (pipelineName ? "-" + pipelineName : ""); +} +exports.getDefaultRunDescription = getDefaultRunDescription; +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.validateTestRunParamsFromPipeline = validateTestRunParamsFromPipeline; +function getAllFileErrors(testObj) { + 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; + } + } + return fileErrors; +} +exports.getAllFileErrors = getAllFileErrors; diff --git a/lib/postProcessJob.js b/lib/postProcessJob.js new file mode 100644 index 00000000..b951cbad --- /dev/null +++ b/lib/postProcessJob.js @@ -0,0 +1,60 @@ +"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 }); +const UtilModels_1 = require("./models/UtilModels"); +const core = __importStar(require("@actions/core")); +const AuthenticationUtils_1 = require("./models/AuthenticationUtils"); +const TaskModels_1 = require("./models/TaskModels"); +const APISupport_1 = require("./models/APISupport"); +const util_1 = require("util"); +function run() { + return __awaiter(this, void 0, void 0, function* () { + try { + const runId = process.env[UtilModels_1.PostTaskParameters.runId]; + const baseUri = process.env[UtilModels_1.PostTaskParameters.baseUri]; + const isRunCompleted = process.env[UtilModels_1.PostTaskParameters.isRunCompleted]; + console.log(runId, baseUri, isRunCompleted); + if (!(0, util_1.isNullOrUndefined)(runId) && !(0, util_1.isNullOrUndefined)(baseUri) && ((0, util_1.isNullOrUndefined)(isRunCompleted) || isRunCompleted != 'true')) { + const yamlConfig = new TaskModels_1.YamlConfig(true); + const authContext = new AuthenticationUtils_1.AuthenticationUtils(); + const apiSupport = new APISupport_1.APISupport(authContext, yamlConfig); + yield apiSupport.stopTestRunPostProcess(baseUri, runId); + } + } + catch (err) { + core.debug("Failed to stop the test run:" + err.message); + } + }); +} +run(); diff --git a/lib/services/FeatureFlagService.js b/lib/services/FeatureFlagService.js new file mode 100644 index 00000000..fafaebd4 --- /dev/null +++ b/lib/services/FeatureFlagService.js @@ -0,0 +1,74 @@ +"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.FeatureFlagService = void 0; +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 { + constructor(authContext) { + this.featureFlagCache = {}; + this.authContext = authContext; + } + getFeatureFlagAsync(flag, baseUrl, useCache = true) { + return __awaiter(this, void 0, void 0, function* () { + 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 = this.authContext.getDataPlaneHeader(UtilModels_1.FetchCallType.get); + let flagResponse = yield FetchUtil.httpClientRetries(uri, headers, UtilModels_1.FetchCallType.get, 3, "", false, false); + try { + let flagObj = (yield util.getResultObj(flagResponse)); + this.featureFlagCache[flag.toString()] = flagObj.enabled; + return flagObj; + } + catch (error) { + // remove item from dict + // handle in case getFlag was called with cache true once and then with cache false, and failed during second call + // remove the item from cache so that it can be fetched again rather than using old value + delete this.featureFlagCache[flag.toString()]; + return null; + } + }); + } + isFeatureEnabledAsync(flag, baseUrl, useCache = true) { + return __awaiter(this, void 0, void 0, function* () { + let flagObj = yield this.getFeatureFlagAsync(flag, baseUrl, useCache); + return flagObj ? flagObj.enabled : false; + }); + } +} +exports.FeatureFlagService = FeatureFlagService; diff --git a/lib/services/FeatureFlags.js b/lib/services/FeatureFlags.js new file mode 100644 index 00000000..67ccad90 --- /dev/null +++ b/lib/services/FeatureFlags.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FeatureFlags = void 0; +var FeatureFlags; +(function (FeatureFlags) { + FeatureFlags["enableTestScriptFragments"] = "enableTestScriptFragments"; +})(FeatureFlags = exports.FeatureFlags || (exports.FeatureFlags = {})); diff --git a/src/main.ts b/src/main.ts index 83d9fc40..cf891623 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import * as util from './models/FileUtils'; -import { resultFolder } from "./models/UtilModels"; +import { OutputVariableInterface, OutPutVariablesConstants, PostTaskParameters, resultFolder } from "./models/UtilModels"; import * as fs from 'fs'; import * as core from '@actions/core'; import { AuthenticationUtils } from "./models/AuthenticationUtils"; @@ -15,6 +15,7 @@ async function run() { await authContext.authorize(); await apiSupport.getResource(); + core.exportVariable(PostTaskParameters.baseUri, apiSupport.baseURL); await apiSupport.getTestAPI(false); if (fs.existsSync(resultFolder)){ util.deleteFile(resultFolder); @@ -22,7 +23,12 @@ async function run() { fs.mkdirSync(resultFolder); await apiSupport.createTestAPI(); + + let outputVar: OutputVariableInterface = { + testRunId: yamlConfig.runTimeParams.testRunId + } + core.setOutput(`${yamlConfig.outputVariableName}.${OutPutVariablesConstants.testRunId}`, outputVar.testRunId); } catch (err:any) { core.setFailed(err.message); diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index dc16cc79..de71b0fa 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -1,22 +1,21 @@ -import { isNull, isNullOrUndefined } from "util"; +import { isNullOrUndefined } from "util"; import { AuthenticationUtils } from "./AuthenticationUtils"; -import { ApiVersionConstants, CallTypeForDP, FileType, reportZipFileName, resultZipFileName } from "./UtilModels"; +import { ApiVersionConstants, FetchCallType, FileType, PostTaskParameters, 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 { FileInfo, TestModel, ExistingParams, TestRunModel, AppComponents, ServerMetricConfig } from "./PayloadModels"; import { YamlConfig } from "./TaskModels"; import * as FetchUtil from './FetchHelper'; +import * as InputConstants from './InputConstants'; export class APISupport { authContext : AuthenticationUtils; yamlModel: YamlConfig; baseURL = ''; - existingParams: ExistingParams = {secrets: {}, env: {}, passFailCriteria: {}}; + existingParams: ExistingParams = {secrets: {}, env: {}, passFailCriteria: {}, appComponents: new Map()}; testId: string; - resourceName: string | undefined; - subName: string | undefined; constructor(authContext: AuthenticationUtils, yamlModel: YamlConfig) { this.authContext = authContext; @@ -30,10 +29,8 @@ export class APISupport { 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'); - this.resourceName = resource_name; - this.subName = this.authContext.subscriptionId; + let response = await FetchUtil.httpClientRetries(armEndpoint.toString(),header,FetchCallType.get,3,""); + let resource_name: string | undefined = core.getInput(InputConstants.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); @@ -48,10 +45,10 @@ export class APISupport { } async getTestAPI(validate:boolean, returnTestObj:boolean = false) : Promise<[string | undefined, TestModel] | string | undefined> { - var urlSuffix = "tests/"+this.testId+"?api-version="+ ApiVersionConstants.tm2024Version; + var urlSuffix = "tests/"+this.testId+"?api-version="+ ApiVersionConstants.latestVersion; urlSuffix = this.baseURL+urlSuffix; - let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.get); - let testResult = await FetchUtil.httpClientRetries(urlSuffix,header,'get',3,""); + let header = await this.authContext.getDataPlaneHeader(FetchCallType.get); + let testResult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.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 " @@ -62,8 +59,8 @@ export class APISupport { 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); + let errorObj:any=await Util.getResultObj(testResult); + let err = errorObj?.error?.message ? errorObj?.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); @@ -101,28 +98,59 @@ export class APISupport { } } } + + async getAppComponents() { + let urlSuffix = "tests/"+this.testId+"/app-components/"+"?api-version="+ ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL+urlSuffix; + let header = await this.authContext.getDataPlaneHeader(FetchCallType.get); + let appComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.get,3,""); + if(appComponentsResult.message.statusCode == 200) { + let appComponentsObj:AppComponents = await Util.getResultObj(appComponentsResult); + for(let guid in appComponentsObj.components){ + let resourceId = appComponentsObj.components[guid]?.resourceId ?? ""; + if(this.existingParams.appComponents.has(resourceId?.toLowerCase())) { + let existingGuids = this.existingParams.appComponents.get(resourceId?.toLowerCase()) ?? []; + existingGuids.push(guid); + this.existingParams.appComponents.set(resourceId.toLowerCase(), existingGuids); + } else { + this.existingParams.appComponents.set(resourceId.toLowerCase(), [guid]); + } + } + } + } + + async getServerMetricsConfig() { + let urlSuffix = "tests/"+this.testId+"/server-metrics-config/"+"?api-version="+ ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL+urlSuffix; + let header = await this.authContext.getDataPlaneHeader(FetchCallType.get); + let serverComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.get,3,""); + if(serverComponentsResult.message.statusCode == 200) { + let serverComponentsObj: ServerMetricConfig = await Util.getResultObj(serverComponentsResult); + this.yamlModel.mergeExistingServerCriteria(serverComponentsObj); + } + } async deleteFileAPI(filename:string) { - var urlSuffix = "tests/"+this.testId+"/files/"+filename+"?api-version="+ ApiVersionConstants.tm2024Version; + var urlSuffix = "tests/"+this.testId+"/files/"+filename+"?api-version="+ ApiVersionConstants.latestVersion; urlSuffix = this.baseURL+urlSuffix; - let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.delete); - let delFileResult = await FetchUtil.httpClientRetries(urlSuffix,header,'del',3,""); + let header = await this.authContext.getDataPlaneHeader(FetchCallType.delete); + let delFileResult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.delete,3,""); if(delFileResult.message.statusCode != 204) { - let delFileObj:any=await Util.getResultObj(delFileResult); - let Message: string = delFileObj ? delFileObj.message : Util.ErrorCorrection(delFileResult); + let errorObj:any=await Util.getResultObj(delFileResult); + let Message: string = errorObj ? errorObj.message : Util.ErrorCorrection(delFileResult); throw new Error(Message); } } async createTestAPI() { - let urlSuffix = "tests/"+this.testId+"?api-version="+ ApiVersionConstants.tm2024Version; + let urlSuffix = "tests/"+this.testId+"?api-version="+ ApiVersionConstants.latestVersion; 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)); + let header = await this.authContext.getDataPlaneHeader(FetchCallType.patch); + let createTestresult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.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)); + let errorObj:any=await Util.getResultObj(createTestresult); + console.log(errorObj ? errorObj : Util.ErrorCorrection(createTestresult)); throw new Error("Error in creating test: " + this.testId); } if(createTestresult.message.statusCode == 201) { @@ -174,68 +202,110 @@ export class APISupport { } await this.uploadConfigFile(); } - + + async patchAppComponents() { + let urlSuffix = "tests/"+this.testId+"/app-components/"+"?api-version="+ ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL+urlSuffix; + let appComponentsData : AppComponents = this.yamlModel.getAppComponentsData(); + let header = await this.authContext.getDataPlaneHeader(FetchCallType.patch); + let appComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.patch,3,JSON.stringify(appComponentsData)); + if(!isNullOrUndefined(appComponentsData?.components) && Object.keys(appComponentsData.components).length == 0) { + return; + } + if(appComponentsResult.message.statusCode != 200 && appComponentsResult.message.statusCode != 201) { + let errorObj:any=await Util.getResultObj(appComponentsResult); + console.log(errorObj ? errorObj : Util.ErrorCorrection(appComponentsResult)); + throw new Error("Error in updating app components"); + } else { + console.log("Updated app components successfully"); + + await this.getServerMetricsConfig(); + await this.patchServerMetrics(); + } + } + + async patchServerMetrics() { + let urlSuffix = "tests/"+this.testId+"/server-metrics-config/"+"?api-version="+ ApiVersionConstants.latestVersion; + urlSuffix = this.baseURL+urlSuffix; + let serverMetricsData : ServerMetricConfig = { + metrics: this.yamlModel.serverMetricsConfig + } + if(!isNullOrUndefined(serverMetricsData?.metrics) && Object.keys(serverMetricsData.metrics).length == 0) { + return; + } + let header = await this.authContext.getDataPlaneHeader(FetchCallType.patch); + let serverMetricsResult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.patch,3,JSON.stringify(serverMetricsData)); + if(serverMetricsResult.message.statusCode != 200 && serverMetricsResult.message.statusCode != 201) { + let errorObj:any=await Util.getResultObj(serverMetricsResult); + console.log(errorObj ? errorObj : Util.ErrorCorrection(serverMetricsResult)); + throw new Error("Error in updating server metrics"); + } else { + console.log("Updated server metrics successfully"); + } + } + 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); - } + let retry = 5; + let filepath = this.yamlModel.testPlan; + let filename = this.yamlModel.getFileName(filepath); + let urlSuffix = "tests/"+this.testId+"/files/"+filename+"?api-version="+ ApiVersionConstants.latestVersion; + + 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(FetchCallType.put) + let uploadresult = await FetchUtil.httpClientRetries(urlSuffix,headers,FetchCallType.put,3,filepath, true); + if(uploadresult.message.statusCode != 201){ + let errorObj:any = await Util.getResultObj(uploadresult); + console.log(errorObj ? errorObj : 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 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}`); } - - await this.createTestRun(); + throw new Error("Validation of one or more files failed. Please correct the errors and try again."); } - else if(validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED") - throw new Error("TestPlan validation timeout. Please try again.") - else - throw new Error("TestPlan validation Failed."); + + await this.patchAppComponents(); + 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() @@ -244,13 +314,13 @@ export class APISupport { 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); + let urlSuffix = "tests/"+ this.testId +"/files/"+filename+"?api-version="+ ApiVersionConstants.latestVersion + ("&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); + let headers = await this.authContext.getDataPlaneHeader(FetchCallType.put); + let uploadresult = await FetchUtil.httpClientRetries(urlSuffix,headers,FetchCallType.put,3,filepath, true); if(uploadresult.message.statusCode != 201){ - let uploadObj:any = await Util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + let errorObj:any = await Util.getResultObj(uploadresult); + console.log(errorObj ? errorObj : Util.ErrorCorrection(uploadresult)); throw new Error("Error in uploading config file for the created test"); } }; @@ -266,13 +336,13 @@ export class APISupport { 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; + var urlSuffix = "tests/"+this.testId+"/files/"+filename+"?api-version=" + ApiVersionConstants.latestVersion+"&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); + let headers = await this.authContext.getDataPlaneHeader(FetchCallType.put); + let uploadresult = await FetchUtil.httpClientRetries(urlSuffix,headers,FetchCallType.put,3,filepath, true); if(uploadresult.message.statusCode != 201){ - let uploadObj:any = await Util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + let errorObj:any = await Util.getResultObj(uploadresult); + console.log(errorObj ? errorObj : Util.ErrorCorrection(uploadresult)); throw new Error("Error in uploading config file for the created test"); } } @@ -288,13 +358,13 @@ export class APISupport { 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; + let urlSuffix = "tests/"+this.testId+"/files/"+filename+"?api-version="+ ApiVersionConstants.latestVersion+"&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); + let headers = await this.authContext.getDataPlaneHeader(FetchCallType.put); + let uploadresult = await FetchUtil.httpClientRetries(urlSuffix,headers,FetchCallType.put,3,propertyFile, true); if(uploadresult.message.statusCode != 201){ - let uploadObj:any = await Util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); + let errorObj:any = await Util.getResultObj(uploadresult); + console.log(errorObj ? errorObj : Util.ErrorCorrection(uploadresult)); throw new Error("Error in uploading TestPlan for the created test"); } console.log(`Uploaded user properties file for the test successfully.`); @@ -306,12 +376,12 @@ export class APISupport { try { var startData = this.yamlModel.getStartTestData(); const testRunId = this.yamlModel.runTimeParams.testRunId; - let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.tm2024Version; + let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.latestVersion; urlSuffix = this.baseURL+urlSuffix; - + core.exportVariable(PostTaskParameters.runId, testRunId); 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 header = await this.authContext.getDataPlaneHeader(FetchCallType.patch); + let startTestresult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.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)); @@ -320,9 +390,9 @@ export class APISupport { let startTime = new Date(); 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 '"+this.resourceName+"' in subscription '"+this.subName+"'") - console.log("2. On the Tests page, go to test '"+this.testId+"'") + console.log("\nView the load test run in Azure portal by following the steps:"); + console.log("1. Go to your Azure Load Testing resource '"+Util.getResourceNameFromResourceId(this.authContext.resourceId)+"' in subscription '"+this.authContext.subscriptionName+"'"); + console.log("2. On the Tests page, go to test '"+this.yamlModel.displayName+"'"); console.log("3. Go to test run '"+testRunDao.displayName+"'\n"); await this.getTestRunAPI(testRunId, status, startTime); } @@ -336,18 +406,18 @@ export class APISupport { async getTestRunAPI(testRunId:string, testStatus:string, startTime : Date) { - let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.tm2024Version; + let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.latestVersion; 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 header = await this.authContext.getDataPlaneHeader(FetchCallType.get); + let testRunResult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.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; + testStatus = testRunObj.status ?? testStatus; if(Util.isTerminalTestStatus(testStatus)) { let vusers = null; let count = 0; @@ -356,8 +426,8 @@ export class APISupport { // 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,""); + let header = await this.authContext.getDataPlaneHeader(FetchCallType.get); + let testRunResult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.get,3,""); testRunObj = await Util.getResultObj(testRunResult); if(testRunObj == null){ throw new Error(Util.ErrorCorrection(testRunResult)); @@ -373,6 +443,7 @@ export class APISupport { reportsAvailable = true; } } + if(testRunObj && testRunObj.startDateTime){ startTime = new Date(testRunObj.startDateTime); } @@ -385,10 +456,10 @@ export class APISupport { Util.printCriteria(testRunObj.passFailCriteria.passFailMetrics) if(testRunObj.testRunStatistics != null && testRunObj.testRunStatistics != undefined) Util.printClientMetrics(testRunObj.testRunStatistics); - + core.exportVariable(PostTaskParameters.isRunCompleted, 'true'); let testResultUrl = Util.getResultFolder(testRunObj.testArtifacts); if(testResultUrl != null) { - const response = await FetchUtil.httpClientRetries(testResultUrl,{},'get',3,""); + const response = await FetchUtil.httpClientRetries(testResultUrl,{},FetchCallType.get,3,""); if (response.message.statusCode != 200) { let respObj:any = await Util.getResultObj(response); console.log(respObj ? respObj : Util.ErrorCorrection(response)); @@ -400,7 +471,7 @@ export class APISupport { } let testReportUrl = Util.getReportFolder(testRunObj.testArtifacts); if(testReportUrl != null) { - const response = await FetchUtil.httpClientRetries(testReportUrl,{},'get',3,""); + const response = await FetchUtil.httpClientRetries(testReportUrl,{},FetchCallType.get,3,""); if (response.message.statusCode != 200) { let respObj:any = await Util.getResultObj(response); console.log(respObj ? respObj : Util.ErrorCorrection(response)); @@ -411,11 +482,11 @@ export class APISupport { } } - if(!isNull(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { + if(!isNullOrUndefined(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { core.setFailed("TestResult: "+ testRunObj.testResult); return; } - if(!isNull(testRunObj.status) && Util.isStatusFailed(testRunObj.status)) { + if(!isNullOrUndefined(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; @@ -434,4 +505,11 @@ export class APISupport { } } } + + // this api is special case and doesnot use the yamlModels, instead uses the task variables for the same, this doesnot have the initialisation too. + async stopTestRunPostProcess(baseUri: string, runId: string) { + let urlSuffix = baseUri + "test-runs/"+runId+":stop?api-version=" + ApiVersionConstants.latestVersion; + let headers = await this.authContext.getDataPlaneHeader(FetchCallType.post); + await FetchUtil.httpClientRetries(urlSuffix,headers,FetchCallType.post,3,''); + } } \ No newline at end of file diff --git a/src/models/AuthenticationUtils.ts b/src/models/AuthenticationUtils.ts index 7bd631df..45d269a7 100644 --- a/src/models/AuthenticationUtils.ts +++ b/src/models/AuthenticationUtils.ts @@ -1,9 +1,10 @@ import { isNullOrUndefined } from "util"; import * as core from '@actions/core'; import { execFile } from "child_process"; -import { CallTypeForDP, ContentTypeMap, TokenScope } from "./UtilModels"; +import { FetchCallType, ContentTypeMap, TokenScope } from "./UtilModels"; import { jwtDecode, JwtPayload } from "jwt-decode"; import { IHeaders } from "typed-rest-client/Interfaces"; +import * as InputConstants from "./InputConstants"; export class AuthenticationUtils { dataPlanetoken : string = ''; @@ -15,20 +16,21 @@ export class AuthenticationUtils { armEndpoint='https://management.azure.com'; resourceId : string = ''; + subscriptionName: 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'); + this.subscriptionName = await this.getSubName(); + const rg: string | undefined = core.getInput(InputConstants.resourceGroup); + const ltres: string | undefined = core.getInput(InputConstants.loadTestResource); if(isNullOrUndefined(rg) || rg == ''){ - throw new Error(`The input field "resourceGroup" is empty. Provide an existing resource group name.`); + throw new Error(`The input field "${InputConstants.resourceGroupLabel}" 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.`); + throw new Error(`The input field "${InputConstants.loadTestResourceLabel}" is empty. Provide an existing load test resource name.`); } this.resourceId = "/subscriptions/"+this.subscriptionId+"/resourcegroups/"+rg+"/providers/microsoft.loadtestservice/loadtests/"+ltres; @@ -110,7 +112,7 @@ export class AuthenticationUtils { } } - async getDataPlaneHeader(apicallType : CallTypeForDP) : Promise { + async getDataPlaneHeader(apicallType : FetchCallType) : Promise { if(!this.isValid(TokenScope.Dataplane)) { let tokenRes:any = await this.getTokenAPI(TokenScope.Dataplane); this.dataPlanetoken = tokenRes; @@ -121,6 +123,20 @@ export class AuthenticationUtils { }; return headers; } + + async getSubName() { + try { + const cmdArguments = ["account", "show"]; + var result: any = await this.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 armTokenHeader() { // right now only get calls from the GH, so no need of content type for now for the get calls. diff --git a/src/models/FetchHelper.ts b/src/models/FetchHelper.ts index 0759dfc1..b733319d 100644 --- a/src/models/FetchHelper.ts +++ b/src/models/FetchHelper.ts @@ -1,29 +1,46 @@ import { IHeaders, IHttpClientResponse } from 'typed-rest-client/Interfaces'; import { ErrorCorrection, getResultObj, getUniqueId, sleep } from './util'; -import { correlationHeader } from './UtilModels'; +import { FetchCallType, 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' +const methodEnumToString : { [key in FetchCallType] : string } = { + [FetchCallType.get] : "get", + [FetchCallType.post] : "post", + [FetchCallType.put] : "put", + [FetchCallType.delete] : "del", + [FetchCallType.patch] : "patch" +} + // (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{ +export async function httpClientRetries(urlSuffix : string, header : IHeaders, method : FetchCallType, 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'){ + if(method == FetchCallType.get){ httpResponse = await httpClient.get(urlSuffix, header); - } - else if(method == 'del'){ + } else if(method == FetchCallType.delete){ httpResponse = await httpClient.del(urlSuffix, header); - } - else if(method == 'put' && isUploadCall){ + } else if(method == FetchCallType.post){ + httpResponse = await httpClient.post(urlSuffix, data, header); + } else if(method == FetchCallType.put && isUploadCall){ let fileContent = uploadFileData(data); - httpResponse = await httpClient.request(method,urlSuffix, fileContent, header); - } - else{ - httpResponse = await httpClient.request(method,urlSuffix, data, header); + httpResponse = await httpClient.request(methodEnumToString[method], urlSuffix, fileContent, header); + } else{ + const githubBaseUrl = process.env.GITHUB_SERVER_URL; + const repository = process.env.GITHUB_REPOSITORY; + const runId = process.env.GITHUB_RUN_ID; + + const pipelineName = process.env.GITHUB_WORKFLOW || "Unknown Pipeline"; + const pipelineUrl = `${githubBaseUrl}/${repository}/actions/runs/${runId}`; + + header['x-ms-pipelineUrl'] = pipelineUrl; + header['x-ms-pipelineName'] = pipelineName; // setting these for patch calls. + + httpResponse = await httpClient.request(methodEnumToString[method], urlSuffix, data, header); } if(httpResponse.message.statusCode!= undefined && httpResponse.message.statusCode >= 300){ core.debug(`correlation id : ${correlationId}`); diff --git a/src/models/InputConstants.ts b/src/models/InputConstants.ts new file mode 100644 index 00000000..5541de28 --- /dev/null +++ b/src/models/InputConstants.ts @@ -0,0 +1,22 @@ +export const testRunName = 'loadTestRunName'; +export const runDescription = 'loadTestRunDescription'; +export const overRideParameters = 'overrideParameters'; +export const outputVariableName = 'outputVariableName'; +export const envVars = 'env'; +export const secrets = 'secrets'; +export const serviceConnectionName = 'connectedServiceNameARM'; +export const resourceGroup = 'resourceGroup'; +export const loadTestResource = 'loadTestResource'; +export const loadTestConfigFile = 'loadTestConfigFile'; + +// labels user visible strings +export const testRunNameLabel = 'Load Test Run Name'; +export const runDescriptionLabel = 'Load Test Run Description'; +export const overRideParametersLabel = 'Override Parameters'; +export const outputVariableNameLabel = 'Output Variable Name'; +export const envVarsLabel = 'env'; +export const secretsLabel = 'Secrets'; +export const serviceConnectionNameLabel = 'Azure subscription'; +export const resourceGroupLabel = 'Resource Group'; +export const loadTestResourceLabel = 'Load Test Resource'; +export const loadTestConfigFileLabel = 'Load Test Config File'; \ No newline at end of file diff --git a/src/models/PayloadModels.ts b/src/models/PayloadModels.ts index c4f1352f..b33996fb 100644 --- a/src/models/PayloadModels.ts +++ b/src/models/PayloadModels.ts @@ -27,6 +27,32 @@ export interface PassFailMetric { result?: string | null; }; +export interface AppComponentDefinition { + resourceName: string; + kind: string | null; + resourceId: string; + resourceType: string; + subscriptionId: string; + resourceGroup: string; +} + +export interface AppComponents { + components: {[key: string]: AppComponentDefinition | null}; +} + +export interface ServerMetricConfig { + metrics: { [key: string]: ResourceMetricModel | null }; +} + +export interface ResourceMetricModel { + name: string| null; + aggregation: string; + metricNamespace : string | null; + resourceId: string; + resourceType: string| null; + id: string; +} + export interface TestModel { testId?: string; description?: string; @@ -60,12 +86,12 @@ export interface TestRunArtifacts { } export interface TestRunModel extends TestModel { - testRunId? : string; + testRunId: string; errorDetails? : any; testArtifacts?: TestRunArtifacts; - testResult: string; - status: string; - testRunStatistics : { [ key: string ] : Statistics }; + testResult?: string; + status?: string; + testRunStatistics? : { [ key: string ] : Statistics }; virtualUserHours?: number; virtualUsers?: number; startDateTime?: string; @@ -139,6 +165,7 @@ export interface ExistingParams { secrets: { [key: string]: SecretMetadata | null }; env: { [key: string]: string | null }; passFailCriteria: { [key: string]: PassFailMetric | null }; + appComponents: Map; // key: resourceId, value: guids of the app components, so that we can make them null when the resourceId is removed from the config file. } export enum ManagedIdentityTypeForAPI { diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts index 0e298dc6..caee0a05 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -6,10 +6,12 @@ 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, ManagedIdentityTypeForAPI } from "./PayloadModels"; -import { AllManagedIdentitiesSegregated, AutoStopCriteriaObjYaml, ParamType, ReferenceIdentityKinds, RunTimeParams } from "./UtilModels"; +import { AppComponentDefinition, AppComponents, AutoStopCriteria, AutoStopCriteria as autoStopCriteriaObjOut, ManagedIdentityTypeForAPI, ResourceMetricModel, ServerMetricConfig, TestRunModel } from "./PayloadModels"; +import { AllManagedIdentitiesSegregated, AutoStopCriteriaObjYaml, ParamType, ReferenceIdentityKinds, RunTimeParams, ServerMetricsClientModel, ValidationModel } from "./UtilModels"; import * as core from '@actions/core'; import { PassFailMetric, ExistingParams, TestModel, CertificateMetadata, SecretMetadata, RegionConfiguration } from "./PayloadModels"; +import { autoStopDisable, OutputVariableName } from "./constants"; +import * as InputConstants from "./InputConstants"; export class YamlConfig { testId:string = ''; @@ -43,17 +45,26 @@ export class YamlConfig { regionalLoadTestConfig: RegionConfiguration[] | null = null; runTimeParams: RunTimeParams = {env: {}, secrets: {}, runDisplayName: '', runDescription: '', testId: '', testRunId: ''}; - constructor() { - let yamlFile = core.getInput('loadTestConfigFile') ?? ''; + appComponents: { [key: string] : AppComponentDefinition | null } = {}; + serverMetricsConfig: { [key: string] : ResourceMetricModel | null } = {}; + + addDefaultsForAppComponents: { [key: string]: boolean } = {}; // when server components are not given for few app components, we need to add the defaults for this. + outputVariableName: string = OutputVariableName; + + constructor(isPostProcess: boolean = false) { + if(isPostProcess) { + return; + } + let yamlFile = core.getInput(InputConstants.loadTestConfigFile) ?? ''; if(isNullOrUndefined(yamlFile) || yamlFile == ''){ - throw new Error(`The input field "loadTestConfigFile" is empty. Provide the path to load test yaml file.`); + throw new Error(`The input field "${InputConstants.loadTestConfigFileLabel}" 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); + let validConfig : ValidationModel = 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`); } @@ -125,6 +136,12 @@ export class YamlConfig { if(config.certificates != undefined){ this.certificates = this.parseParameters(config.certificates, ParamType.cert) as CertificateMetadata | null; } + + if(config.appComponents != undefined) { + let appcomponents = config.appComponents as Array; + this.getAppComponentsAndServerMetricsConfig(appcomponents); + } + if(config.keyVaultReferenceIdentity != undefined || config.keyVaultReferenceIdentityType != undefined) { this.keyVaultReferenceIdentityType = config.keyVaultReferenceIdentity ? ManagedIdentityTypeForAPI.UserAssigned : ManagedIdentityTypeForAPI.SystemAssigned; this.keyVaultReferenceIdentity = config.keyVaultReferenceIdentity ?? null; @@ -137,13 +154,7 @@ export class YamlConfig { 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+"."); } @@ -151,6 +162,44 @@ export class YamlConfig { Util.validateTestRunParamsFromPipeline(this.runTimeParams); } + getAppComponentsAndServerMetricsConfig(appComponents: Array) { + for(let value of appComponents) { + let resourceId = value.resourceId.toLowerCase(); + this.appComponents[resourceId] = { + resourceName: (value.resourceName || Util.getResourceNameFromResourceId(resourceId)), + kind: value.kind ?? null, + resourceType: Util.getResourceTypeFromResourceId(resourceId) ?? '', + resourceId: resourceId, + subscriptionId: Util.getSubscriptionIdFromResourceId(resourceId) ?? '', + resourceGroup: Util.getResourceGroupFromResourceId(resourceId) ?? '' + }; + let metrics = (value.metrics ?? []) as Array; + + if(this.addDefaultsForAppComponents[resourceId] == undefined) { + this.addDefaultsForAppComponents[resourceId] = metrics.length == 0; + } else { + this.addDefaultsForAppComponents[resourceId] = this.addDefaultsForAppComponents[resourceId] && metrics.length == 0; + // when the same resource has metrics at one place, but not at other, we dont need defaults anymore. + } + + for(let serverComponent of metrics) { + let key : string = resourceId.toLowerCase() + '/' + (serverComponent.namespace ?? Util.getResourceTypeFromResourceId(resourceId)) + '/' + serverComponent.name; + if(!this.serverMetricsConfig.hasOwnProperty(key) || isNullOrUndefined(this.serverMetricsConfig[key])) { + this.serverMetricsConfig[key] = { + name: serverComponent.name, + aggregation: serverComponent.aggregation, + metricNamespace: serverComponent.namespace ?? Util.getResourceTypeFromResourceId(resourceId), + resourceId: resourceId, + resourceType: Util.getResourceTypeFromResourceId(resourceId) ?? '', + id: key + } + } else { + this.serverMetricsConfig[key]!.aggregation = this.serverMetricsConfig[key]!.aggregation + "," + serverComponent.aggregation; + } + } + } + } + getReferenceIdentities(referenceIdentities: {[key: string]: string}[]) { let segregatedManagedIdentities : AllManagedIdentitiesSegregated = Util.validateAndGetSegregatedManagedIdentities(referenceIdentities); @@ -171,8 +220,36 @@ export class YamlConfig { } } + getOverRideParams() { + let overRideParams = core.getInput(InputConstants.overRideParameters); + if(overRideParams) { + let overRideParamsObj = JSON.parse(overRideParams); + + if(overRideParamsObj.testId != undefined) { + this.testId = overRideParamsObj.testId; + } + if(overRideParamsObj.displayName != undefined) { + this.displayName = overRideParamsObj.displayName; + } + if(overRideParamsObj.description != undefined) { + this.description = overRideParamsObj.description; + } + if(overRideParamsObj.engineInstances != undefined) { + this.engineInstances = overRideParamsObj.engineInstances; + } + if(overRideParamsObj.autoStop != undefined) { + this.autoStop = this.getAutoStopCriteria(overRideParamsObj.autoStop); + } + } + } + + getOutPutVarName() { + let outputVarName = core.getInput(InputConstants.outputVariableName) ?? OutputVariableName; + this.outputVariableName = outputVarName; + } + getRunTimeParams() { - var secretRun = core.getInput('secrets'); + var secretRun = core.getInput(InputConstants.secrets); let secretsParsed : {[key: string] : SecretMetadata} = {}; let envParsed : {[key: string] : string} = {}; if(secretRun) { @@ -189,10 +266,10 @@ export class YamlConfig { } 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"); + throw new Error(`Invalid format of ${InputConstants.secretsLabel} in the pipeline file. Refer to the pipeline syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-configure-load-test-cicd?tabs=pipelines#update-the-azure-pipelines-workflow`); } } - var eRun = core.getInput('env'); + var eRun = core.getInput(InputConstants.envVars); if(eRun) { try { var obj = JSON.parse(eRun); @@ -207,13 +284,35 @@ export class YamlConfig { } 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"); + throw new Error(`Invalid format of ${InputConstants.envVarsLabel} in the pipeline file. Refer to the pipeline syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-configure-load-test-cicd?tabs=pipelines#update-the-azure-pipelines-workflow`); } } - const runDisplayName = core.getInput('loadTestRunName') ?? Util.getDefaultTestRunName(); - const runDescription = core.getInput('loadTestRunDescription') ?? Util.getDefaultRunDescription(); + let runDisplayNameInput = core.getInput(InputConstants.testRunName); + const runDisplayName = !isNullOrUndefined(runDisplayNameInput) && runDisplayNameInput != '' ? runDisplayNameInput : Util.getDefaultTestRunName(); + let runDescriptionInput = core.getInput(InputConstants.runDescription); + const runDescription = !isNullOrUndefined(runDescriptionInput) && runDescriptionInput != '' ? runDescriptionInput : Util.getDefaultRunDescription(); let runTimeParams : RunTimeParams = {env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: ''}; + this.runTimeParams = runTimeParams; + let overRideParamsInput = core.getInput(InputConstants.overRideParameters); + let outputVariableNameInput = core.getInput(InputConstants.outputVariableName); + let overRideParams = !isNullOrUndefined(overRideParamsInput) && overRideParamsInput != '' ? overRideParamsInput : undefined; + let outputVarName = !isNullOrUndefined(outputVariableNameInput) && outputVariableNameInput != '' ? outputVariableNameInput : OutputVariableName; + console.log(`overRideParams: ${overRideParams}`, `outputVarName: ${outputVarName}`); + let validation = Util.validateOverRideParameters(overRideParams); + if(validation.valid == false) { + console.log(validation.error); + throw new Error(`Invalid ${InputConstants.overRideParametersLabel}. Refer to the pipeline syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-configure-load-test-cicd?tabs=pipelines#update-the-azure-pipelines-workflow`); + } + + validation = Util.validateOutputParametervariableName(outputVarName); + if(validation.valid == false) { + console.log(validation.error); + throw new Error(`Invalid ${InputConstants.outputVariableNameLabel}. Refer to the pipeline syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-configure-load-test-cicd?tabs=pipelines#update-the-azure-pipelines-workflow`); + } + + this.getOverRideParams(); + this.getOutPutVarName(); return runTimeParams; } @@ -255,6 +354,36 @@ export class YamlConfig { if(!this.env.hasOwnProperty(key)) this.env[key] = null; } + + for(let [resourceId, keys] of existingData.appComponents) { + if(!this.appComponents.hasOwnProperty(resourceId.toLowerCase())) { + for(let key of keys) { + this.appComponents[key] = null; + } + } else { + for(let key of keys) { + if(key != null && key != resourceId.toLowerCase()) { + this.appComponents[key] = null; + } + } + } + } + } + + mergeExistingServerCriteria(existingServerCriteria: ServerMetricConfig) { + for(let key in existingServerCriteria.metrics) { + let resourceId = existingServerCriteria.metrics[key]?.resourceId?.toLowerCase() ?? ""; + if(this.addDefaultsForAppComponents.hasOwnProperty(resourceId) && !this.addDefaultsForAppComponents[resourceId] && !this.serverMetricsConfig.hasOwnProperty(key)) { + this.serverMetricsConfig[key] = null; + } + } + } + + getAppComponentsData() : AppComponents { + let appComponentsApiModel : AppComponents = { + components: this.appComponents + } + return appComponentsApiModel; } getCreateTestData(existingData:ExistingParams) { @@ -288,17 +417,25 @@ export class YamlConfig { return data; } - getStartTestData() { + getStartTestData() : TestRunModel{ this.runTimeParams.testId = this.testId; this.runTimeParams.testRunId = Util.getUniqueId(); - return this.runTimeParams; + let startData : TestRunModel = { + testId: this.testId, + testRunId: this.runTimeParams.testRunId, + environmentVariables: this.runTimeParams.env, + secrets: this.runTimeParams.secrets, + displayName: this.runTimeParams.runDisplayName, + description: this.runTimeParams.runDescription + } + return startData; } 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") { + if (autoStopInput == autoStopDisable) { let data = { autoStopDisabled : true, errorRate: 90, diff --git a/src/models/UtilModels.ts b/src/models/UtilModels.ts index 64e7777b..407c410f 100644 --- a/src/models/UtilModels.ts +++ b/src/models/UtilModels.ts @@ -32,11 +32,12 @@ export enum TokenScope { ControlPlane } -export enum CallTypeForDP { +export enum FetchCallType { get, patch, put, - delete + delete, + post } export interface PassFailCount { @@ -44,11 +45,12 @@ export interface PassFailCount { 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 const ContentTypeMap : { [key in FetchCallType]: string | null } = { + [FetchCallType.get]: null, + [FetchCallType.patch]: 'application/merge-patch+json', + [FetchCallType.put]: 'application/octet-stream', + [FetchCallType.delete]: 'application/json', + [FetchCallType.post]: 'application/json' } export enum FileType{ @@ -66,8 +68,7 @@ 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 latestVersion = '2024-12-01-preview'; export const tm2022Version = '2022-11-01'; export const cp2022Version = '2022-12-01' } @@ -93,7 +94,32 @@ export enum ManagedIdentityType { UserAssigned = "UserAssigned", } +export interface ServerMetricsClientModel { + name: string; + aggregation: string; + namespace?: string; +} + export interface AllManagedIdentitiesSegregated { referenceIdentityValuesUAMIMap: { [key in ReferenceIdentityKinds]: string[] }, referenceIdentiesSystemAssignedCount : { [key in ReferenceIdentityKinds]: number } -} \ No newline at end of file +} + +export interface ValidationModel { + valid: boolean; + error: string; +} + +export interface OutputVariableInterface { + testRunId: string; +} + +export module PostTaskParameters { + export const runId = 'LOADTEST_RUNID'; + export const baseUri = 'LOADTEST_RESOURCE_URI'; + export const isRunCompleted = 'LOADTEST_RUN_COMPLETED'; // this is set when the task is completed, to avoid get calls for the test again. +} + +export module OutPutVariablesConstants { + export const testRunId = 'testRunId'; +} diff --git a/src/models/constants.ts b/src/models/constants.ts index 7a39654d..8e209f93 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -1,66 +1,52 @@ -export const defaultYaml: any = +export class DefaultYamlModel { - version: 'v0.1', + version: string =''; + testId: string = ''; + testName: string = ''; + displayName: string = ''; + description: string = ''; + testPlan: string = ''; + testType: string = ''; + engineInstances: number = 0; + subnetId: string = ''; + publicIPDisabled: boolean = false; + configurationFiles: Array = []; + zipArtifacts: Array = []; + splitAllCSVs: boolean = false; + properties: { userPropertyFile: string } = { userPropertyFile: '' }; + env: Array<{ name: string, value: string }> = []; + certificates: Array<{ name: string, value: string }> = []; + secrets: Array<{ name: string, value: string }> = []; + failureCriteria: Array = []; + appComponents: Array<{resourceId: string, kind: string, metrics: Array<{name: string, aggregation: string, namespace?: string}>}> = []; + autoStop: { errorPercentage: number, timeWindow: number } = { errorPercentage: 0, timeWindow: 0 }; + keyVaultReferenceIdentity: string = ''; + keyVaultReferenceIdentityType: string = ''; + regionalLoadTestConfig: Array<{region: string, engineInstances: number}> = []; + referenceIdentities: Array<{kind: string, type: string, value: string}> = []; +} + +export const overRideParamsJSON: any = { + testId: 'SampleTest', - testName: 'SampleTest', - displayName: 'Sample Test', + displayName: 'SampleTest', description: 'Load test website home page', - testPlan: 'SampleTest.jmx', - testType: 'JMX', - engineInstances: 2, - subnetId: '/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Network/virtualNetworks/load-testing-vnet/subnets/load-testing', - publicIPDisabled: false, - configurationFiles: ['sampledata.csv'], - zipArtifacts: ['bigdata.zip'], - splitAllCSVs: true, - properties: { userPropertyFile: 'user.properties' }, - env: [{ name: 'domain', value: 'https://www.contoso-ads.com' }], - certificates: [ - { - name: 'my-certificate', - value: 'https://akv-contoso.vault.azure.net/certificates/MyCertificate/abc1234567890def12345' - } - ], - secrets: [ - { - name: 'my-secret', - value: 'https://akv-contoso.vault.azure.net/secrets/MySecret/abc1234567890def12345' - } - ], - failureCriteria: [ - 'avg(response_time_ms) > 300', - 'percentage(error) > 50', - { GetCustomerDetails: 'avg(latency) >200' } - ], + engineInstances: 1, autoStop: { errorPercentage: 80, timeWindow: 60 }, - keyVaultReferenceIdentity: '/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity', - keyVaultReferenceIdentityType: 'SystemAssigned', - regionalLoadTestConfig: [ - { - region: 'eastus', - engineInstances: 1, - }, - { - region: 'westus', - engineInstances: 1, - } - ], - referenceIdentities: [ - { - kind: "KeyVault", - type: "UserAssigned", - value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity" - }, - { - kind: "Metrics", - type: "UserAssigned", - value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity" - } - ] +} + +export class OverRideParametersModel { + testId: string = ''; + displayName: string = ''; + description: string = ''; + engineInstances: number = 0; + autoStop: { errorPercentage: number, timeWindow: number } = { errorPercentage: 0, timeWindow: 0 }; } export const testmanagerApiVersion = "2024-07-01-preview"; +export const autoStopDisable = "disable"; + namespace BaseAPIRoute { export const featureFlag = "featureFlags"; } @@ -69,3 +55,5 @@ export namespace APIRoute { const latestVersion = "api-version="+testmanagerApiVersion; export const FeatureFlags = (flag: string) => `${BaseAPIRoute.featureFlag}/${flag}?${latestVersion}`; } + +export const OutputVariableName = 'ALTOutputVar'; \ No newline at end of file diff --git a/src/models/util.ts b/src/models/util.ts index 40fdd778..8d1a4e0a 100644 --- a/src/models/util.ts +++ b/src/models/util.ts @@ -1,12 +1,14 @@ import { IHttpClientResponse } from 'typed-rest-client/Interfaces'; const { v4: uuidv4 } = require('uuid'); import { isNull, isNullOrUndefined } from 'util'; -import { defaultYaml } from './constants'; +import { autoStopDisable, DefaultYamlModel, OverRideParametersModel } from './constants'; import * as EngineUtil from './engine/Util'; import { BaseLoadTestFrameworkModel } from './engine/BaseLoadTestFrameworkModel'; import { TestKind } from "./engine/TestKind"; import { PassFailMetric, Statistics, TestRunArtifacts, TestRunModel, TestModel, ManagedIdentityTypeForAPI } from './PayloadModels'; -import { RunTimeParams, ValidAggregateList, ValidConditionList, ManagedIdentityType, PassFailCount, ReferenceIdentityKinds, AllManagedIdentitiesSegregated } from './UtilModels'; +import { RunTimeParams, ValidAggregateList, ValidConditionList, ManagedIdentityType, PassFailCount, ReferenceIdentityKinds, AllManagedIdentitiesSegregated, ValidationModel } from './UtilModels'; +import * as InputConstants from './InputConstants'; +import * as core from '@actions/core'; export function checkFileType(filePath: string, fileExtToValidate: string): boolean{ if(isNullOrUndefined(filePath)){ @@ -266,6 +268,13 @@ function isArrayOfStrings(variable: any): variable is string[] { return Array.isArray(variable) && variable.every((item) => typeof item === 'string'); } +function isInvalidString(variable: any, allowNull : boolean = false): variable is string[] { + if(allowNull){ + return !isNullOrUndefined(variable) && (typeof variable != 'string' || variable == ""); + } + return isNullOrUndefined(variable) || typeof variable != 'string' || variable == ""; +} + function inValidEngineInstances(engines : number) : boolean{ if(engines > 400 || engines < 1){ return true; @@ -273,12 +282,33 @@ function inValidEngineInstances(engines : number) : boolean{ return false; } +export function getResourceTypeFromResourceId(resourceId:string){ + return resourceId && resourceId.split("/").length > 7 ? resourceId.split("/")[6] + "/" + resourceId.split("/")[7] : null +} + +export function getResourceNameFromResourceId(resourceId:string){ + return resourceId && resourceId.split("/").length > 8 ? resourceId.split("/")[8] : null +} + +export function getResourceGroupFromResourceId(resourceId:string){ + return resourceId && resourceId.split("/").length > 4 ? resourceId.split("/")[4] : null +} + +export function getSubscriptionIdFromResourceId(resourceId:string){ + return resourceId && resourceId.split("/").length > 2 ? resourceId.split("/")[2] : null +} + +function isValidGUID(guid: string): boolean { + const guidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + return guidRegex.test(guid); +} + export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : string} { if(!isDictionary(givenYaml)) { return {valid : false,error :`Invalid YAML syntax.`}; } let unSupportedKeys : string[] = []; - let supportedKeys : string[] = Object.keys(defaultYaml); + let supportedKeys : string[] = Object.keys(new DefaultYamlModel()); Object.keys(givenYaml).forEach(element => { if(supportedKeys.indexOf(element) == -1){ unSupportedKeys.push(element); @@ -339,6 +369,7 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s 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(!isNullOrUndefined(givenYaml.referenceIdentities)) { if(!Array.isArray(givenYaml.referenceIdentities)){ return {valid : false, error : `The value "${givenYaml.referenceIdentities.toString()}" for referenceIdentities is invalid. Provide a valid list of reference identities.`}; @@ -383,17 +414,19 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s return {valid : false, error : `The value "${givenYaml.properties.userPropertyFile}" for userPropertyFile is invalid. Provide a valid file path of type ${framework.ClientResources.userPropertyFileExtensionsFriendly}. Refer to the YAML syntax at https://learn.microsoft.com/azure/load-testing/reference-test-config-yaml#properties-configuration.`} } } - if(givenYaml.autoStop){ - if(typeof givenYaml.autoStop != 'string'){ - if(isNullOrUndefined(givenYaml.autoStop.errorPercentage) || isNaN(givenYaml.autoStop.errorPercentage) || givenYaml.autoStop.errorPercentage > 100 || givenYaml.autoStop.errorPercentage < 0) { - return {valid : false, error : `The value "${givenYaml.autoStop.errorPercentage}" for errorPercentage of auto-stop criteria is invalid. The value should be valid decimal number from 0 to 100.`}; - } - if(isNullOrUndefined(givenYaml.autoStop.timeWindow) || isNaN(givenYaml.autoStop.timeWindow) || givenYaml.autoStop.timeWindow <= 0 || !Number.isInteger(givenYaml.autoStop.timeWindow)){ - return {valid : false, error : `The value "${givenYaml.autoStop.timeWindow}" for timeWindow of auto-stop criteria is invalid. The value should be valid integer greater than 0.`}; - } + if(givenYaml.appComponents) { + if(!Array.isArray(givenYaml.appComponents)){ + return {valid : false, error : `The value "${givenYaml.appComponents}" for appComponents is invalid. Provide a valid list of application components.`}; } - else if(givenYaml.autoStop != "disable"){ - return {valid : false, error : 'Invalid value for "autoStop", for disabling auto stop use "autoStop: disable"'}; + let validationAppComponents = validateAppComponentAndServerMetricsConfig(givenYaml.appComponents); + if(validationAppComponents.valid == false){ + return validationAppComponents; + } + } + if(givenYaml.autoStop){ + let validation = validateAutoStop(givenYaml.autoStop); + if(validation.valid == false){ + return validation; } } if(givenYaml.regionalLoadTestConfig){ @@ -423,6 +456,30 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s return {valid : true, error : ""}; } +export function validateAutoStop(autoStop: any, isPipelineParam: boolean = false): ValidationModel { + if(typeof autoStop != 'string'){ + if(isNullOrUndefined(autoStop.errorPercentage) || isNaN(autoStop.errorPercentage) || autoStop.errorPercentage > 100 || autoStop.errorPercentage < 0) { + let errorMessage = isPipelineParam + ? `The value "${autoStop.errorPercentage}" for errorPercentage of auto-stop criteria is invalid in the overrideParameters provided. The value should be valid decimal number from 0 to 100.` + : `The value "${autoStop.errorPercentage}" for errorPercentage of auto-stop criteria is invalid. The value should be valid decimal number from 0 to 100.`; + return {valid : false, error : errorMessage}; + } + if(isNullOrUndefined(autoStop.timeWindow) || isNaN(autoStop.timeWindow) || autoStop.timeWindow <= 0 || !Number.isInteger(autoStop.timeWindow)){ + let errorMessage = isPipelineParam + ? `The value "${autoStop.timeWindow}" for timeWindow of auto-stop criteria is invalid in the overrideParameters provided. The value should be valid integer greater than 0.` + : `The value "${autoStop.timeWindow}" for timeWindow of auto-stop criteria is invalid. The value should be valid integer greater than 0.` + return {valid : false, error : errorMessage}; + } + } + else if(autoStop != autoStopDisable){ + let errorMessage = isPipelineParam + ? 'Invalid value for "autoStop" in the overrideParameters provided, for disabling auto stop use "autoStop: disable"' + : 'Invalid value for "autoStop", for disabling auto stop use "autoStop: disable"' + return {valid : false, error : errorMessage}; + } + return {valid : true, error : ""}; +} + export function validateAndGetSegregatedManagedIdentities(referenceIdentities: {[key: string]: string}[], keyVaultGivenOutOfReferenceIdentities: boolean = false) : AllManagedIdentitiesSegregated { let referenceIdentityValuesUAMIMap: { [key in ReferenceIdentityKinds]: string[] } = { @@ -449,7 +506,7 @@ export function validateAndGetSegregatedManagedIdentities(referenceIdentities: { // key-vault which needs back-compat. if(keyVaultGivenOutOfReferenceIdentities) { if(referenceIdentityValuesUAMIMap[ReferenceIdentityKinds.KeyVault].length > 0 || referenceIdentiesSystemAssignedCount[ReferenceIdentityKinds.KeyVault] > 0) { - throw new Error("KeyVault reference identity should not be provided in the referenceIdentities array if keyVaultReferenceIdentity is provided."); + throw new Error("Two KeyVault references are defined in the YAML config file. Use either the keyVaultReferenceIdentity field or the referenceIdentities section to specify the KeyVault reference identity."); } // this will be assigned above if the given is outside the refIds so no need to assign again. } @@ -473,6 +530,59 @@ export function validateAndGetSegregatedManagedIdentities(referenceIdentities: { return {referenceIdentityValuesUAMIMap, referenceIdentiesSystemAssignedCount}; } +function validateAppComponentAndServerMetricsConfig(appComponents: Array) : ValidationModel { + let appComponentsParsed = appComponents; + for(let i = 0; i < appComponentsParsed.length; i++){ + if(!isDictionary(appComponentsParsed[i])){ + return {valid : false, error : `The value "${appComponentsParsed[i].toString()}" for AppComponents in the index "${i}" is invalid. Provide a valid dictionary.`}; + } + let resourceId = appComponentsParsed[i].resourceId; + if(isInvalidString(resourceId)){ + return {valid : false, error : `The value "${appComponentsParsed[i].resourceId}" for resourceId in appComponents is invalid. Provide a valid resourceId.`}; + } + resourceId = resourceId.toLowerCase(); + let subscriptionId = getSubscriptionIdFromResourceId(resourceId); + let resourceType = getResourceTypeFromResourceId(resourceId); + let name = getResourceNameFromResourceId(resourceId); + let resourceGroup = getResourceGroupFromResourceId(resourceId); + if(isNullOrUndefined(resourceGroup) || isNullOrUndefined(subscriptionId) + || isNullOrUndefined(resourceType) || isNullOrUndefined(name) + || !isValidGUID(subscriptionId)){ + return {valid : false, error : `The value "${resourceId}" for resourceId in appComponents is invalid. Provide a valid resourceId.`}; + } + if(isInvalidString(appComponentsParsed[i].kind, true)){ + return {valid : false, error : `The value "${appComponentsParsed[i].kind?.toString()}" for kind in appComponents is invalid. Provide a valid string.`}; + } + if(isInvalidString(appComponentsParsed[i].resourceName, true)){ + return {valid : false, error : `The value "${appComponentsParsed[i].resourceName?.toString()}" for resourceName in appComponents is invalid. Provide a valid string.`}; + } + let resourceName = appComponentsParsed[i].resourceName || name; + if(!isNullOrUndefined(appComponentsParsed[i].metrics)) { + let metrics = appComponentsParsed[i].metrics; + if(!Array.isArray(metrics)){ + return {valid : false, error : `The value "${metrics?.toString()}" for metrics in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid list of metrics.`}; + } + for(let metric of metrics){ + if(!isDictionary(metric)){ + return {valid : false, error : `The value "${metric?.toString()}" for metrics in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid dictionary.`}; + } + if(metric && isInvalidString(metric.name)){ + return {valid : false, error : `The value "${metric.name?.toString()}" for name in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid string.`}; + } + if(isInvalidString(metric.aggregation)){ + return {valid : false, error : `The value "${metric.aggregation?.toString()}" for aggregation in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid string.`}; + } + if(isInvalidString(metric.namespace, true)){ + return {valid : false, error : `The value "${metric.namespace?.toString()}" for namespace in the appComponent with resourceName "${resourceName}" is invalid. Provide a valid string.`}; + } + } + } else { + console.log(`Metrics not provided for the appComponent "${resourceName}", default metrics will be enabled for the same.`); + } + } + return {valid : true, error : ""}; +} + function validateReferenceIdentities(referenceIdentities: Array) : {valid : boolean, error : string} { for(let referenceIdentity of referenceIdentities){ if(!isDictionary(referenceIdentity)){ @@ -503,6 +613,68 @@ function validateReferenceIdentities(referenceIdentities: Array) : {valid : return {valid : true, error : ""}; } +export function validateOverRideParameters(overRideParams: string | undefined): ValidationModel { + try { + if(!isNullOrUndefined(overRideParams)) { + let overRideParamsObj : any; + try{ + overRideParamsObj = JSON.parse(overRideParams); + } + catch(error) { + return { valid: false, error:`Invalid format provided in the ${InputConstants.overRideParametersLabel} field in pipeline, provide a valid json string.` }; + }; + let unSupportedKeys : string[] = []; + let supportedKeys : string[] = Object.keys(new OverRideParametersModel()); + Object.keys(overRideParamsObj).forEach(element => { + if(supportedKeys.indexOf(element) == -1){ + unSupportedKeys.push(element); + } + }); + if(unSupportedKeys.length) { + const result = unSupportedKeys.map(element => `${element}`).join(", "); + return {valid : false, error : `The ${InputConstants.overRideParametersLabel} provided has unsupported field(s) "${result}".`}; + } + if(overRideParamsObj.testId != undefined) { + if(typeof overRideParamsObj.testId != 'string') { + return {valid : false, error : `The testId provided in the overrideParameters is not a string.`}; + } + } + if(overRideParamsObj.displayName != undefined) { + if(typeof overRideParamsObj.displayName != 'string') { + return {valid : false, error : `The displayName provided in the overrideParameters is not a string.`}; + } + } + if(overRideParamsObj.description != undefined) { + if(typeof overRideParamsObj.description != 'string') { + return {valid : false, error : `The description provided in the overrideParameters is not a string.`}; + } + } + if(overRideParamsObj.engineInstances != undefined) { + if(typeof overRideParamsObj.engineInstances != 'number') { + return {valid : false, error : `The engineInstances provided in the overrideParameters is not a number.`}; + } + } + if(!isNullOrUndefined(overRideParamsObj.autoStop)) { + let validation = validateAutoStop(overRideParamsObj.autoStop, true); + if(validation.valid == false){ + return validation; + } + } + } + } + catch (error) { + return {valid: false, error: (error ?? '').toString()}; + } + return {valid : true, error : ""}; +} + +export function validateOutputParametervariableName(outputVarName: string): ValidationModel { + if(isNullOrUndefined(outputVarName) || typeof outputVarName != 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(outputVarName)){ + return { valid: false, error: `Invalid output variable name '${outputVarName}'. Use only letters, numbers, and underscores.`}; + } + return {valid : true, error : ""}; +} + /* ado takes the full pf criteria as a string after parsing the string into proper data model, */ @@ -611,7 +783,8 @@ export function getDefaultTestRunName() export function getDefaultRunDescription() { - return "Started using GitHub Actions" + const pipelineName = process.env.GITHUB_WORKFLOW || "Unknown Pipeline"; + return "Started using GH workflows" + (pipelineName ? "-" + pipelineName : ""); } export function validateTestRunParamsFromPipeline(runTimeParams: RunTimeParams){ diff --git a/src/postProcessJob.ts b/src/postProcessJob.ts new file mode 100644 index 00000000..496496e0 --- /dev/null +++ b/src/postProcessJob.ts @@ -0,0 +1,28 @@ +import { PostTaskParameters } from "./models/UtilModels"; +import * as core from '@actions/core'; +import { AuthenticationUtils } from "./models/AuthenticationUtils"; +import { YamlConfig } from "./models/TaskModels"; +import { APISupport } from "./models/APISupport"; +import { isNullOrUndefined } from "util"; + +async function run() { + try { + + const runId = process.env[PostTaskParameters.runId]; + const baseUri = process.env[PostTaskParameters.baseUri]; + const isRunCompleted = process.env[PostTaskParameters.isRunCompleted]; + console.log(runId, baseUri, isRunCompleted); + if(!isNullOrUndefined(runId) && !isNullOrUndefined(baseUri) && (isNullOrUndefined(isRunCompleted) || isRunCompleted != 'true')) { + const yamlConfig = new YamlConfig(true); + const authContext = new AuthenticationUtils(); + const apiSupport = new APISupport(authContext, yamlConfig); + await apiSupport.stopTestRunPostProcess(baseUri, runId); + } + } + + catch(err : any) { + core.debug("Failed to stop the test run:" + err.message); + } +} + +run(); \ No newline at end of file diff --git a/src/services/FeatureFlagService.ts b/src/services/FeatureFlagService.ts index b9fd047b..d109a605 100644 --- a/src/services/FeatureFlagService.ts +++ b/src/services/FeatureFlagService.ts @@ -3,7 +3,7 @@ import { Definitions } from "../models/APIResponseModel"; import { APIRoute } from "../models/constants"; import * as util from '../models/util'; import { AuthenticationUtils } from "../models/AuthenticationUtils"; -import { CallTypeForDP } from "../models/UtilModels"; +import { FetchCallType } from "../models/UtilModels"; import * as FetchUtil from './../models/FetchHelper'; export class FeatureFlagService { @@ -20,8 +20,9 @@ export class FeatureFlagService { } let uri: string = baseUrl + APIRoute.FeatureFlags(flag.toString()); - let headers = this.authContext.getDataPlaneHeader(CallTypeForDP.get); - let flagResponse = await FetchUtil.httpClientRetries(uri, headers, 'get', 3, "", false, false); + let headers = this.authContext.getDataPlaneHeader(FetchCallType.get); + let flagResponse = await FetchUtil.httpClientRetries(uri, headers, FetchCallType.get, 3, "", false, false); + try { let flagObj = (await util.getResultObj(flagResponse)) as Definitions["FeatureFlagResponse"]; this.featureFlagCache[flag.toString()] = flagObj.enabled; diff --git a/test/Utils/AppComponentsAndServerConfigYamls.ts b/test/Utils/AppComponentsAndServerConfigYamls.ts new file mode 100644 index 00000000..d45b2e81 --- /dev/null +++ b/test/Utils/AppComponentsAndServerConfigYamls.ts @@ -0,0 +1,307 @@ +export const appComponentsWithMetrics : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: "app", + metrics: [ + { + name: "CpuPercentage", + aggregation: "Average" + }, + { + name: "MemoryPercentage", + aggregation: "Average", + namespace: "Microsoft.Web/serverfarms" + } + ] + }, + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: "app, functionapp", + metrics: [ + { + name: "CpuPercentage", + aggregation: "Average" + }, + { + name: "MemoryPercentage", + aggregation: "Average", + } + ] + }, + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web/xyz", + resourceName: "xyz", + metrics: [ + { + name: "CpuPercentage", + aggregation: "Average" + } + ] + } + ] +} + +export const appComponentsWithoutMetricsAndKind : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + } + ] +} + +// invalid starts +export const appCompsInvalidResourceId : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms", + } + ] +} + +export const appCompsInvalidKind : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: ["test", "test2"] + } + ] +} + +export const appCompsInvalidResourceName : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: "app", + resourceName: ["test", "test2"] + } + ] +} + +export const appCompsInvalidMetricsArray : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: "app", + resourceName: "test", + metrics: "dummy" + } + ] +} + +export const appCompsInvalidMetricDict : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: "app", + resourceName: "test", + metrics: [ + "hi,123" + ] + } + ] +} + +export const appCompsInvalidMetricName : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: "app", + resourceName: "test", + metrics: [ + { + name: [123], + aggregation: "Average" + } + ] + } + ] +} + +export const appCompsInvalidMetricAggregation : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: "app", + resourceName: "test", + metrics: [ + { + name: "123", + aggregation: ["Average", "Min"] + } + ] + } + ] +} + +export const appCompsInvalidMetricNameSpace : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: "app", + resourceName: "test", + metrics: [ + { + name: "123", + aggregation: "Average, min", + namespace: ["dummy", "dummy2"] + } + ] + } + ] +} + +export const appCompsInvalidAppComponentDictionary : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web", + kind: "app", + resourceName: "test", + metrics: [ + { + name: "123", + aggregation: "Average, min", + namespace: "dummy" + } + ] + }, + "hi,123" + ] +} + +export const appCompsInvalidResourceIdString : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + description: 'Load test website home page', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + appComponents: [ + { + resourceId: ["/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web"], + kind: "app", + resourceName: "test", + metrics: [ + { + name: "123", + aggregation: "Average, min", + namespace: "dummy" + } + ] + }, + ] +} diff --git a/test/Utils/checkForValidationOfYaml.test.ts b/test/Utils/checkForValidationOfYaml.test.ts index 43736ed4..8292e9cc 100644 --- a/test/Utils/checkForValidationOfYaml.test.ts +++ b/test/Utils/checkForValidationOfYaml.test.ts @@ -1,6 +1,7 @@ import { checkValidityYaml, getAllFileErrors } from '../../src/models/util' import * as constants from './testYamls'; import * as referenceIdentityConstants from './ReferenceIdentityYamls'; +import * as appCompsConstants from './AppComponentsAndServerConfigYamls'; describe('invalid Yaml tests', () =>{ describe('basic scenarios for invalid cases', ()=>{ @@ -205,7 +206,7 @@ describe('reference identity validations', () => { }); test('KeyVault inside and outside', () => { - expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesGivenInKeyVaultOutsideAndInside)).toStrictEqual({valid : false, error : 'KeyVault reference identity should not be provided in the referenceIdentities array if keyVaultReferenceIdentity is provided.'}); + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesGivenInKeyVaultOutsideAndInside)).toStrictEqual({valid : false, error : 'Two KeyVault references are defined in the YAML config file. Use either the keyVaultReferenceIdentity field or the referenceIdentities section to specify the KeyVault reference identity.'}); }); test('reference identities is not an array', () => { @@ -241,6 +242,47 @@ describe('reference identity validations', () => { expect(checkValidityYaml(referenceIdentityConstants.referenceIdentityTypewithInvalidStringInKVID)).toStrictEqual({valid : false, error : `The value "UserAssigned,SystemAssigned" for type in referenceIdentities is invalid. Allowed values are "SystemAssigned" and "UserAssigned".`}); }); }); +describe('app components and server config tests', () => { + test('app components with metrics', () => { + expect(checkValidityYaml(appCompsConstants.appComponentsWithMetrics)).toStrictEqual({valid : true, error : ''}); + }); + test('without metrics and kind', () => { + expect(checkValidityYaml(appCompsConstants.appComponentsWithoutMetricsAndKind)).toStrictEqual({valid : true, error : ''}); + }); + + // invalid starts + test('invalid resource id as string', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidResourceIdString)).toStrictEqual({valid : false, error : 'The value "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sample-web" for resourceId in appComponents is invalid. Provide a valid resourceId.'}); + }); + // above one returns as it is string and this retuns the lowercase. + test('invalid resource id', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidResourceId)).toStrictEqual({valid : false, error : 'The value "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourcegroups/sample-rg/providers/microsoft.web/serverfarms" for resourceId in appComponents is invalid. Provide a valid resourceId.'}); + }); + test('invalid kind', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidKind)).toStrictEqual({valid : false, error : 'The value "test,test2" for kind in appComponents is invalid. Provide a valid string.'}); + }); + test('invalid resource name', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidResourceName)).toStrictEqual({valid : false, error : 'The value "test,test2" for resourceName in appComponents is invalid. Provide a valid string.'}); + }); + test('invalid metrics array', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidMetricsArray)).toStrictEqual({valid : false, error : 'The value "dummy" for metrics in the appComponent with resourceName "test" is invalid. Provide a valid list of metrics.'}); + }); + test('invalid metrics dictionary', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidMetricDict)).toStrictEqual({valid : false, error : 'The value "hi,123" for metrics in the appComponent with resourceName "test" is invalid. Provide a valid dictionary.'}); + }); + test('invalid metric name', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidMetricName)).toStrictEqual({valid : false, error : 'The value "123" for name in the appComponent with resourceName "test" is invalid. Provide a valid string.'}); + }); + test('invalid metric aggregation', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidMetricAggregation)).toStrictEqual({valid : false, error : 'The value "Average,Min" for aggregation in the appComponent with resourceName "test" is invalid. Provide a valid string.'}); + }); + test('invalid metric namepspace', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidMetricNameSpace)).toStrictEqual({valid : false, error : 'The value "dummy,dummy2" for namespace in the appComponent with resourceName "test" is invalid. Provide a valid string.'}); + }); + test('invalid app component dictionary', () => { + expect(checkValidityYaml(appCompsConstants.appCompsInvalidAppComponentDictionary)).toStrictEqual({valid : false, error : 'The value "hi,123" for AppComponents in the index "1" is invalid. Provide a valid dictionary.'}); + }); +}); describe('file errors', () => { test('Test object with no file validation errors', () => { // https://learn.microsoft.com/en-us/rest/api/loadtesting/dataplane/load-test-administration/get-test?view=rest-loadtesting-dataplane-2022-11-01&tabs=HTTP