From be811c189199165c6e8b3150c31b4bdca2f8f830 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 00:56:24 +0530 Subject: [PATCH 01/29] phase1 changes for cicd improvements. --- lib/main.js | 24 +- lib/models/APISupport.js | 36 +- lib/models/AuthenticationUtils.js | 26 +- lib/models/FetchHelper.js | 31 +- lib/models/FileUtils.js | 35 +- lib/models/PayloadModels.js | 8 +- lib/models/TaskModels.js | 66 +- lib/models/UtilModels.js | 20 +- lib/models/constants.js | 14 +- lib/models/engine/TestKind.js | 2 +- lib/models/engine/Util.js | 18 +- lib/models/util.js | 186 +++-- lib/services/FeatureFlagService.js | 32 +- lib/services/FeatureFlags.js | 2 +- src/models/APISupport.ts | 7 +- src/models/PayloadModels.ts | 10 +- src/models/TaskModels.ts | 52 +- src/models/UtilModels.ts | 11 + src/models/constants.ts | 12 + src/models/util.ts | 111 ++- test/Utils/ReferenceIdentityYamls.ts | 757 ++++++++++++++++++++ test/Utils/checkForValidationOfYaml.test.ts | 93 ++- 22 files changed, 1322 insertions(+), 231 deletions(-) create mode 100644 test/Utils/ReferenceIdentityYamls.ts diff --git a/lib/main.js b/lib/main.js index 83e69edc..60610ed3 100644 --- a/lib/main.js +++ b/lib/main.js @@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); +var __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) { diff --git a/lib/models/APISupport.js b/lib/models/APISupport.js index 0c3b235f..306e3901 100644 --- a/lib/models/APISupport.js +++ b/lib/models/APISupport.js @@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); +var __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) { @@ -80,9 +70,9 @@ class APISupport { this.baseURL = 'https://' + dataPlaneUrl + '/'; }); } - getTestAPI(validate_1) { - return __awaiter(this, arguments, void 0, function* (validate, returnTestObj = false) { - var _a, _b, _c, _d; + 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.tm2024Version; urlSuffix = this.baseURL + urlSuffix; let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.get); @@ -334,11 +324,11 @@ class APISupport { } createTestRun() { return __awaiter(this, void 0, void 0, function* () { - const testRunId = Util.getUniqueId(); - let urlSuffix = "test-runs/" + testRunId + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; - urlSuffix = this.baseURL + urlSuffix; try { var startData = this.yamlModel.getStartTestData(); + const testRunId = this.yamlModel.runTimeParams.testRunId; + let urlSuffix = "test-runs/" + testRunId + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL + urlSuffix; console.log("Creating and running a testRun for the test"); let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.patch); let startTestresult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'patch', 3, JSON.stringify(startData)); diff --git a/lib/models/AuthenticationUtils.js b/lib/models/AuthenticationUtils.js index e9a04ace..99b98bbc 100644 --- a/lib/models/AuthenticationUtils.js +++ b/lib/models/AuthenticationUtils.js @@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); +var __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) { @@ -147,8 +137,8 @@ class AuthenticationUtils { } } getDataPlaneHeader(apicallType) { + var _a; return __awaiter(this, void 0, void 0, function* () { - var _a; if (!this.isValid(UtilModels_1.TokenScope.Dataplane)) { let tokenRes = yield this.getTokenAPI(UtilModels_1.TokenScope.Dataplane); this.dataPlanetoken = tokenRes; diff --git a/lib/models/FetchHelper.js b/lib/models/FetchHelper.js index 6fba7639..bee2c205 100644 --- a/lib/models/FetchHelper.js +++ b/lib/models/FetchHelper.js @@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); +var __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) { @@ -42,7 +32,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.httpClientRetries = httpClientRetries; +exports.httpClientRetries = void 0; const util_1 = require("./util"); const UtilModels_1 = require("./UtilModels"); const httpc = __importStar(require("typed-rest-client/HttpClient")); @@ -50,8 +40,8 @@ const FileUtils_1 = require("./FileUtils"); const httpClient = new httpc.HttpClient('MALT-GHACTION'); const core = __importStar(require("@actions/core")); // (note mohit): shift to the enum later. -function httpClientRetries(urlSuffix_1, header_1, method_1) { - return __awaiter(this, arguments, void 0, function* (urlSuffix, header, method, retries = 1, data, isUploadCall = true, log = true) { +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)()}`; @@ -93,3 +83,4 @@ function httpClientRetries(urlSuffix_1, header_1, method_1) { } }); } +exports.httpClientRetries = httpClientRetries; diff --git a/lib/models/FileUtils.js b/lib/models/FileUtils.js index c5d77c4e..5b7c65a2 100644 --- a/lib/models/FileUtils.js +++ b/lib/models/FileUtils.js @@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); +var __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) { @@ -45,15 +35,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.uploadFileToResultsFolder = uploadFileToResultsFolder; -exports.deleteFile = deleteFile; -exports.uploadFileData = uploadFileData; +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_1) { - return __awaiter(this, arguments, void 0, function* (response, fileName = UtilModels_1.resultZipFileName) { +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); @@ -76,6 +64,7 @@ function uploadFileToResultsFolder(response_1) { } }); } +exports.uploadFileToResultsFolder = uploadFileToResultsFolder; function deleteFile(foldername) { if (fs.existsSync(foldername)) { fs.readdirSync(foldername).forEach((file, index) => { @@ -90,6 +79,7 @@ function deleteFile(foldername) { fs.rmdirSync(foldername); } } +exports.deleteFile = deleteFile; function uploadFileData(filepath) { try { let filedata = fs.readFileSync(filepath); @@ -104,3 +94,4 @@ function uploadFileData(filepath) { throw new Error(err.message); } } +exports.uploadFileData = uploadFileData; diff --git a/lib/models/PayloadModels.js b/lib/models/PayloadModels.js index b75facdd..905c2aea 100644 --- a/lib/models/PayloadModels.js +++ b/lib/models/PayloadModels.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.CertificateMetadata = void 0; +exports.ManagedIdentityTypeForAPI = exports.CertificateMetadata = void 0; class CertificateMetadata { } exports.CertificateMetadata = CertificateMetadata; @@ -13,3 +13,9 @@ 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 index fb729106..03f61cc6 100644 --- a/lib/models/TaskModels.js +++ b/lib/models/TaskModels.js @@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); +var __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"); @@ -41,11 +31,12 @@ 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")); class YamlConfig { constructor() { - var _a, _b, _c, _d, _e; + var _a, _b, _c, _d, _e, _f; this.testId = ''; this.displayName = ''; this.description = ''; @@ -62,9 +53,13 @@ class YamlConfig { this.secrets = {}; this.failureCriteria = {}; // this is yaml model. this.passFailApiModel = {}; // this is api model. - this.autoStop = null; + this.keyVaultReferenceIdentityType = PayloadModels_1.ManagedIdentityTypeForAPI.SystemAssigned; + this.metricsReferenceIdentityType = PayloadModels_1.ManagedIdentityTypeForAPI.SystemAssigned; + this.engineReferenceIdentityType = PayloadModels_1.ManagedIdentityTypeForAPI.None; this.keyVaultReferenceIdentity = null; - this.keyVaultReferenceIdentityType = UtilModels_1.ManagedIdentityType.SystemAssigned; + this.metricsReferenceIdentity = null; + this.engineReferenceIdentities = null; + this.autoStop = null; this.regionalLoadTestConfig = null; this.runTimeParams = { env: {}, secrets: {}, runDisplayName: '', runDescription: '', testId: '', testRunId: '' }; let yamlFile = (_a = core.getInput('loadTestConfigFile')) !== null && _a !== void 0 ? _a : ''; @@ -138,7 +133,6 @@ class YamlConfig { } } if (config.secrets != undefined) { - this.keyVaultReferenceIdentityType = UtilModels_1.ManagedIdentityType.SystemAssigned; this.secrets = this.parseParameters(config.secrets, UtilModels_1.ParamType.secrets); } if (config.env != undefined) { @@ -147,9 +141,12 @@ class YamlConfig { if (config.certificates != undefined) { this.certificates = this.parseParameters(config.certificates, UtilModels_1.ParamType.cert); } - if (config.keyVaultReferenceIdentity != undefined) { - this.keyVaultReferenceIdentityType = UtilModels_1.ManagedIdentityType.UserAssigned; - this.keyVaultReferenceIdentity = config.keyVaultReferenceIdentity; + if (config.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); @@ -167,6 +164,23 @@ class YamlConfig { this.runTimeParams = this.getRunTimeParams(); Util.validateTestRunParamsFromPipeline(this.runTimeParams); } + 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; + } + } getRunTimeParams() { var _a, _b; var secretRun = core.getInput('secrets'); @@ -270,6 +284,10 @@ class YamlConfig { publicIPDisabled: this.publicIPDisabled, keyvaultReferenceIdentityType: this.keyVaultReferenceIdentityType, keyvaultReferenceIdentityId: this.keyVaultReferenceIdentity, + engineBuiltinIdentityIds: this.engineReferenceIdentities, + engineBuiltinIdentityType: this.engineReferenceIdentityType, + metricsReferenceIdentityType: this.metricsReferenceIdentityType, + metricsReferenceIdentityId: this.metricsReferenceIdentity }; return data; } diff --git a/lib/models/UtilModels.js b/lib/models/UtilModels.js index 596e4b53..de0dd104 100644 --- a/lib/models/UtilModels.js +++ b/lib/models/UtilModels.js @@ -1,24 +1,30 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ManagedIdentityType = exports.ValidConditionList = exports.ValidAggregateList = exports.ApiVersionConstants = exports.correlationHeader = exports.resultZipFileName = exports.reportZipFileName = exports.resultFolder = exports.FileType = exports.ContentTypeMap = exports.CallTypeForDP = exports.TokenScope = exports.ParamType = void 0; +exports.ManagedIdentityType = exports.ValidConditionList = exports.ValidAggregateList = exports.ApiVersionConstants = exports.correlationHeader = exports.resultZipFileName = exports.reportZipFileName = exports.resultFolder = exports.FileType = exports.ContentTypeMap = exports.CallTypeForDP = exports.TokenScope = exports.ReferenceIdentityKinds = exports.ParamType = void 0; var ParamType; (function (ParamType) { ParamType["env"] = "env"; ParamType["secrets"] = "secrets"; ParamType["cert"] = "cert"; -})(ParamType || (exports.ParamType = ParamType = {})); +})(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 = TokenScope = {})); +})(TokenScope = exports.TokenScope || (exports.TokenScope = {})); var CallTypeForDP; (function (CallTypeForDP) { CallTypeForDP[CallTypeForDP["get"] = 0] = "get"; CallTypeForDP[CallTypeForDP["patch"] = 1] = "patch"; CallTypeForDP[CallTypeForDP["put"] = 2] = "put"; CallTypeForDP[CallTypeForDP["delete"] = 3] = "delete"; -})(CallTypeForDP || (exports.CallTypeForDP = CallTypeForDP = {})); +})(CallTypeForDP = exports.CallTypeForDP || (exports.CallTypeForDP = {})); exports.ContentTypeMap = { [CallTypeForDP.get]: null, [CallTypeForDP.patch]: 'application/merge-patch+json', @@ -33,7 +39,7 @@ var FileType; FileType["ZIPPED_ARTIFACTS"] = "ZIPPED_ARTIFACTS"; FileType["URL_TEST_CONFIG"] = "URL_TEST_CONFIG"; FileType["TEST_SCRIPT"] = "TEST_SCRIPT"; -})(FileType || (exports.FileType = FileType = {})); +})(FileType = exports.FileType || (exports.FileType = {})); exports.resultFolder = 'loadTest'; exports.reportZipFileName = 'report.zip'; exports.resultZipFileName = 'results.zip'; @@ -44,7 +50,7 @@ var ApiVersionConstants; ApiVersionConstants.tm2023Version = '2023-04-01-preview'; ApiVersionConstants.tm2022Version = '2022-11-01'; ApiVersionConstants.cp2022Version = '2022-12-01'; -})(ApiVersionConstants || (exports.ApiVersionConstants = ApiVersionConstants = {})); +})(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'], @@ -63,4 +69,4 @@ var ManagedIdentityType; (function (ManagedIdentityType) { ManagedIdentityType["SystemAssigned"] = "SystemAssigned"; ManagedIdentityType["UserAssigned"] = "UserAssigned"; -})(ManagedIdentityType || (exports.ManagedIdentityType = ManagedIdentityType = {})); +})(ManagedIdentityType = exports.ManagedIdentityType || (exports.ManagedIdentityType = {})); diff --git a/lib/models/constants.js b/lib/models/constants.js index 96f9f039..d6327bf6 100644 --- a/lib/models/constants.js +++ b/lib/models/constants.js @@ -46,6 +46,18 @@ exports.defaultYaml = { 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" + } ] }; exports.testmanagerApiVersion = "2024-07-01-preview"; @@ -57,4 +69,4 @@ var APIRoute; (function (APIRoute) { const latestVersion = "api-version=" + exports.testmanagerApiVersion; APIRoute.FeatureFlags = (flag) => `${BaseAPIRoute.featureFlag}/${flag}?${latestVersion}`; -})(APIRoute || (exports.APIRoute = APIRoute = {})); +})(APIRoute = exports.APIRoute || (exports.APIRoute = {})); diff --git a/lib/models/engine/TestKind.js b/lib/models/engine/TestKind.js index c934c083..0b773c6b 100644 --- a/lib/models/engine/TestKind.js +++ b/lib/models/engine/TestKind.js @@ -9,4 +9,4 @@ var TestKind; TestKind["URL"] = "URL"; TestKind["JMX"] = "JMX"; TestKind["Locust"] = "Locust"; -})(TestKind || (exports.TestKind = TestKind = {})); +})(TestKind = exports.TestKind || (exports.TestKind = {})); diff --git a/lib/models/engine/Util.js b/lib/models/engine/Util.js index b852fa0e..4a9cd9bb 100644 --- a/lib/models/engine/Util.js +++ b/lib/models/engine/Util.js @@ -1,12 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Resources = exports.LoadTestFramework = void 0; -exports.getOrderedLoadTestFrameworks = getOrderedLoadTestFrameworks; -exports.getLoadTestFrameworkModel = getLoadTestFrameworkModel; -exports.getLoadTestFrameworkDisplayName = getLoadTestFrameworkDisplayName; -exports.isTestKindConvertibleToJMX = isTestKindConvertibleToJMX; -exports.getLoadTestFrameworkFromKind = getLoadTestFrameworkFromKind; -exports.getLoadTestFrameworkModelFromKind = getLoadTestFrameworkModelFromKind; +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"); @@ -19,7 +13,7 @@ var LoadTestFramework; (function (LoadTestFramework) { LoadTestFramework["JMeter"] = "JMeter"; LoadTestFramework["Locust"] = "Locust"; -})(LoadTestFramework || (exports.LoadTestFramework = LoadTestFramework = {})); +})(LoadTestFramework = exports.LoadTestFramework || (exports.LoadTestFramework = {})); /** * Retrieves an array of load test frameworks in a specific order. * @returns An array of load test frameworks. @@ -27,6 +21,7 @@ var LoadTestFramework; function getOrderedLoadTestFrameworks() { return [LoadTestFramework.JMeter, LoadTestFramework.Locust]; } +exports.getOrderedLoadTestFrameworks = getOrderedLoadTestFrameworks; /** * Returns the corresponding LoadTestFrameworkModel based on the provided LoadTestFramework enum. * If the provided framework is not recognized, it assumes JMeter by default. @@ -44,6 +39,7 @@ function getLoadTestFrameworkModel(framework) { return _jmeterFramework; } } +exports.getLoadTestFrameworkModel = getLoadTestFrameworkModel; /** * Retrieves the display name of a load test framework. * @param framework The load test framework. @@ -52,6 +48,7 @@ function getLoadTestFrameworkModel(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. @@ -65,6 +62,7 @@ function isTestKindConvertibleToJMX(kind) { } return kind === TestKind_1.TestKind.URL; } +exports.isTestKindConvertibleToJMX = isTestKindConvertibleToJMX; /** * Retrieves the load test framework from a given test kind. * If no kind is provided, it assumes JMX by default. @@ -85,6 +83,7 @@ function getLoadTestFrameworkFromKind(kind) { return LoadTestFramework.JMeter; } } +exports.getLoadTestFrameworkFromKind = getLoadTestFrameworkFromKind; /** * Retrieves the load test framework model from a given test kind. * If no kind is provided, it assumes JMX by default. @@ -94,10 +93,11 @@ function getLoadTestFrameworkFromKind(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 = Resources = {})); +})(Resources = exports.Resources || (exports.Resources = {})); diff --git a/lib/models/util.js b/lib/models/util.js index f0807531..e5573d0e 100644 --- a/lib/models/util.js +++ b/lib/models/util.js @@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); +var __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) { @@ -42,34 +32,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.checkFileType = checkFileType; -exports.checkFileTypes = checkFileTypes; -exports.printTestDuration = printTestDuration; -exports.printCriteria = printCriteria; -exports.ErrorCorrection = ErrorCorrection; -exports.printClientMetrics = printClientMetrics; -exports.sleep = sleep; -exports.getUniqueId = getUniqueId; -exports.getResultFolder = getResultFolder; -exports.getReportFolder = getReportFolder; -exports.indexOfFirstDigit = indexOfFirstDigit; -exports.removeUnits = removeUnits; -exports.isTerminalTestStatus = isTerminalTestStatus; -exports.isStatusFailed = isStatusFailed; -exports.validCriteria = validCriteria; -exports.getResultObj = getResultObj; -exports.invalidDisplayName = invalidDisplayName; -exports.invalidDescription = invalidDescription; -exports.checkValidityYaml = checkValidityYaml; -exports.getPassFailCriteriaFromString = getPassFailCriteriaFromString; -exports.ValidateCriteriaAndConvertToWorkingStringModel = ValidateCriteriaAndConvertToWorkingStringModel; -exports.validateUrl = validateUrl; -exports.validateUrlcert = validateUrlcert; -exports.getDefaultTestName = getDefaultTestName; -exports.getDefaultTestRunName = getDefaultTestRunName; -exports.getDefaultRunDescription = getDefaultRunDescription; -exports.validateTestRunParamsFromPipeline = validateTestRunParamsFromPipeline; -exports.getAllFileErrors = getAllFileErrors; +exports.getAllFileErrors = exports.validateTestRunParamsFromPipeline = exports.getDefaultRunDescription = exports.getDefaultTestRunName = exports.getDefaultTestName = exports.validateUrlcert = exports.validateUrl = exports.ValidateCriteriaAndConvertToWorkingStringModel = exports.getPassFailCriteriaFromString = exports.validateAndGetSegregatedManagedIdentities = exports.checkValidityYaml = 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"); @@ -83,6 +46,7 @@ function checkFileType(filePath, fileExtToValidate) { let split = filePath.split('.'); return split[split.length - 1].toLowerCase() == fileExtToValidate.toLowerCase(); } +exports.checkFileType = checkFileType; function checkFileTypes(filePath, fileExtsToValidate) { var _a; if ((0, util_1.isNullOrUndefined)(filePath)) { @@ -92,9 +56,10 @@ function checkFileTypes(filePath, fileExtsToValidate) { let fileExtsToValidateLower = fileExtsToValidate.map(ext => ext.toLowerCase()); return fileExtsToValidateLower.includes((_a = split[split.length - 1]) === null || _a === void 0 ? void 0 : _a.toLowerCase()); } +exports.checkFileTypes = checkFileTypes; function printTestDuration(testRunObj) { + var _a, _b; return __awaiter(this, void 0, void 0, function* () { - var _a, _b; console.log("Summary generation completed\n"); console.log("-------------------Summary ---------------"); console.log("TestRun start time: " + new Date((_a = testRunObj.startDateTime) !== null && _a !== void 0 ? _a : new Date())); @@ -104,6 +69,7 @@ function printTestDuration(testRunObj) { return; }); } +exports.printTestDuration = printTestDuration; function printCriteria(criteria) { if (Object.keys(criteria).length == 0) return; @@ -132,9 +98,11 @@ function printCriteria(criteria) { } console.log("\n"); } +exports.printCriteria = printCriteria; function ErrorCorrection(result) { return "Unable to fetch the response. Please re-run or contact support if the issue persists. " + "Status code :" + result.message.statusCode; } +exports.ErrorCorrection = ErrorCorrection; function printTestResult(criteria) { var _a, _b; let pass = 0; @@ -173,6 +141,7 @@ function printClientMetrics(obj) { } }); } +exports.printClientMetrics = printClientMetrics; function getAbsVal(data) { if ((0, util_1.isNullOrUndefined)(data)) { return "undefined"; @@ -186,45 +155,53 @@ function sleep(ms) { 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": @@ -241,6 +218,7 @@ function validCriteria(data) { 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"); @@ -275,6 +253,7 @@ function getResultObj(data) { } }); } +exports.getResultObj = getResultObj; function isDictionary(variable) { return typeof variable === 'object' && variable !== null && !Array.isArray(variable); } @@ -289,19 +268,24 @@ function invalidDisplayName(value) { 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 isInValidKVId(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); } @@ -375,12 +359,32 @@ function checkValidityYaml(givenYaml) { 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' || isInValidKVId(givenYaml.keyVaultReferenceIdentity))) { + 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.` }; } @@ -444,6 +448,83 @@ function checkValidityYaml(givenYaml) { } return { valid: true, error: "" }; } +exports.checkValidityYaml = checkValidityYaml; +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("KeyVault reference identity should not be provided in the referenceIdentities array if keyVaultReferenceIdentity is provided."); + } + // 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 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: "" }; +} /* ado takes the full pf criteria as a string after parsing the string into proper data model, */ @@ -494,6 +575,7 @@ function getPassFailCriteriaFromString(passFailCriteria) { }); 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 @@ -520,35 +602,42 @@ function ValidateCriteriaAndConvertToWorkingStringModel(data, failureCriteriaVal 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() { return "Started using GitHub Actions"; } +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 = []; @@ -572,3 +661,4 @@ function getAllFileErrors(testObj) { } return fileErrors; } +exports.getAllFileErrors = getAllFileErrors; diff --git a/lib/services/FeatureFlagService.js b/lib/services/FeatureFlagService.js index 259d3e84..8c7592b1 100644 --- a/lib/services/FeatureFlagService.js +++ b/lib/services/FeatureFlagService.js @@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( }) : function(o, v) { o["default"] = v; }); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); +var __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) { @@ -52,8 +42,8 @@ class FeatureFlagService { this.featureFlagCache = {}; this.authContext = authContext; } - getFeatureFlagAsync(flag_1, baseUrl_1) { - return __awaiter(this, arguments, void 0, function* (flag, baseUrl, useCache = true) { + 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()] }; } @@ -74,8 +64,8 @@ class FeatureFlagService { } }); } - isFeatureEnabledAsync(flag_1, baseUrl_1) { - return __awaiter(this, arguments, void 0, function* (flag, baseUrl, useCache = true) { + 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; }); diff --git a/lib/services/FeatureFlags.js b/lib/services/FeatureFlags.js index 5c24cf5f..67ccad90 100644 --- a/lib/services/FeatureFlags.js +++ b/lib/services/FeatureFlags.js @@ -4,4 +4,4 @@ exports.FeatureFlags = void 0; var FeatureFlags; (function (FeatureFlags) { FeatureFlags["enableTestScriptFragments"] = "enableTestScriptFragments"; -})(FeatureFlags || (exports.FeatureFlags = FeatureFlags = {})); +})(FeatureFlags = exports.FeatureFlags || (exports.FeatureFlags = {})); diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index e666f2d4..f780280e 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -299,11 +299,12 @@ export class APISupport { } async createTestRun() { - const testRunId = Util.getUniqueId(); - let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.tm2024Version; - urlSuffix = this.baseURL+urlSuffix; try { var startData = this.yamlModel.getStartTestData(); + const testRunId = this.yamlModel.runTimeParams.testRunId; + let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.tm2024Version; + urlSuffix = this.baseURL+urlSuffix; + 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)); diff --git a/src/models/PayloadModels.ts b/src/models/PayloadModels.ts index 029a4430..c4f1352f 100644 --- a/src/models/PayloadModels.ts +++ b/src/models/PayloadModels.ts @@ -47,9 +47,9 @@ export interface TestModel { keyvaultReferenceIdentityType?: string; keyvaultReferenceIdentityId?: string| null; metricsReferenceIdentityType?: string; - metricsReferenceIdentityId?: string; + metricsReferenceIdentityId?: string | null; engineBuiltinIdentityType?: string; - engineBuiltinIdentityIds?: string[]; + engineBuiltinIdentityIds?: string[] | null; baselineTestRunId?: string; kind?: TestKind; }; @@ -139,4 +139,10 @@ export interface ExistingParams { secrets: { [key: string]: SecretMetadata | null }; env: { [key: string]: string | null }; passFailCriteria: { [key: string]: PassFailMetric | null }; +} + +export enum ManagedIdentityTypeForAPI { + SystemAssigned = "SystemAssigned", + UserAssigned = "UserAssigned", + None = "None" } \ No newline at end of file diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts index 7f059310..0e298dc6 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -6,8 +6,8 @@ import { TestKind } from "./engine/TestKind"; import { BaseLoadTestFrameworkModel } from "./engine/BaseLoadTestFrameworkModel"; const yaml = require('js-yaml'); import * as fs from 'fs'; -import { AutoStopCriteria, AutoStopCriteria as autoStopCriteriaObjOut } from "./PayloadModels"; -import { AutoStopCriteriaObjYaml, ManagedIdentityType, ParamType, RunTimeParams } from "./UtilModels"; +import { AutoStopCriteria, AutoStopCriteria as autoStopCriteriaObjOut, ManagedIdentityTypeForAPI } from "./PayloadModels"; +import { AllManagedIdentitiesSegregated, AutoStopCriteriaObjYaml, ParamType, ReferenceIdentityKinds, RunTimeParams } from "./UtilModels"; import * as core from '@actions/core'; import { PassFailMetric, ExistingParams, TestModel, CertificateMetadata, SecretMetadata, RegionConfiguration } from "./PayloadModels"; @@ -29,9 +29,17 @@ export class YamlConfig { secrets: { [key: string] : SecretMetadata | null} = {}; failureCriteria: { [key: string]: number } = {}; // this is yaml model. passFailApiModel : { [key: string]: PassFailMetric | null } = {}; // this is api model. - autoStop: autoStopCriteriaObjOut | null = null; + + keyVaultReferenceIdentityType: ManagedIdentityTypeForAPI = ManagedIdentityTypeForAPI.SystemAssigned; + metricsReferenceIdentityType: ManagedIdentityTypeForAPI = ManagedIdentityTypeForAPI.SystemAssigned; + engineReferenceIdentityType: ManagedIdentityTypeForAPI = ManagedIdentityTypeForAPI.None; + keyVaultReferenceIdentity: string| null = null; - keyVaultReferenceIdentityType: ManagedIdentityType = ManagedIdentityType.SystemAssigned; + metricsReferenceIdentity: string| null = null; + engineReferenceIdentities: string[] | null = null; + + autoStop: autoStopCriteriaObjOut | null = null; + regionalLoadTestConfig: RegionConfiguration[] | null = null; runTimeParams: RunTimeParams = {env: {}, secrets: {}, runDisplayName: '', runDescription: '', testId: '', testRunId: ''}; @@ -109,7 +117,6 @@ export class YamlConfig { } } if(config.secrets != undefined) { - this.keyVaultReferenceIdentityType = ManagedIdentityType.SystemAssigned; this.secrets = this.parseParameters(config.secrets, ParamType.secrets) as { [key: string]: SecretMetadata }; } if(config.env != undefined) { @@ -118,10 +125,15 @@ export class YamlConfig { if(config.certificates != undefined){ this.certificates = this.parseParameters(config.certificates, ParamType.cert) as CertificateMetadata | null; } - if(config.keyVaultReferenceIdentity != undefined) { - this.keyVaultReferenceIdentityType = ManagedIdentityType.UserAssigned; - this.keyVaultReferenceIdentity = config.keyVaultReferenceIdentity; + if(config.keyVaultReferenceIdentity != undefined || config.keyVaultReferenceIdentityType != undefined) { + this.keyVaultReferenceIdentityType = config.keyVaultReferenceIdentity ? ManagedIdentityTypeForAPI.UserAssigned : ManagedIdentityTypeForAPI.SystemAssigned; + this.keyVaultReferenceIdentity = config.keyVaultReferenceIdentity ?? null; + } + + if(config.referenceIdentities != undefined) { + this.getReferenceIdentities(config.referenceIdentities as {[key: string]: string}[]); } + if(config.regionalLoadTestConfig != undefined) { this.regionalLoadTestConfig = this.getMultiRegionLoadTestConfig(config.regionalLoadTestConfig); } @@ -139,6 +151,26 @@ export class YamlConfig { Util.validateTestRunParamsFromPipeline(this.runTimeParams); } + getReferenceIdentities(referenceIdentities: {[key: string]: string}[]) { + + let segregatedManagedIdentities : AllManagedIdentitiesSegregated = Util.validateAndGetSegregatedManagedIdentities(referenceIdentities); + + this.keyVaultReferenceIdentity = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[ReferenceIdentityKinds.KeyVault].length > 0 ? segregatedManagedIdentities.referenceIdentityValuesUAMIMap[ReferenceIdentityKinds.KeyVault][0] : null; + this.keyVaultReferenceIdentityType = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[ReferenceIdentityKinds.KeyVault].length > 0 ? ManagedIdentityTypeForAPI.UserAssigned : ManagedIdentityTypeForAPI.SystemAssigned; + + this.metricsReferenceIdentity = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[ReferenceIdentityKinds.Metrics].length > 0 ? segregatedManagedIdentities.referenceIdentityValuesUAMIMap[ReferenceIdentityKinds.Metrics][0] : null; + this.metricsReferenceIdentityType = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[ReferenceIdentityKinds.Metrics].length > 0 ? ManagedIdentityTypeForAPI.UserAssigned : ManagedIdentityTypeForAPI.SystemAssigned; + + if(segregatedManagedIdentities.referenceIdentiesSystemAssignedCount[ReferenceIdentityKinds.Engine] > 0) { + this.engineReferenceIdentityType = ManagedIdentityTypeForAPI.SystemAssigned; + } else if(segregatedManagedIdentities.referenceIdentityValuesUAMIMap[ReferenceIdentityKinds.Engine].length > 0) { + this.engineReferenceIdentityType = ManagedIdentityTypeForAPI.UserAssigned; + this.engineReferenceIdentities = segregatedManagedIdentities.referenceIdentityValuesUAMIMap[ReferenceIdentityKinds.Engine]; + } else { + this.engineReferenceIdentityType = ManagedIdentityTypeForAPI.None; + } + } + getRunTimeParams() { var secretRun = core.getInput('secrets'); let secretsParsed : {[key: string] : SecretMetadata} = {}; @@ -248,6 +280,10 @@ export class YamlConfig { publicIPDisabled : this.publicIPDisabled, keyvaultReferenceIdentityType: this.keyVaultReferenceIdentityType, keyvaultReferenceIdentityId: this.keyVaultReferenceIdentity, + engineBuiltinIdentityIds: this.engineReferenceIdentities, + engineBuiltinIdentityType: this.engineReferenceIdentityType, + metricsReferenceIdentityType: this.metricsReferenceIdentityType, + metricsReferenceIdentityId: this.metricsReferenceIdentity }; return data; } diff --git a/src/models/UtilModels.ts b/src/models/UtilModels.ts index 5a0fe058..64e7777b 100644 --- a/src/models/UtilModels.ts +++ b/src/models/UtilModels.ts @@ -21,6 +21,12 @@ export interface RunTimeParams { testId: string; } +export enum ReferenceIdentityKinds { + KeyVault = "KeyVault", + Metrics = "Metrics", + Engine = "Engine" +} + export enum TokenScope { Dataplane, ControlPlane @@ -85,4 +91,9 @@ export const ValidConditionList = { export enum ManagedIdentityType { SystemAssigned = "SystemAssigned", UserAssigned = "UserAssigned", +} + +export interface AllManagedIdentitiesSegregated { + referenceIdentityValuesUAMIMap: { [key in ReferenceIdentityKinds]: string[] }, + referenceIdentiesSystemAssignedCount : { [key in ReferenceIdentityKinds]: number } } \ No newline at end of file diff --git a/src/models/constants.ts b/src/models/constants.ts index 7b93ab26..7a39654d 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -44,6 +44,18 @@ export const defaultYaml: any = 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" + } ] } diff --git a/src/models/util.ts b/src/models/util.ts index 111fbcdd..40fdd778 100644 --- a/src/models/util.ts +++ b/src/models/util.ts @@ -5,8 +5,8 @@ import { defaultYaml } from './constants'; import * as EngineUtil from './engine/Util'; import { BaseLoadTestFrameworkModel } from './engine/BaseLoadTestFrameworkModel'; import { TestKind } from "./engine/TestKind"; -import { PassFailMetric, Statistics, TestRunArtifacts, TestRunModel, TestModel } from './PayloadModels'; -import { RunTimeParams, ValidAggregateList, ValidConditionList, ManagedIdentityType, PassFailCount } from './UtilModels'; +import { PassFailMetric, Statistics, TestRunArtifacts, TestRunModel, TestModel, ManagedIdentityTypeForAPI } from './PayloadModels'; +import { RunTimeParams, ValidAggregateList, ValidConditionList, ManagedIdentityType, PassFailCount, ReferenceIdentityKinds, AllManagedIdentitiesSegregated } from './UtilModels'; export function checkFileType(filePath: string, fileExtToValidate: string): boolean{ if(isNullOrUndefined(filePath)){ @@ -243,12 +243,17 @@ function isInValidSubnet(uri: string): boolean { return !(pattern.test(uri)); } -function isInValidKVId(uri: string): boolean { +function isInvalidManagedIdentityId(uri: string): boolean { const pattern = /^\/subscriptions\/[a-f0-9-]+\/resourceGroups\/[a-zA-Z0-9\u0080-\uFFFF()._-]+\/providers\/Microsoft\.ManagedIdentity\/userAssignedIdentities\/[a-zA-Z0-9._-]+$/i; return !(pattern.test(uri)); } +function isValidReferenceIdentityKind(value: string): value is ManagedIdentityType { + return Object.values(ReferenceIdentityKinds).includes(value as ReferenceIdentityKinds); +} + + function isValidTestKind(value: string): value is TestKind { return Object.values(TestKind).includes(value as TestKind); } @@ -328,12 +333,30 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s 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' || isInValidKVId(givenYaml.keyVaultReferenceIdentity))){ + 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(!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?.valid == false){ + return result; + } + try { + if(givenYaml.keyVaultReferenceIdentityType || givenYaml.keyVaultReferenceIdentity){ + validateAndGetSegregatedManagedIdentities(givenYaml.referenceIdentities as Array<{[key: string] : string}>, true); + } else { + validateAndGetSegregatedManagedIdentities(givenYaml.referenceIdentities as Array<{[key: string] : string}>); + } + } catch (error : any) { + return {valid : false, error : error.message}; + } + } if(!isNullOrUndefined(givenYaml.keyVaultReferenceIdentity) && givenYaml.keyVaultReferenceIdentityType == ManagedIdentityType.SystemAssigned){ return {valid : false, error : `The "keyVaultReferenceIdentity" should omitted or set to null when using the "SystemAssigned" identity type.`}; } @@ -400,6 +423,86 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s return {valid : true, error : ""}; } +export function validateAndGetSegregatedManagedIdentities(referenceIdentities: {[key: string]: string}[], keyVaultGivenOutOfReferenceIdentities: boolean = false) : AllManagedIdentitiesSegregated { + + let referenceIdentityValuesUAMIMap: { [key in ReferenceIdentityKinds]: string[] } = { + [ReferenceIdentityKinds.KeyVault]: [], + [ReferenceIdentityKinds.Metrics]: [], + [ReferenceIdentityKinds.Engine]: [] + }; + + let referenceIdentiesSystemAssignedCount : { [key in ReferenceIdentityKinds]: number } = { + [ReferenceIdentityKinds.KeyVault]: 0, + [ReferenceIdentityKinds.Metrics]: 0, + [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 as ReferenceIdentityKinds].push(referenceIdentity.value); + } else { + referenceIdentiesSystemAssignedCount[referenceIdentity.kind as ReferenceIdentityKinds]++; + } + } + + // 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."); + } + // this will be assigned above if the given is outside the refIds so no need to assign again. + } + + for(let key in ReferenceIdentityKinds) { + if(key != ReferenceIdentityKinds.Engine) { + if(referenceIdentityValuesUAMIMap[key as ReferenceIdentityKinds].length > 1 || referenceIdentiesSystemAssignedCount[key as ReferenceIdentityKinds] > 1) { + throw new Error(`Only one ${key} reference identity should be provided in the referenceIdentities array.`); + } else if(referenceIdentityValuesUAMIMap[key as ReferenceIdentityKinds].length == 1 && referenceIdentiesSystemAssignedCount[key as ReferenceIdentityKinds] > 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[ReferenceIdentityKinds.Engine].length > 0 && referenceIdentiesSystemAssignedCount[ReferenceIdentityKinds.Engine] > 0) { + throw new Error("Engine reference identity should be either SystemAssigned or UserAssigned but not both."); + } else if(referenceIdentiesSystemAssignedCount[ReferenceIdentityKinds.Engine] > 1) { + throw new Error("Only one Engine reference identity with SystemAssigned should be provided in the referenceIdentities array."); + } + return {referenceIdentityValuesUAMIMap, referenceIdentiesSystemAssignedCount}; +} + +function validateReferenceIdentities(referenceIdentities: Array) : {valid : boolean, error : string} { + 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(!isNullOrUndefined(referenceIdentity.value) && referenceIdentity.type == ManagedIdentityType.SystemAssigned){ + return {valid : false, error : `The "reference identity value" should omitted or set to null when using the "SystemAssigned" identity type.`}; + } + if(isNullOrUndefined(referenceIdentity.value) && referenceIdentity.type == 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 : ""}; +} + /* ado takes the full pf criteria as a string after parsing the string into proper data model, */ diff --git a/test/Utils/ReferenceIdentityYamls.ts b/test/Utils/ReferenceIdentityYamls.ts new file mode 100644 index 00000000..3f6b8c12 --- /dev/null +++ b/test/Utils/ReferenceIdentityYamls.ts @@ -0,0 +1,757 @@ +// valid scenarios: +export const referenceIdentitiesBasicYaml : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + 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' } + ], + autoStop: { errorPercentage: 80, timeWindow: 60 }, + 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" + }, + { + kind : "Engine", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + }, + { + kind : "Engine", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-engine2" + }, + ], + regionalLoadTestConfig: [ + { + region: 'eastus', + engineInstances: 1, + }, + { + region: 'westus', + engineInstances: 1, + } + ] +} + +export const referenceIdentitiesSystemAssignedBasicYaml : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + 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' } + ], + autoStop: { errorPercentage: 80, timeWindow: 60 }, + referenceIdentities: [ + { + kind : "KeyVault", + type: "SystemAssigned", + }, + { + kind : "Metrics", + }, + { + kind : "Engine", + } + ], + regionalLoadTestConfig: [ + { + region: 'eastus', + engineInstances: 1, + }, + { + region: 'westus', + engineInstances: 1, + } + ] +} + +export const referenceIdentitiesSystemAssignedAndUserAssignedYaml : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + 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' } + ], + autoStop: { errorPercentage: 80, timeWindow: 60 }, + referenceIdentities: [ + { + kind : "KeyVault", + type: "SystemAssigned", + }, + { + kind : "Metrics", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity" + }, + { + kind : "Engine", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ], + regionalLoadTestConfig: [ + { + region: 'eastus', + engineInstances: 1, + }, + { + region: 'westus', + engineInstances: 1, + } + ] +} + +export const noReferenceIdentities : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + 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' } + ], + autoStop: { errorPercentage: 80, timeWindow: 60 }, + regionalLoadTestConfig: [ + { + region: 'eastus', + engineInstances: 1, + }, + { + region: 'westus', + engineInstances: 1, + } + ] +} + +export const keyVaultGivenOutOfRefIds : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + 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', + keyVaultReferenceIdentity : "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1", + 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' } + ], + autoStop: { errorPercentage: 80, timeWindow: 60 }, + regionalLoadTestConfig: [ + { + region: 'eastus', + engineInstances: 1, + }, + { + region: 'westus', + engineInstances: 1, + } + ] +} + +// invalid starts +export const referenceIdentities2SystemAssignedForKeyVault : any = +{ + version: 'v0.1', + testId: 'SampleTest', + testName: 'SampleTest', + displayName: 'Sample Test', + 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' } + ], + autoStop: { errorPercentage: 80, timeWindow: 60 }, + referenceIdentities: [ + { + kind : "KeyVault", + }, + { + kind : "KeyVault", + }, + { + kind : "Metrics", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity" + }, + { + kind : "Engine", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ], + regionalLoadTestConfig: [ + { + region: 'eastus', + engineInstances: 1, + }, + { + region: 'westus', + engineInstances: 1, + } + ] +} + +export const referenceIdentities2SystemAssignedForMetrics : 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, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Metrics", + }, + { + kind : "Metrics", + }, + { + kind : "KeyVault", + }, + { + kind : "Engine", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ] +} + +export const referenceIdentities2SystemAssignedForEngine : 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, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Engine", + }, + { + kind : "Engine", + }, + { + kind : "KeyVault", + }, + { + kind : "Metrics", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ] +} + +export const referenceIdentities2UAMIForKeyVault : 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, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Engine", + }, + { + kind : "Metrics", + }, + { + kind : "KeyVault", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + }, + { + kind : "KeyVault", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ] +} + +export const referenceIdentities2UAMIForMetrics : 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, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Engine", + }, + { + kind : "KeyVault", + }, + { + kind : "Metrics", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + }, + { + kind : "Metrics", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ] +} + +export const referenceIdentitiesSystemAssignedAndUAMIForKeyVault : 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, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Engine", + }, + { + kind : "KeyVault", + }, + { + kind : "KeyVault", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + }, + { + kind : "Metrics", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ] +} + +export const referenceIdentitiesSystemAssignedAndUAMIForMetrics : any = +{ + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Engine", + }, + { + kind : "Metrics", + }, + { + kind : "KeyVault", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + }, + { + kind : "Metrics", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ] +} + +export const referenceIdentitiesSystemAssignedAndUAMIForEngine : any = +{ + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Engine", + }, + { + kind : "Metrics", + }, + { + kind : "KeyVault", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + }, + { + kind : "Engine", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ] +} + +export const referenceIdentitiesGivenInKeyVaultOutsideAndInside : any = +{ + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + keyVaultReferenceIdentity : "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1", + referenceIdentities: [ + { + kind : "Metrics", + }, + { + kind : "KeyVault", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + }, + { + kind : "Engine", + type: "UserAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ] +} + +export const referenceIdentitiesNotAnArray : any = +{ + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + keyVaultReferenceIdentity : "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1", + referenceIdentities: { + hi: 123 + } +} + +export const referenceIdentitiesWithImproperKind : any = +{ + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + keyVaultReferenceIdentity : "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1", + referenceIdentities: [ + { + kind : "MetricsDummy", + } + ] +} + +export const referenceIdentitiesWithImproperType : any = +{ + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + keyVaultReferenceIdentity : "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1", + referenceIdentities: [ + { + kind : "Metrics", + type: "Dummy" + } + ] +} + +export const referenceIdentityWithValueButSystemAssigned: any = { + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Metrics", + type: "SystemAssigned", + value: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity-1" + } + ] +} + +export const referenceIdentityWithNoValueButUserAssigned: any = { + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Metrics", + type: "UserAssigned" + } + ] +} + +export const referenceIdentitywithInvalidKVID: any = { + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Metrics", + type: "UserAssigned", + value: "dummy" + } + ] +} + +export const referenceIdentitywithInvalidKVIDAsStringItself: any = { + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Metrics", + type: "UserAssigned", + value: ["hi123", "123"] + } + ] +} + + +export const referenceIdentitywithInvalidDict: any = { + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + "mohit1", "mohit2" + ] +} + +export const referenceIdentityTypewithInvalidStringInKVID: any = { + version: 'v0.1', + testName: 'SampleTest', + testPlan: 'SampleTest.jmx', + testType: 'JMX', + engineInstances: 2, + publicIPDisabled: false, + configurationFiles: [ 'sampledata.csv' ], + zipArtifacts: [ 'bigdata.zip' ], + referenceIdentities: [ + { + kind : "Metrics", + type: ["UserAssigned", "SystemAssigned"], + } + ] +} \ No newline at end of file diff --git a/test/Utils/checkForValidationOfYaml.test.ts b/test/Utils/checkForValidationOfYaml.test.ts index a3d75272..43736ed4 100644 --- a/test/Utils/checkForValidationOfYaml.test.ts +++ b/test/Utils/checkForValidationOfYaml.test.ts @@ -1,5 +1,6 @@ import { checkValidityYaml, getAllFileErrors } from '../../src/models/util' import * as constants from './testYamls'; +import * as referenceIdentityConstants from './ReferenceIdentityYamls'; describe('invalid Yaml tests', () =>{ describe('basic scenarios for invalid cases', ()=>{ @@ -148,8 +149,98 @@ describe('valid yaml tests', () => { test('subnet id and PIP is true', () => { expect(checkValidityYaml(constants.subnetIdPIPDisabledTrue)).toStrictEqual({valid : true, error : ""}); }); -}) +}); +describe('reference identity validations', () => { + test('Basic test with UAMI for all the refIds', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesBasicYaml)).toStrictEqual({valid : true, error : ""}); + }); + + test('Basic test with SAMI for all the refIds', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesSystemAssignedBasicYaml)).toStrictEqual({valid : true, error : ""}); + }); + + test('Basic test with UAMI and SAMI for few of the refIds', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesSystemAssignedAndUserAssignedYaml)).toStrictEqual({valid : true, error : ""}); + }); + + test('no refIds', () => { + expect(checkValidityYaml(referenceIdentityConstants.noReferenceIdentities)).toStrictEqual({valid : true, error : ""}); + }); + + test('keyVault is given outside of the refIds', () => { + expect(checkValidityYaml(referenceIdentityConstants.keyVaultGivenOutOfRefIds)).toStrictEqual({valid : true, error : ''}); + }); + + // invalid scenarios. + test('2 system assigned ids on the KeyVault', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentities2SystemAssignedForKeyVault)).toStrictEqual({valid : false, error : 'Only one KeyVault reference identity should be provided in the referenceIdentities array.'}); + }); + + test('2 system assigned on the Metrics', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentities2SystemAssignedForMetrics)).toStrictEqual({valid : false, error : 'Only one Metrics reference identity should be provided in the referenceIdentities array.'}); + }); + + test('2 system assigned on the Engine', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentities2SystemAssignedForEngine)).toStrictEqual({valid : false, error : 'Only one Engine reference identity with SystemAssigned should be provided in the referenceIdentities array.'}); + }); + + test('2 UAMI ids on the KeyVault', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentities2UAMIForKeyVault)).toStrictEqual({valid : false, error : 'Only one KeyVault reference identity should be provided in the referenceIdentities array.'}); + }); + + test('2 UAMI on the Metrics', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentities2UAMIForMetrics)).toStrictEqual({valid : false, error : 'Only one Metrics reference identity should be provided in the referenceIdentities array.'}); + }); + + test('UAMI and SAMI for KeyVault', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesSystemAssignedAndUAMIForKeyVault)).toStrictEqual({valid : false, error : 'KeyVault reference identity should be either SystemAssigned or UserAssigned but not both.'}); + }); + + test('UAMI and SAMI for Metrics', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesSystemAssignedAndUAMIForMetrics)).toStrictEqual({valid : false, error : 'Metrics reference identity should be either SystemAssigned or UserAssigned but not both.'}); + }); + + test('UAMI and SAMI for Engine', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesSystemAssignedAndUAMIForEngine)).toStrictEqual({valid : false, error : 'Engine reference identity should be either SystemAssigned or UserAssigned but not both.'}); + }); + + 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.'}); + }); + + test('reference identities is not an array', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesNotAnArray)).toStrictEqual({valid : false, error : `The value "${referenceIdentityConstants.referenceIdentitiesNotAnArray.referenceIdentities.toString()}" for referenceIdentities is invalid. Provide a valid list of reference identities.`}); + }); + + test('reference identities has wrong kind', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesWithImproperKind)).toStrictEqual({valid : false, error : `The value "MetricsDummy" for kind in referenceIdentity is invalid. Allowed values are 'Metrics', 'Keyvault' and 'Engine'.`}); + }); + test('reference identities has wrong type', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitiesWithImproperType)).toStrictEqual({valid : false, error : 'The value "Dummy" for type in referenceIdentities is invalid. Allowed values are "SystemAssigned" and "UserAssigned".'}); + }); + + test('system assigned with value given', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentityWithValueButSystemAssigned)).toStrictEqual({valid : false, error : 'The "reference identity value" should omitted or set to null when using the "SystemAssigned" identity type.'}); + }); + + test('system assigned with value given', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentityWithNoValueButUserAssigned)).toStrictEqual({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'.`}); + }); + + test('invalid kvid', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitywithInvalidKVID)).toStrictEqual({valid : false, error : `The value "dummy" for reference identity is invalid. The value should be a string of the format: "/subscriptions/{subsId}/resourceGroups/{rgName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}".`}); + }); + test('invalid values for kvid', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitywithInvalidKVIDAsStringItself)).toStrictEqual({valid : false, error : `The value "hi123,123" for id in referenceIdentities is invalid. Provide a valid string.`}); + }); + test('invalid values as string', () => { + expect(checkValidityYaml(referenceIdentityConstants.referenceIdentitywithInvalidDict)).toStrictEqual({valid : false, error : `The value "mohit1" for referenceIdentities is invalid. Provide a valid dictionary with kind, value and type.`}); + }); + test('invalid type as string', () => { + 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('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 From d367912b036f87085d854a446eb607169e89bd7b Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 00:57:46 +0530 Subject: [PATCH 02/29] deleting js files. --- lib/main.js | 62 -- lib/models/APIResponseModel.js | 2 - lib/models/APISupport.js | 452 ------------ lib/models/AuthenticationUtils.js | 165 ----- lib/models/FetchHelper.js | 86 --- lib/models/FileUtils.js | 97 --- lib/models/PayloadModels.js | 21 - lib/models/TaskModels.js | 388 ---------- lib/models/UtilModels.js | 72 -- lib/models/constants.js | 72 -- .../engine/BaseLoadTestFrameworkModel.js | 2 - lib/models/engine/JMeterFrameworkModel.js | 40 -- lib/models/engine/LocustFrameworkModel.js | 40 -- lib/models/engine/TestKind.js | 12 - lib/models/engine/Util.js | 103 --- lib/models/util.js | 664 ------------------ lib/services/FeatureFlagService.js | 74 -- lib/services/FeatureFlags.js | 7 - 18 files changed, 2359 deletions(-) delete mode 100644 lib/main.js delete mode 100644 lib/models/APIResponseModel.js delete mode 100644 lib/models/APISupport.js delete mode 100644 lib/models/AuthenticationUtils.js delete mode 100644 lib/models/FetchHelper.js delete mode 100644 lib/models/FileUtils.js delete mode 100644 lib/models/PayloadModels.js delete mode 100644 lib/models/TaskModels.js delete mode 100644 lib/models/UtilModels.js delete mode 100644 lib/models/constants.js delete mode 100644 lib/models/engine/BaseLoadTestFrameworkModel.js delete mode 100644 lib/models/engine/JMeterFrameworkModel.js delete mode 100644 lib/models/engine/LocustFrameworkModel.js delete mode 100644 lib/models/engine/TestKind.js delete mode 100644 lib/models/engine/Util.js delete mode 100644 lib/models/util.js delete mode 100644 lib/services/FeatureFlagService.js delete mode 100644 lib/services/FeatureFlags.js diff --git a/lib/main.js b/lib/main.js deleted file mode 100644 index 60610ed3..00000000 --- a/lib/main.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -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(); - yield apiSupport.getTestAPI(false); - if (fs.existsSync(UtilModels_1.resultFolder)) { - util.deleteFile(UtilModels_1.resultFolder); - } - fs.mkdirSync(UtilModels_1.resultFolder); - yield apiSupport.createTestAPI(); - } - catch (err) { - core.setFailed(err.message); - } - }); -} -run(); diff --git a/lib/models/APIResponseModel.js b/lib/models/APIResponseModel.js deleted file mode 100644 index c8ad2e54..00000000 --- a/lib/models/APIResponseModel.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/models/APISupport.js b/lib/models/APISupport.js deleted file mode 100644 index 306e3901..00000000 --- a/lib/models/APISupport.js +++ /dev/null @@ -1,452 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APISupport = void 0; -const util_1 = require("util"); -const UtilModels_1 = require("./UtilModels"); -const TestKind_1 = require("./engine/TestKind"); -const Util = __importStar(require("./util")); -const FileUtils = __importStar(require("./FileUtils")); -const core = __importStar(require("@actions/core")); -const FetchUtil = __importStar(require("./FetchHelper")); -class APISupport { - constructor(authContext, yamlModel) { - this.baseURL = ''; - this.existingParams = { secrets: {}, env: {}, passFailCriteria: {} }; - this.authContext = authContext; - this.yamlModel = yamlModel; - this.testId = this.yamlModel.testId; - } - getResource() { - return __awaiter(this, void 0, void 0, function* () { - let id = this.authContext.resourceId; - let armUrl = this.authContext.armEndpoint; - let armEndpointSuffix = id + "?api-version=" + UtilModels_1.ApiVersionConstants.cp2022Version; - let armEndpoint = new URL(armEndpointSuffix, armUrl); - let header = yield this.authContext.armTokenHeader(); - let response = yield FetchUtil.httpClientRetries(armEndpoint.toString(), header, 'get', 3, ""); - let resource_name = core.getInput('loadTestResource'); - if (response.message.statusCode == 404) { - var message = `The Azure Load Testing resource ${resource_name} does not exist. Please provide an existing resource.`; - throw new Error(message); - } - let respObj = yield Util.getResultObj(response); - if (response.message.statusCode != 200) { - console.log(respObj ? respObj : Util.ErrorCorrection(response)); - throw new Error("Error fetching resource " + resource_name); - } - let dataPlaneUrl = respObj.properties.dataPlaneURI; - this.baseURL = 'https://' + dataPlaneUrl + '/'; - }); - } - getTestAPI(validate, 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.tm2024Version; - urlSuffix = this.baseURL + urlSuffix; - let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.get); - let testResult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'get', 3, ""); - if (testResult.message.statusCode == 401 || testResult.message.statusCode == 403) { - var message = "Service Principal does not have sufficient permissions. Please assign " - + "the Load Test Contributor role to the service principal. Follow the steps listed at " - + "https://docs.microsoft.com/azure/load-testing/tutorial-cicd-github-actions#configure-the-github-actions-workflow-to-run-a-load-test "; - throw new Error(message); - } - if (testResult.message.statusCode != 200 && testResult.message.statusCode != 201) { - if (validate) { // validate is called, then get should not be false, and this validate had retries because of the conflicts in jmx test, so lets not print in the console, instead put this in the error itself. - let testObj = yield Util.getResultObj(testResult); - let err = ((_a = testObj === null || testObj === void 0 ? void 0 : testObj.error) === null || _a === void 0 ? void 0 : _a.message) ? (_b = testObj === null || testObj === void 0 ? void 0 : testObj.error) === null || _b === void 0 ? void 0 : _b.message : Util.ErrorCorrection(testResult); - throw new Error(err); - } - else if (!validate && testResult.message.statusCode != 404) { // if not validate, then its to check if it is edit or create thats all, so it should not throw the error for 404. - let testObj = yield Util.getResultObj(testResult); - console.log(testObj ? testObj : Util.ErrorCorrection(testResult)); - throw new Error("Error in getting the test."); - } - // note : kumarmoh - /// else { - // do nothing if the validate = false and status code is 404, as it is for create test. - // } this is just for comment - } - if (testResult.message.statusCode == 200) { - let testObj = yield Util.getResultObj(testResult); - if (testObj == null) { - throw new Error(Util.ErrorCorrection(testResult)); - } - let inputScriptFileInfo = testObj.kind == TestKind_1.TestKind.URL ? (_c = testObj.inputArtifacts) === null || _c === void 0 ? void 0 : _c.urlTestConfigFileInfo : (_d = testObj.inputArtifacts) === null || _d === void 0 ? void 0 : _d.testScriptFileInfo; - if (validate) { - if (returnTestObj) { - return [inputScriptFileInfo === null || inputScriptFileInfo === void 0 ? void 0 : inputScriptFileInfo.validationStatus, testObj]; - } - return inputScriptFileInfo === null || inputScriptFileInfo === void 0 ? void 0 : inputScriptFileInfo.validationStatus; - } - else { - if (!(0, util_1.isNullOrUndefined)(testObj.passFailCriteria) && !(0, util_1.isNullOrUndefined)(testObj.passFailCriteria.passFailMetrics)) - this.existingParams.passFailCriteria = testObj.passFailCriteria.passFailMetrics; - if (testObj.secrets != null) { - this.existingParams.secrets = testObj.secrets; - } - if (testObj.environmentVariables != null) { - this.existingParams.env = testObj.environmentVariables; - } - } - } - }); - } - deleteFileAPI(filename) { - return __awaiter(this, void 0, void 0, function* () { - var urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; - urlSuffix = this.baseURL + urlSuffix; - let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.delete); - let delFileResult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'del', 3, ""); - if (delFileResult.message.statusCode != 204) { - let delFileObj = yield Util.getResultObj(delFileResult); - let Message = delFileObj ? delFileObj.message : Util.ErrorCorrection(delFileResult); - throw new Error(Message); - } - }); - } - createTestAPI() { - return __awaiter(this, void 0, void 0, function* () { - let urlSuffix = "tests/" + this.testId + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; - urlSuffix = this.baseURL + urlSuffix; - let createData = this.yamlModel.getCreateTestData(this.existingParams); - let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.patch); - let createTestresult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'patch', 3, JSON.stringify(createData)); - if (createTestresult.message.statusCode != 200 && createTestresult.message.statusCode != 201) { - let testRunObj = yield Util.getResultObj(createTestresult); - console.log(testRunObj ? testRunObj : Util.ErrorCorrection(createTestresult)); - throw new Error("Error in creating test: " + this.testId); - } - if (createTestresult.message.statusCode == 201) { - console.log("Creating a new load test " + this.testId); - console.log("Successfully created load test " + this.testId); - } - else { - console.log("Test '" + this.testId + "' already exists"); - // test script will anyway be updated by the ado in later steps, this will be error if the test script is not present in the test. - // this will be error in the url tests when the quick test is getting updated to the url test. so removing this. - let testObj = yield Util.getResultObj(createTestresult); - var testFiles = testObj.inputArtifacts; - if (testFiles.userPropUrl != null) { - console.log(`Deleting the existing UserProperty file.`); - yield this.deleteFileAPI(testFiles.userPropFileInfo.fileName); - } - if (testFiles.testScriptFileInfo != null) { - console.log(`Deleting the existing TestScript file.`); - yield this.deleteFileAPI(testFiles.testScriptFileInfo.fileName); - } - if (testFiles.additionalFileInfo != null) { - // delete existing files which are not present in yaml, the files which are in yaml will anyway be uploaded again. - let existingFiles = []; - let file; - for (file of testFiles.additionalFileInfo) { - existingFiles.push(file.fileName); - } - for (let file of this.yamlModel.configurationFiles) { - file = this.yamlModel.getFileName(file); - let indexOfFile = existingFiles.indexOf(file); - if (indexOfFile != -1) { - existingFiles.splice(indexOfFile, 1); - } - } - for (let file of this.yamlModel.zipArtifacts) { - file = this.yamlModel.getFileName(file); - let indexOfFile = existingFiles.indexOf(file); - if (indexOfFile != -1) { - existingFiles.splice(indexOfFile, 1); - } - } - if (existingFiles.length > 0) { - console.log(`Deleting the ${existingFiles.length} existing test file(s) which is(are) not in the configuration yaml file.`); - } - for (const file of existingFiles) { - yield this.deleteFileAPI(file); - } - } - } - yield this.uploadConfigFile(); - }); - } - uploadTestPlan() { - return __awaiter(this, void 0, void 0, function* () { - let retry = 5; - let filepath = this.yamlModel.testPlan; - let filename = this.yamlModel.getFileName(filepath); - let urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; - let fileType = UtilModels_1.FileType.TEST_SCRIPT; - if (this.yamlModel.kind == TestKind_1.TestKind.URL) { - fileType = UtilModels_1.FileType.URL_TEST_CONFIG; - } - urlSuffix = this.baseURL + urlSuffix + ("&fileType=" + fileType); - let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.put); - let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); - if (uploadresult.message.statusCode != 201) { - let uploadObj = yield Util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading TestPlan for the created test"); - } - else { - console.log("Uploaded test plan for the test"); - let minutesToAdd = 10; - let startTime = new Date(); - let maxAllowedTime = new Date(startTime.getTime() + minutesToAdd * 60000); - let validationStatus = "VALIDATION_INITIATED"; - let testObj = null; - while (maxAllowedTime > (new Date()) && (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED" || validationStatus == null)) { - try { - [validationStatus, testObj] = (yield this.getTestAPI(true, true)); - } - catch (e) { - retry--; - if (retry == 0) { - throw new Error("Unable to validate the test plan. Please retry. Failed with error :" + e); - } - } - yield Util.sleep(5000); - } - console.log("Validation status of the test plan: " + validationStatus); - if (validationStatus == null || validationStatus == "VALIDATION_SUCCESS") { - console.log(`Validated test plan for the test successfully.`); - // Get errors from all files - let fileErrors = Util.getAllFileErrors(testObj); - if (Object.keys(fileErrors).length > 0) { - console.log("Validation failed for the following files:"); - for (const [file, error] of Object.entries(fileErrors)) { - console.log(`File: ${file}, Error: ${error}`); - } - throw new Error("Validation of one or more files failed. Please correct the errors and try again."); - } - yield this.createTestRun(); - } - else if (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED") - throw new Error("TestPlan validation timeout. Please try again."); - else - throw new Error("TestPlan validation Failed."); - } - }); - } - uploadConfigFile() { - return __awaiter(this, void 0, void 0, function* () { - let configFiles = this.yamlModel.configurationFiles; - if (configFiles != undefined && configFiles.length > 0) { - for (let filepath of configFiles) { - let filename = this.yamlModel.getFileName(filepath); - let urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version + ("&fileType=" + UtilModels_1.FileType.ADDITIONAL_ARTIFACTS); - urlSuffix = this.baseURL + urlSuffix; - let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.put); - let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); - if (uploadresult.message.statusCode != 201) { - let uploadObj = yield Util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading config file for the created test"); - } - } - ; - console.log(`Uploaded ${configFiles.length} configuration file(s) for the test successfully.`); - } - yield this.uploadZipArtifacts(); - }); - } - uploadZipArtifacts() { - return __awaiter(this, void 0, void 0, function* () { - let zipFiles = this.yamlModel.zipArtifacts; - if (zipFiles != undefined && zipFiles.length > 0) { - console.log("Uploading and validating the zip artifacts"); - for (const filepath of zipFiles) { - let filename = this.yamlModel.getFileName(filepath); - var urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version + "&fileType=" + UtilModels_1.FileType.ZIPPED_ARTIFACTS; - urlSuffix = this.baseURL + urlSuffix; - let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.put); - let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); - if (uploadresult.message.statusCode != 201) { - let uploadObj = yield Util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading config file for the created test"); - } - } - console.log(`Uploaded ${zipFiles.length} zip artifact(s) for the test successfully.`); - } - let statuscode = yield this.uploadPropertyFile(); - if (statuscode == 201) - yield this.uploadTestPlan(); - }); - } - uploadPropertyFile() { - return __awaiter(this, void 0, void 0, function* () { - let propertyFile = this.yamlModel.propertyFile; - if (propertyFile != undefined && propertyFile != '') { - let filename = this.yamlModel.getFileName(propertyFile); - let urlSuffix = "tests/" + this.testId + "/files/" + filename + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version + "&fileType=" + UtilModels_1.FileType.USER_PROPERTIES; - urlSuffix = this.baseURL + urlSuffix; - let headers = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.put); - let uploadresult = yield FetchUtil.httpClientRetries(urlSuffix, headers, 'put', 3, propertyFile, true); - if (uploadresult.message.statusCode != 201) { - let uploadObj = yield Util.getResultObj(uploadresult); - console.log(uploadObj ? uploadObj : Util.ErrorCorrection(uploadresult)); - throw new Error("Error in uploading TestPlan for the created test"); - } - console.log(`Uploaded user properties file for the test successfully.`); - } - return 201; - }); - } - createTestRun() { - return __awaiter(this, void 0, void 0, function* () { - try { - var startData = this.yamlModel.getStartTestData(); - const testRunId = this.yamlModel.runTimeParams.testRunId; - let urlSuffix = "test-runs/" + testRunId + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; - urlSuffix = this.baseURL + urlSuffix; - console.log("Creating and running a testRun for the test"); - let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.patch); - let startTestresult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'patch', 3, JSON.stringify(startData)); - let testRunDao = yield Util.getResultObj(startTestresult); - if (startTestresult.message.statusCode != 200 && startTestresult.message.statusCode != 201) { - console.log(testRunDao ? testRunDao : Util.ErrorCorrection(startTestresult)); - throw new Error("Error in running the test"); - } - let startTime = new Date(); - let portalUrl = testRunDao.portalUrl; - let status = testRunDao.status; - if (status == "ACCEPTED") { - console.log("View the load test run in progress at: " + portalUrl); - yield this.getTestRunAPI(testRunId, status, startTime); - } - } - catch (err) { - if (!err.message) - err.message = "Error in running the test"; - throw new Error(err.message); - } - }); - } - getTestRunAPI(testRunId, testStatus, startTime) { - return __awaiter(this, void 0, void 0, function* () { - let urlSuffix = "test-runs/" + testRunId + "?api-version=" + UtilModels_1.ApiVersionConstants.tm2024Version; - urlSuffix = this.baseURL + urlSuffix; - while (!Util.isTerminalTestStatus(testStatus)) { - let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.get); - let testRunResult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'get', 3, ""); - let testRunObj = yield Util.getResultObj(testRunResult); - if (testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201) { - console.log(testRunObj ? testRunObj : Util.ErrorCorrection(testRunResult)); - throw new Error("Error in getting the test run"); - } - testStatus = testRunObj.status; - if (Util.isTerminalTestStatus(testStatus)) { - let vusers = null; - let count = 0; - let reportsAvailable = false; - console.log("Test run completed. Polling for statistics and dashboard report to populate."); - // Polling for max 3 min for statistics and pass fail criteria to populate - while ((!reportsAvailable || (0, util_1.isNullOrUndefined)(vusers)) && count < 18) { - yield Util.sleep(10000); - let header = yield this.authContext.getDataPlaneHeader(UtilModels_1.CallTypeForDP.get); - let testRunResult = yield FetchUtil.httpClientRetries(urlSuffix, header, 'get', 3, ""); - testRunObj = yield Util.getResultObj(testRunResult); - if (testRunObj == null) { - throw new Error(Util.ErrorCorrection(testRunResult)); - } - if (testRunResult.message.statusCode != 200 && testRunResult.message.statusCode != 201) { - console.log(testRunResult ? testRunResult : Util.ErrorCorrection(testRunResult)); - throw new Error("Error in getting the test run"); - } - vusers = testRunObj.virtualUsers; - count++; - let testReport = Util.getReportFolder(testRunObj.testArtifacts); - if (testReport) { - reportsAvailable = true; - } - } - if (testRunObj && testRunObj.startDateTime) { - startTime = new Date(testRunObj.startDateTime); - } - let endTime = new Date(); - if (testRunObj && testRunObj.endDateTime) { - endTime = new Date(testRunObj.endDateTime); - } - Util.printTestDuration(testRunObj); - if (!(0, util_1.isNullOrUndefined)(testRunObj.passFailCriteria) && !(0, util_1.isNullOrUndefined)(testRunObj.passFailCriteria.passFailMetrics)) - Util.printCriteria(testRunObj.passFailCriteria.passFailMetrics); - if (testRunObj.testRunStatistics != null && testRunObj.testRunStatistics != undefined) - Util.printClientMetrics(testRunObj.testRunStatistics); - let testResultUrl = Util.getResultFolder(testRunObj.testArtifacts); - if (testResultUrl != null) { - const response = yield FetchUtil.httpClientRetries(testResultUrl, {}, 'get', 3, ""); - if (response.message.statusCode != 200) { - let respObj = yield Util.getResultObj(response); - console.log(respObj ? respObj : Util.ErrorCorrection(response)); - throw new Error("Error in fetching results "); - } - else { - yield FileUtils.uploadFileToResultsFolder(response, UtilModels_1.resultZipFileName); - } - } - let testReportUrl = Util.getReportFolder(testRunObj.testArtifacts); - if (testReportUrl != null) { - const response = yield FetchUtil.httpClientRetries(testReportUrl, {}, 'get', 3, ""); - if (response.message.statusCode != 200) { - let respObj = yield Util.getResultObj(response); - console.log(respObj ? respObj : Util.ErrorCorrection(response)); - throw new Error("Error in fetching report "); - } - else { - yield FileUtils.uploadFileToResultsFolder(response, UtilModels_1.reportZipFileName); - } - } - if (!(0, util_1.isNull)(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { - core.setFailed("TestResult: " + testRunObj.testResult); - return; - } - if (!(0, util_1.isNull)(testRunObj.status) && Util.isStatusFailed(testRunObj.status)) { - console.log("Please go to the Portal for more error details: " + testRunObj.portalUrl); - core.setFailed("TestStatus: " + testRunObj.status); - return; - } - return; - } - else { - if (!Util.isTerminalTestStatus(testStatus)) { - if (testStatus === "DEPROVISIONING" || testStatus === "DEPROVISIONED" || testStatus != "EXECUTED") - yield Util.sleep(5000); - else - yield Util.sleep(20000); - } - } - } - }); - } -} -exports.APISupport = APISupport; diff --git a/lib/models/AuthenticationUtils.js b/lib/models/AuthenticationUtils.js deleted file mode 100644 index 99b98bbc..00000000 --- a/lib/models/AuthenticationUtils.js +++ /dev/null @@ -1,165 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.AuthenticationUtils = void 0; -const util_1 = require("util"); -const core = __importStar(require("@actions/core")); -const child_process_1 = require("child_process"); -const UtilModels_1 = require("./UtilModels"); -const jwt_decode_1 = require("jwt-decode"); -class AuthenticationUtils { - constructor() { - this.dataPlanetoken = ''; - this.controlPlaneToken = ''; - this.subscriptionId = ''; - this.env = 'AzureCloud'; - this.armTokenScope = 'https://management.core.windows.net'; - this.dataPlaneTokenScope = 'https://loadtest.azure-dev.com'; - this.armEndpoint = 'https://management.azure.com'; - this.resourceId = ''; - } - authorize() { - return __awaiter(this, void 0, void 0, function* () { - // NOTE: This will set the subscription id - yield this.getTokenAPI(UtilModels_1.TokenScope.ControlPlane); - const rg = core.getInput('resourceGroup'); - const ltres = core.getInput('loadTestResource'); - if ((0, util_1.isNullOrUndefined)(rg) || rg == '') { - throw new Error(`The input field "resourceGroup" is empty. Provide an existing resource group name.`); - } - if ((0, util_1.isNullOrUndefined)(ltres) || ltres == '') { - throw new Error(`The input field "loadTestResource" is empty. Provide an existing load test resource name.`); - } - this.resourceId = "/subscriptions/" + this.subscriptionId + "/resourcegroups/" + rg + "/providers/microsoft.loadtestservice/loadtests/" + ltres; - yield this.setEndpointAndScope(); - }); - } - setEndpointAndScope() { - return __awaiter(this, void 0, void 0, function* () { - try { - const cmdArguments = ["cloud", "show"]; - var result = yield this.execAz(cmdArguments); - let env = result ? result.name : null; - this.env = env ? env : this.env; - let endpointUrl = (result && result.endpoints) ? result.endpoints.resourceManager : null; - this.armEndpoint = endpointUrl ? endpointUrl : this.armEndpoint; - if (this.env == 'AzureUSGovernment') { - this.dataPlaneTokenScope = 'https://cnt-prod.loadtesting.azure.us'; - this.armTokenScope = 'https://management.usgovcloudapi.net'; - } - } - catch (err) { - const message = `An error occurred while getting credentials from ` + - `Azure CLI for setting endPoint and scope: ${err.message}`; - throw new Error(message); - } - }); - } - getTokenAPI(scope) { - return __awaiter(this, void 0, void 0, function* () { - let tokenScopeDecoded = scope == UtilModels_1.TokenScope.Dataplane ? this.dataPlaneTokenScope : this.armTokenScope; - try { - const cmdArguments = ["account", "get-access-token", "--resource"]; - cmdArguments.push(tokenScopeDecoded); - var result = yield this.execAz(cmdArguments); - let token = result.accessToken; - // NOTE: Setting the subscription id - this.subscriptionId = result.subscription; - scope == UtilModels_1.TokenScope.ControlPlane ? this.controlPlaneToken = token : this.dataPlanetoken = token; - return token; - } - catch (err) { - const message = `An error occurred while getting credentials from ` + `Azure CLI: ${err.message}`; - throw new Error(message); - } - }); - } - execAz(cmdArguments) { - return __awaiter(this, void 0, void 0, function* () { - const azCmd = process.platform === "win32" ? "az.cmd" : "az"; - return new Promise((resolve, reject) => { - (0, child_process_1.execFile)(azCmd, [...cmdArguments, "--out", "json"], { encoding: "utf8", shell: true }, (error, stdout) => { - if (error) { - return reject(error); - } - try { - return resolve(JSON.parse(stdout)); - } - catch (err) { - const msg = `An error occurred while parsing the output "${stdout}", of ` + - `the cmd az "${cmdArguments}": ${err.message}.`; - return reject(new Error(msg)); - } - }); - }); - }); - } - isValid(scope) { - let token = scope == UtilModels_1.TokenScope.Dataplane ? this.dataPlanetoken : this.controlPlaneToken; - try { - let header = token && (0, jwt_decode_1.jwtDecode)(token); - const now = Math.floor(Date.now() / 1000); - return (header && (header === null || header === void 0 ? void 0 : header.exp) && header.exp + 2 > now); - } - catch (error) { - console.log("Error in getting the token"); - } - } - getDataPlaneHeader(apicallType) { - 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; - }); - } - 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 deleted file mode 100644 index bee2c205..00000000 --- a/lib/models/FetchHelper.js +++ /dev/null @@ -1,86 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.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")); -// (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 == 'get') { - httpResponse = yield httpClient.get(urlSuffix, header); - } - else if (method == 'del') { - httpResponse = yield httpClient.del(urlSuffix, header); - } - else if (method == 'put' && isUploadCall) { - let fileContent = (0, FileUtils_1.uploadFileData)(data); - httpResponse = yield httpClient.request(method, urlSuffix, fileContent, header); - } - else { - httpResponse = yield httpClient.request(method, urlSuffix, data, header); - } - if (httpResponse.message.statusCode != undefined && httpResponse.message.statusCode >= 300) { - core.debug(`correlation id : ${correlationId}`); - } - if (httpResponse.message.statusCode != undefined && [408, 429, 502, 503, 504].includes(httpResponse.message.statusCode)) { - let err = yield (0, util_1.getResultObj)(httpResponse); - throw { message: (err && err.error && err.error.message) ? err.error.message : (0, util_1.ErrorCorrection)(httpResponse) }; // throwing as message to catch it as err.message - } - return httpResponse; - } - catch (err) { - if (retries) { - let sleeptime = (5 - retries) * 1000 + Math.floor(Math.random() * 5001); - if (log) { - console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime / 1000} seconds`); - } - yield (0, util_1.sleep)(sleeptime); - return yield httpClientRetries(urlSuffix, header, method, retries - 1, data); - } - else { - throw new Error(`Operation did not succeed after 3 retries. Pipeline failed with error : ${err.message}`); - } - } - }); -} -exports.httpClientRetries = httpClientRetries; diff --git a/lib/models/FileUtils.js b/lib/models/FileUtils.js deleted file mode 100644 index 5b7c65a2..00000000 --- a/lib/models/FileUtils.js +++ /dev/null @@ -1,97 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -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/PayloadModels.js b/lib/models/PayloadModels.js deleted file mode 100644 index 905c2aea..00000000 --- a/lib/models/PayloadModels.js +++ /dev/null @@ -1,21 +0,0 @@ -"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 deleted file mode 100644 index 03f61cc6..00000000 --- a/lib/models/TaskModels.js +++ /dev/null @@ -1,388 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -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")); -class YamlConfig { - constructor() { - 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: '' }; - let yamlFile = (_a = core.getInput('loadTestConfigFile')) !== null && _a !== void 0 ? _a : ''; - if ((0, util_1.isNullOrUndefined)(yamlFile) || yamlFile == '') { - throw new Error(`The input field "loadTestConfigFile" is empty. Provide the path to load test yaml file.`); - } - let yamlPath = yamlFile; - if (!(pathLib.extname(yamlPath) === ".yaml" || pathLib.extname(yamlPath) === ".yml")) - throw new Error("The Load Test configuration file should be of type .yaml or .yml"); - const config = yaml.load(fs.readFileSync(yamlPath, 'utf8')); - let validConfig = Util.checkValidityYaml(config); - if (!validConfig.valid) { - throw new Error(validConfig.error + ` Refer to the load test YAML syntax at https://learn.microsoft.com/azure/load-testing/reference-test-config-yaml`); - } - this.testId = (_b = config.testId) !== null && _b !== void 0 ? _b : config.testName; - this.testId = this.testId.toLowerCase(); - this.displayName = (_c = config.displayName) !== null && _c !== void 0 ? _c : this.testId; - this.description = config.description; - this.engineInstances = (_d = config.engineInstances) !== null && _d !== void 0 ? _d : 1; - let path = pathLib.dirname(yamlPath); - this.testPlan = pathLib.join(path, config.testPlan); - this.kind = (_e = config.testType) !== null && _e !== void 0 ? _e : TestKind_1.TestKind.JMX; - let framework = EngineUtil.getLoadTestFrameworkModelFromKind(this.kind); - if (config.configurationFiles != undefined) { - var tempconfigFiles = []; - tempconfigFiles = config.configurationFiles; - for (let file of tempconfigFiles) { - if (this.kind == TestKind_1.TestKind.URL && !Util.checkFileType(file, 'csv')) { - throw new Error("Only CSV files are allowed as configuration files for a URL-based test."); - } - file = pathLib.join(path, file); - this.configurationFiles.push(file); - } - ; - } - if (config.zipArtifacts != undefined) { - var tempconfigFiles = []; - tempconfigFiles = config.zipArtifacts; - if (this.kind == TestKind_1.TestKind.URL && tempconfigFiles.length > 0) { - throw new Error("Zip artifacts are not supported for the URL-based test."); - } - for (let file of tempconfigFiles) { - file = pathLib.join(path, file); - this.zipArtifacts.push(file); - } - ; - } - if (config.splitAllCSVs != undefined) { - this.splitAllCSVs = config.splitAllCSVs; - } - if (config.failureCriteria != undefined) { - this.failureCriteria = Util.getPassFailCriteriaFromString(config.failureCriteria); - } - if (config.autoStop != undefined) { - this.autoStop = this.getAutoStopCriteria(config.autoStop); - } - if (config.subnetId != undefined) { - this.subnetId = (config.subnetId); - } - if (config.publicIPDisabled != undefined) { - this.publicIPDisabled = (config.publicIPDisabled); - } - if (config.properties != undefined && config.properties.userPropertyFile != undefined) { - if (this.kind == TestKind_1.TestKind.URL) { - throw new Error("User property file is not supported for the URL-based test."); - } - let propFile = config.properties.userPropertyFile; - this.propertyFile = pathLib.join(path, propFile); - if (!Util.checkFileTypes(config.properties.userPropertyFile, framework.userPropertyFileExtensions)) { - throw new Error(`User property file with extension other than ${framework.ClientResources.userPropertyFileExtensionsFriendly} is not permitted.`); - } - } - if (config.secrets != undefined) { - this.secrets = this.parseParameters(config.secrets, UtilModels_1.ParamType.secrets); - } - if (config.env != undefined) { - this.env = this.parseParameters(config.env, UtilModels_1.ParamType.env); - } - if (config.certificates != undefined) { - this.certificates = this.parseParameters(config.certificates, UtilModels_1.ParamType.cert); - } - if (config.keyVaultReferenceIdentity != undefined || 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); - } - // commenting out for now, will re-write this logic with the changed options. - // if(config.engineBuiltInIdentityType != undefined) { - // engineBuiltInIdentityType = config.engineBuiltInIdentityType; - // } - // if(config.engineBuiltInIdentityIds != undefined) { - // engineBuiltInIdentityIds = config.engineBuiltInIdentityIds; - // } - if (this.testId === '' || (0, util_1.isNullOrUndefined)(this.testId) || this.testPlan === '' || (0, util_1.isNullOrUndefined)(this.testPlan)) { - throw new Error("The required fields testId/testPlan are missing in " + yamlPath + "."); - } - this.runTimeParams = this.getRunTimeParams(); - Util.validateTestRunParamsFromPipeline(this.runTimeParams); - } - 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; - } - } - getRunTimeParams() { - var _a, _b; - var secretRun = core.getInput('secrets'); - let secretsParsed = {}; - let envParsed = {}; - if (secretRun) { - try { - var obj = JSON.parse(secretRun); - for (var index in obj) { - var val = obj[index]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid secret name at pipeline parameters at ${str}`); - } - secretsParsed[val.name] = { type: 'SECRET_VALUE', value: val.value }; - } - } - catch (error) { - console.log(error); - throw new Error("Invalid format of secrets in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); - } - } - var eRun = core.getInput('env'); - if (eRun) { - try { - var obj = JSON.parse(eRun); - for (var index in obj) { - var val = obj[index]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid environment name at pipeline parameters at ${str}`); - } - envParsed[val.name] = val.value; - } - } - catch (error) { - console.log(error); - throw new Error("Invalid format of env in the pipeline yaml file. Refer to the pipeline YAML syntax at : https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-secured-endpoints?tabs=pipelines#reference-the-secret-in-the-load-test-configuration"); - } - } - const runDisplayName = (_a = core.getInput('loadTestRunName')) !== null && _a !== void 0 ? _a : Util.getDefaultTestRunName(); - const runDescription = (_b = core.getInput('loadTestRunDescription')) !== null && _b !== void 0 ? _b : Util.getDefaultRunDescription(); - let runTimeParams = { env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: '' }; - return runTimeParams; - } - getFileName(filepath) { - var filename = pathLib.basename(filepath); - return filename; - } - mergeExistingData(existingData) { - let existingCriteria = existingData.passFailCriteria; - let existingCriteriaIds = Object.keys(existingCriteria); - var numberOfExistingCriteria = existingCriteriaIds.length; - var index = 0; - for (var key in this.failureCriteria) { - var splitted = key.split(" "); - var criteriaId = index < numberOfExistingCriteria ? existingCriteriaIds[index++] : Util.getUniqueId(); - this.passFailApiModel[criteriaId] = { - clientMetric: splitted[0], - aggregate: splitted[1], - condition: splitted[2], - action: splitted[3], - value: this.failureCriteria[key], - requestName: splitted.length > 4 ? splitted.slice(4).join(' ') : null - }; - } - for (; index < numberOfExistingCriteria; index++) { - this.passFailApiModel[existingCriteriaIds[index]] = null; - } - let existingParams = existingCriteria.secrets; - for (var key in existingParams) { - if (!this.secrets.hasOwnProperty(key)) - this.secrets[key] = null; - } - var existingEnv = existingCriteria.env; - for (var key in existingEnv) { - if (!this.env.hasOwnProperty(key)) - this.env[key] = null; - } - } - getCreateTestData(existingData) { - this.mergeExistingData(existingData); - var data = { - testId: this.testId, - description: this.description, - displayName: this.displayName, - loadTestConfiguration: { - engineInstances: this.engineInstances, - splitAllCSVs: this.splitAllCSVs, - regionalLoadTestConfig: this.regionalLoadTestConfig, - }, - secrets: this.secrets, - kind: this.kind, - certificate: this.certificates, - environmentVariables: this.env, - passFailCriteria: { - passFailMetrics: this.passFailApiModel - }, - autoStopCriteria: this.autoStop, - subnetId: this.subnetId, - publicIPDisabled: this.publicIPDisabled, - keyvaultReferenceIdentityType: this.keyVaultReferenceIdentityType, - keyvaultReferenceIdentityId: this.keyVaultReferenceIdentity, - 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(); - return this.runTimeParams; - } - getAutoStopCriteria(autoStopInput) { - let autoStop; - if (autoStopInput == null) { - autoStop = null; - return autoStop; - } - if (typeof autoStopInput == "string") { - if (autoStopInput == "disable") { - let data = { - autoStopDisabled: true, - errorRate: 90, - errorRateTimeWindowInSeconds: 60, - }; - autoStop = data; - } - else { - throw new Error("Invalid value, for disabling auto stop use 'autoStop: disable'"); - } - } - else { - let data = { - autoStopDisabled: false, - errorRate: autoStopInput.errorPercentage, - errorRateTimeWindowInSeconds: autoStopInput.timeWindow, - }; - autoStop = data; - } - return autoStop; - } - parseParameters(obj, type) { - if (type == UtilModels_1.ParamType.secrets) { - let secretsParsed = {}; - for (var index in obj) { - var val = obj[index]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid secret name at ${str}`); - } - if (!Util.validateUrl(val.value)) { - throw new Error(`Invalid secret url at ${str}`); - } - secretsParsed[val.name] = { type: 'AKV_SECRET_URI', value: val.value }; - } - return secretsParsed; - } - if (type == UtilModels_1.ParamType.env) { - let envParsed = {}; - for (var index in obj) { - let val = obj[index]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid environment name at ${str}`); - } - val = obj[index]; - envParsed[val.name] = val.value; - } - return envParsed; - } - if (type == UtilModels_1.ParamType.cert) { - let cert = null; - if (obj.length > 1) { - throw new Error(`Only one certificate can be added in the load test configuration.`); - } - if (obj.length == 1) { - let val = obj[0]; - let str = `name : ${val.name}, value : ${val.value}`; - if ((0, util_1.isNullOrUndefined)(val.name)) { - throw new Error(`Invalid certificate name at ${str}`); - } - if (!Util.validateUrlcert(val.value)) - throw new Error(`Invalid certificate url at ${str}`); - cert = { name: val.name, type: 'AKV_CERT_URI', value: val.value }; - } - return cert; - } - return null; - } - getMultiRegionLoadTestConfig(multiRegionalConfig) { - let parsedMultiRegionConfiguration = []; - multiRegionalConfig.forEach(regionConfig => { - let data = { - region: regionConfig.region, - engineInstances: regionConfig.engineInstances, - }; - parsedMultiRegionConfiguration.push(data); - }); - return parsedMultiRegionConfiguration; - } -} -exports.YamlConfig = YamlConfig; diff --git a/lib/models/UtilModels.js b/lib/models/UtilModels.js deleted file mode 100644 index de0dd104..00000000 --- a/lib/models/UtilModels.js +++ /dev/null @@ -1,72 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ManagedIdentityType = exports.ValidConditionList = exports.ValidAggregateList = exports.ApiVersionConstants = exports.correlationHeader = exports.resultZipFileName = exports.reportZipFileName = exports.resultFolder = exports.FileType = exports.ContentTypeMap = exports.CallTypeForDP = exports.TokenScope = exports.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 CallTypeForDP; -(function (CallTypeForDP) { - CallTypeForDP[CallTypeForDP["get"] = 0] = "get"; - CallTypeForDP[CallTypeForDP["patch"] = 1] = "patch"; - CallTypeForDP[CallTypeForDP["put"] = 2] = "put"; - CallTypeForDP[CallTypeForDP["delete"] = 3] = "delete"; -})(CallTypeForDP = exports.CallTypeForDP || (exports.CallTypeForDP = {})); -exports.ContentTypeMap = { - [CallTypeForDP.get]: null, - [CallTypeForDP.patch]: 'application/merge-patch+json', - [CallTypeForDP.put]: 'application/octet-stream', - [CallTypeForDP.delete]: 'application/json' -}; -var FileType; -(function (FileType) { - FileType["JMX_FILE"] = "JMX_FILE"; - FileType["USER_PROPERTIES"] = "USER_PROPERTIES"; - FileType["ADDITIONAL_ARTIFACTS"] = "ADDITIONAL_ARTIFACTS"; - FileType["ZIPPED_ARTIFACTS"] = "ZIPPED_ARTIFACTS"; - FileType["URL_TEST_CONFIG"] = "URL_TEST_CONFIG"; - FileType["TEST_SCRIPT"] = "TEST_SCRIPT"; -})(FileType = exports.FileType || (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.tm2024Version = '2024-05-01-preview'; - ApiVersionConstants.tm2023Version = '2023-04-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 = {})); diff --git a/lib/models/constants.js b/lib/models/constants.js deleted file mode 100644 index d6327bf6..00000000 --- a/lib/models/constants.js +++ /dev/null @@ -1,72 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APIRoute = exports.testmanagerApiVersion = exports.defaultYaml = void 0; -exports.defaultYaml = { - version: 'v0.1', - testId: 'SampleTest', - testName: 'SampleTest', - displayName: 'Sample Test', - 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' } - ], - 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" - } - ] -}; -exports.testmanagerApiVersion = "2024-07-01-preview"; -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 = {})); diff --git a/lib/models/engine/BaseLoadTestFrameworkModel.js b/lib/models/engine/BaseLoadTestFrameworkModel.js deleted file mode 100644 index c8ad2e54..00000000 --- a/lib/models/engine/BaseLoadTestFrameworkModel.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/models/engine/JMeterFrameworkModel.js b/lib/models/engine/JMeterFrameworkModel.js deleted file mode 100644 index 26b71211..00000000 --- a/lib/models/engine/JMeterFrameworkModel.js +++ /dev/null @@ -1,40 +0,0 @@ -"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 deleted file mode 100644 index 6f4cd4df..00000000 --- a/lib/models/engine/LocustFrameworkModel.js +++ /dev/null @@ -1,40 +0,0 @@ -"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 deleted file mode 100644 index 0b773c6b..00000000 --- a/lib/models/engine/TestKind.js +++ /dev/null @@ -1,12 +0,0 @@ -"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 deleted file mode 100644 index 4a9cd9bb..00000000 --- a/lib/models/engine/Util.js +++ /dev/null @@ -1,103 +0,0 @@ -"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 deleted file mode 100644 index e5573d0e..00000000 --- a/lib/models/util.js +++ /dev/null @@ -1,664 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getAllFileErrors = exports.validateTestRunParamsFromPipeline = exports.getDefaultRunDescription = exports.getDefaultTestRunName = exports.getDefaultTestName = exports.validateUrlcert = exports.validateUrl = exports.ValidateCriteriaAndConvertToWorkingStringModel = exports.getPassFailCriteriaFromString = exports.validateAndGetSegregatedManagedIdentities = exports.checkValidityYaml = 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"); -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 inValidEngineInstances(engines) { - if (engines > 400 || engines < 1) { - return true; - } - return false; -} -function checkValidityYaml(givenYaml) { - var _a, _b; - if (!isDictionary(givenYaml)) { - return { valid: false, error: `Invalid YAML syntax.` }; - } - let unSupportedKeys = []; - let supportedKeys = Object.keys(constants_1.defaultYaml); - 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.autoStop) { - if (typeof givenYaml.autoStop != 'string') { - if ((0, util_1.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 ((0, util_1.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.` }; - } - } - else if (givenYaml.autoStop != "disable") { - return { valid: false, error: 'Invalid value for "autoStop", for disabling auto stop use "autoStop: disable"' }; - } - } - 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 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("KeyVault reference identity should not be provided in the referenceIdentities array if keyVaultReferenceIdentity is provided."); - } - // 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 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: "" }; -} -/* - 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() { - return "Started using GitHub Actions"; -} -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/services/FeatureFlagService.js b/lib/services/FeatureFlagService.js deleted file mode 100644 index 8c7592b1..00000000 --- a/lib/services/FeatureFlagService.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.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.CallTypeForDP.get); - let flagResponse = yield FetchUtil.httpClientRetries(uri, headers, 'get', 3, "", false, false); - try { - let flagObj = (yield util.getResultObj(flagResponse)); - this.featureFlagCache[flag.toString()] = flagObj.enabled; - 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 deleted file mode 100644 index 67ccad90..00000000 --- a/lib/services/FeatureFlags.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.FeatureFlags = void 0; -var FeatureFlags; -(function (FeatureFlags) { - FeatureFlags["enableTestScriptFragments"] = "enableTestScriptFragments"; -})(FeatureFlags = exports.FeatureFlags || (exports.FeatureFlags = {})); From 481a6a12b602310a361abc12c9ac2c31007d792f Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 00:58:31 +0530 Subject: [PATCH 03/29] adding js files to git ignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 71397229..1855c466 100644 --- a/.gitignore +++ b/.gitignore @@ -334,3 +334,4 @@ node_modules/ .vscode TmpFiles loadTest +lib \ No newline at end of file From b8bfb4f1625f2df8ff6b109f21d603ba8a874d3c Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 01:29:41 +0530 Subject: [PATCH 04/29] Your commit message --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1855c466..7ce7a18b 100644 --- a/.gitignore +++ b/.gitignore @@ -334,4 +334,4 @@ node_modules/ .vscode TmpFiles loadTest -lib \ No newline at end of file +lib From efaebc882e2abe4e9e5c96ac697ec0e749def725 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 01:32:25 +0530 Subject: [PATCH 05/29] adding tsc in the pipeline. --- .github/workflows/pr_check_load_test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr_check_load_test.yml b/.github/workflows/pr_check_load_test.yml index ef39205f..f62e2533 100644 --- a/.github/workflows/pr_check_load_test.yml +++ b/.github/workflows/pr_check_load_test.yml @@ -53,6 +53,7 @@ jobs: run: | npm install --include=dev -f npm ci + tsc - name: Azure authentication uses: azure/login@v1 From 10a8e48db91b8799a824f58bcfe0a3f4cc720712 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 01:35:58 +0530 Subject: [PATCH 06/29] adding the npm install typescript seperately. --- .github/workflows/pr_check_load_test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr_check_load_test.yml b/.github/workflows/pr_check_load_test.yml index f62e2533..21d24c67 100644 --- a/.github/workflows/pr_check_load_test.yml +++ b/.github/workflows/pr_check_load_test.yml @@ -52,6 +52,7 @@ jobs: - name: Installing dependencies and building latest changes run: | npm install --include=dev -f + npm install typescript -g npm ci tsc From 9ffdb35c9d59d1d8d2386e05fd1e24e6084e0f07 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 01:38:14 +0530 Subject: [PATCH 07/29] adding a commment. --- .github/workflows/pr_check_load_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_check_load_test.yml b/.github/workflows/pr_check_load_test.yml index 21d24c67..2a1973dd 100644 --- a/.github/workflows/pr_check_load_test.yml +++ b/.github/workflows/pr_check_load_test.yml @@ -52,10 +52,10 @@ jobs: - name: Installing dependencies and building latest changes run: | npm install --include=dev -f - npm install typescript -g 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 From d2b3468a039fc92cd6ec55feba5cec64322c3e3c Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 01:49:02 +0530 Subject: [PATCH 08/29] reverting the print statements from portal to normal prints. --- src/models/APISupport.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index f780280e..dc16cc79 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -15,6 +15,8 @@ export class APISupport { baseURL = ''; existingParams: ExistingParams = {secrets: {}, env: {}, passFailCriteria: {}}; testId: string; + resourceName: string | undefined; + subName: string | undefined; constructor(authContext: AuthenticationUtils, yamlModel: YamlConfig) { this.authContext = authContext; @@ -30,6 +32,8 @@ export class APISupport { 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; 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); @@ -314,10 +318,12 @@ export class APISupport { throw new Error("Error in running the test"); } let startTime = new Date(); - let portalUrl = testRunDao.portalUrl; let status = testRunDao.status; if(status == "ACCEPTED") { - console.log("View the load test run in progress at: "+ portalUrl) + 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("3. Go to test run '"+testRunDao.displayName+"'\n"); await this.getTestRunAPI(testRunId, status, startTime); } } From 9746ca2d378c897b5559b49447b4a3b70bc16275 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 02:07:00 +0530 Subject: [PATCH 09/29] phase2 changes for app components and server metricss config. --- src/models/APISupport.ts | 237 +++++++++----- src/models/PayloadModels.ts | 27 ++ src/models/TaskModels.ts | 91 +++++- src/models/UtilModels.ts | 14 +- src/models/constants.ts | 31 ++ src/models/util.ts | 91 +++++- .../AppComponentsAndServerConfigYamls.ts | 307 ++++++++++++++++++ test/Utils/checkForValidationOfYaml.test.ts | 42 +++ 8 files changed, 746 insertions(+), 94 deletions(-) create mode 100644 test/Utils/AppComponentsAndServerConfigYamls.ts diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index dc16cc79..cd9386a4 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -5,7 +5,7 @@ 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'; @@ -13,10 +13,8 @@ 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; @@ -32,8 +30,6 @@ export class APISupport { 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; 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,7 +44,7 @@ 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,""); @@ -62,8 +58,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 +97,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(CallTypeForDP.get); + let appComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,'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(CallTypeForDP.get); + let serverComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,'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,""); 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)); 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 +201,114 @@ 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(CallTypeForDP.patch); + let appComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,'patch',3,JSON.stringify(appComponentsData)); + 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"); + 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]); + } + } + 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 + } + let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.patch); + let serverMetricsResult = await FetchUtil.httpClientRetries(urlSuffix,header,'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(CallTypeForDP.put) + let uploadresult = await FetchUtil.httpClientRetries(urlSuffix,headers,'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); + } + await this.patchAppComponents(); + 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.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 +317,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); 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 +339,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); 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 +361,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); 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,7 +379,7 @@ 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; console.log("Creating and running a testRun for the test"); @@ -321,7 +394,7 @@ export class APISupport { 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("1. Go to your Azure Load Testing resource '"+Util.getResourceNameFromResourceId(this.authContext.resourceId)+"' in subscription '"+Util.getSubscriptionIdFromResourceId(this.authContext.resourceId)+"'") console.log("2. On the Tests page, go to test '"+this.testId+"'") console.log("3. Go to test run '"+testRunDao.displayName+"'\n"); await this.getTestRunAPI(testRunId, status, startTime); @@ -336,7 +409,7 @@ 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)) { diff --git a/src/models/PayloadModels.ts b/src/models/PayloadModels.ts index c4f1352f..599ed295 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; @@ -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..ffea4b57 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -6,8 +6,8 @@ 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 } from "./PayloadModels"; +import { AllManagedIdentitiesSegregated, AutoStopCriteriaObjYaml, ParamType, ReferenceIdentityKinds, RunTimeParams, ServerMetricsClientModel } from "./UtilModels"; import * as core from '@actions/core'; import { PassFailMetric, ExistingParams, TestModel, CertificateMetadata, SecretMetadata, RegionConfiguration } from "./PayloadModels"; @@ -43,6 +43,11 @@ export class YamlConfig { regionalLoadTestConfig: RegionConfiguration[] | null = null; runTimeParams: RunTimeParams = {env: {}, secrets: {}, runDisplayName: '', runDescription: '', testId: '', testRunId: ''}; + 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. + constructor() { let yamlFile = core.getInput('loadTestConfigFile') ?? ''; if(isNullOrUndefined(yamlFile) || yamlFile == ''){ @@ -125,6 +130,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 +148,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 +156,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); @@ -255,6 +298,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) { diff --git a/src/models/UtilModels.ts b/src/models/UtilModels.ts index 64e7777b..ab04afe9 100644 --- a/src/models/UtilModels.ts +++ b/src/models/UtilModels.ts @@ -66,8 +66,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 +92,18 @@ 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 } +} + +export interface ValidationModel { + valid: boolean; + error: string; } \ No newline at end of file diff --git a/src/models/constants.ts b/src/models/constants.ts index 7a39654d..6ee71051 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -32,6 +32,37 @@ export const defaultYaml: any = 'percentage(error) > 50', { GetCustomerDetails: 'avg(latency) >200' } ], + appComponents: [ + { + resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sampleApp", + 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.KeyVault/vaults/sampleApp", + metrics:[ + { + name: "ServiceApiHit", + aggregation: "Count", + namespace: "Microsoft.KeyVault/vaults" + }, + { + name: "ServiceApiLatency", + aggregation: "Average" + } + ] + } + ], autoStop: { errorPercentage: 80, timeWindow: 60 }, keyVaultReferenceIdentity: '/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/sample-identity', keyVaultReferenceIdentityType: 'SystemAssigned', diff --git a/src/models/util.ts b/src/models/util.ts index 40fdd778..cdc89ae5 100644 --- a/src/models/util.ts +++ b/src/models/util.ts @@ -6,7 +6,7 @@ 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'; export function checkFileType(filePath: string, fileExtToValidate: string): boolean{ if(isNullOrUndefined(filePath)){ @@ -266,6 +266,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,6 +280,27 @@ 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.`}; @@ -383,6 +411,15 @@ 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.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 = validateAppComponentAndServerComponents(givenYaml.appComponents); + if(validationAppComponents.valid == false){ + return validationAppComponents; + } + } 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) { @@ -472,6 +509,58 @@ export function validateAndGetSegregatedManagedIdentities(referenceIdentities: { } return {referenceIdentityValuesUAMIMap, referenceIdentiesSystemAssignedCount}; } +function validateAppComponentAndServerComponents(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){ 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..2ac01a7b 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', ()=>{ @@ -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 From da21f25efef4ea48e787c167c69dae4498890c1f Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 09:11:56 +0530 Subject: [PATCH 10/29] [phase3: CICD] adding over-ride params and outputVar support in the PipleineYaml. --- action.yml | 7 ++ src/main.ts | 7 +- src/models/APISupport.ts | 19 +-- src/models/AuthenticationUtils.ts | 9 +- src/models/InputConstants.ts | 22 ++++ src/models/TaskModels.ts | 69 +++++++++-- src/models/UtilModels.ts | 8 ++ src/models/constants.ts | 129 +++++++------------- src/models/util.ts | 111 ++++++++++++++--- test/Utils/checkForValidationOfYaml.test.ts | 2 +- 10 files changed, 252 insertions(+), 131 deletions(-) create mode 100644 src/models/InputConstants.ts diff --git a/action.yml b/action.yml index 6f01bfb5..cda482a0 100644 --- a/action.yml +++ b/action.yml @@ -22,6 +22,13 @@ 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' diff --git a/src/main.ts b/src/main.ts index 83d9fc40..16d992aa 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, resultFolder } from "./models/UtilModels"; import * as fs from 'fs'; import * as core from '@actions/core'; import { AuthenticationUtils } from "./models/AuthenticationUtils"; @@ -22,7 +22,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 cd9386a4..1e4a8e75 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -8,6 +8,7 @@ import * as core from '@actions/core'; 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; @@ -29,7 +30,7 @@ export class APISupport { 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'); + 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); @@ -214,17 +215,7 @@ export class APISupport { throw new Error("Error in updating app components"); } else { console.log("Updated app components successfully"); - 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]); - } - } + await this.getServerMetricsConfig(); await this.patchServerMetrics(); } @@ -286,7 +277,6 @@ export class APISupport { } await Util.sleep(5000); } - await this.patchAppComponents(); console.log("Validation status of the test plan: "+ validationStatus); if(validationStatus == null || validationStatus == "VALIDATION_SUCCESS" ){ console.log(`Validated test plan for the test successfully.`); @@ -301,7 +291,8 @@ export class APISupport { } throw new Error("Validation of one or more files failed. Please correct the errors and try again."); } - + + await this.patchAppComponents(); await this.createTestRun(); } else if(validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED") diff --git a/src/models/AuthenticationUtils.ts b/src/models/AuthenticationUtils.ts index 7bd631df..6bed3b4f 100644 --- a/src/models/AuthenticationUtils.ts +++ b/src/models/AuthenticationUtils.ts @@ -4,6 +4,7 @@ import { execFile } from "child_process"; import { CallTypeForDP, 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 = ''; @@ -22,13 +23,13 @@ export class AuthenticationUtils { // 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'); + 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; 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/TaskModels.ts b/src/models/TaskModels.ts index ffea4b57..19844a16 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -7,9 +7,11 @@ import { BaseLoadTestFrameworkModel } from "./engine/BaseLoadTestFrameworkModel" const yaml = require('js-yaml'); import * as fs from 'fs'; import { AppComponentDefinition, AppComponents, AutoStopCriteria, AutoStopCriteria as autoStopCriteriaObjOut, ManagedIdentityTypeForAPI, ResourceMetricModel, ServerMetricConfig } from "./PayloadModels"; -import { AllManagedIdentitiesSegregated, AutoStopCriteriaObjYaml, ParamType, ReferenceIdentityKinds, RunTimeParams, ServerMetricsClientModel } from "./UtilModels"; +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 = ''; @@ -47,18 +49,19 @@ export class YamlConfig { 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() { - let yamlFile = core.getInput('loadTestConfigFile') ?? ''; + 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`); } @@ -188,7 +191,7 @@ export class YamlConfig { id: key } } else { - this.serverMetricsConfig[key].aggregation = this.serverMetricsConfig[key].aggregation + "," + serverComponent.aggregation; + this.serverMetricsConfig[key]!.aggregation = this.serverMetricsConfig[key]!.aggregation + "," + serverComponent.aggregation; } } } @@ -214,8 +217,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) { @@ -232,10 +263,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); @@ -250,13 +281,31 @@ 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 runTimeParams : RunTimeParams = {env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: ''}; + + let overRideParams = core.getInput(InputConstants.overRideParameters); + let outputVarName = core.getInput(InputConstants.outputVariableName) ?? OutputVariableName; + + 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; } @@ -371,7 +420,7 @@ export class YamlConfig { 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 ab04afe9..cc90b0ba 100644 --- a/src/models/UtilModels.ts +++ b/src/models/UtilModels.ts @@ -106,4 +106,12 @@ export interface AllManagedIdentitiesSegregated { export interface ValidationModel { valid: boolean; error: string; +} + +export interface OutputVariableInterface { + testRunId: string; +} + +export module OutPutVariablesConstants { + export const testRunId = 'testRunId'; } \ No newline at end of file diff --git a/src/models/constants.ts b/src/models/constants.ts index 6ee71051..8e209f93 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -1,97 +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' } - ], - appComponents: [ - { - resourceId: "/subscriptions/abcdef01-2345-6789-0abc-def012345678/resourceGroups/sample-rg/providers/Microsoft.Web/serverfarms/sampleApp", - 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.KeyVault/vaults/sampleApp", - metrics:[ - { - name: "ServiceApiHit", - aggregation: "Count", - namespace: "Microsoft.KeyVault/vaults" - }, - { - name: "ServiceApiLatency", - aggregation: "Average" - } - ] - } - ], + 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"; } @@ -100,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 cdc89ae5..a728828d 100644 --- a/src/models/util.ts +++ b/src/models/util.ts @@ -1,12 +1,13 @@ 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, ValidationModel } from './UtilModels'; +import * as InputConstants from './InputConstants'; export function checkFileType(filePath: string, fileExtToValidate: string): boolean{ if(isNullOrUndefined(filePath)){ @@ -306,7 +307,7 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s 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); @@ -415,22 +416,15 @@ export function checkValidityYaml(givenYaml : any) : {valid : boolean, error : s 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 = validateAppComponentAndServerComponents(givenYaml.appComponents); + let validationAppComponents = validateAppComponentAndServerMetricsConfig(givenYaml.appComponents); if(validationAppComponents.valid == false){ return validationAppComponents; } } 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.`}; - } - } - else if(givenYaml.autoStop != "disable"){ - return {valid : false, error : 'Invalid value for "autoStop", for disabling auto stop use "autoStop: disable"'}; + let validation = validateAutoStop(givenYaml.autoStop); + if(validation.valid == false){ + return validation; } } if(givenYaml.regionalLoadTestConfig){ @@ -460,6 +454,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[] } = { @@ -486,7 +504,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. } @@ -509,7 +527,8 @@ export function validateAndGetSegregatedManagedIdentities(referenceIdentities: { } return {referenceIdentityValuesUAMIMap, referenceIdentiesSystemAssignedCount}; } -function validateAppComponentAndServerComponents(appComponents: Array) : ValidationModel { + +function validateAppComponentAndServerMetricsConfig(appComponents: Array) : ValidationModel { let appComponentsParsed = appComponents; for(let i = 0; i < appComponentsParsed.length; i++){ if(!isDictionary(appComponentsParsed[i])){ @@ -592,6 +611,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, */ diff --git a/test/Utils/checkForValidationOfYaml.test.ts b/test/Utils/checkForValidationOfYaml.test.ts index 2ac01a7b..8292e9cc 100644 --- a/test/Utils/checkForValidationOfYaml.test.ts +++ b/test/Utils/checkForValidationOfYaml.test.ts @@ -206,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', () => { From 059935ef4c95d2c63e235fa93d001c1285c89ec1 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 09:45:42 +0530 Subject: [PATCH 11/29] [phase4: CICD] adding the post process job for cancellation. --- action.yml | 1 + src/main.ts | 3 +- src/models/APISupport.ts | 89 +++++++++++++++++------------- src/models/AuthenticationUtils.ts | 4 +- src/models/FetchHelper.ts | 39 +++++++++---- src/models/PayloadModels.ts | 8 +-- src/models/TaskModels.ts | 23 ++++++-- src/models/UtilModels.ts | 24 +++++--- src/models/util.ts | 4 +- src/postProcessJob.ts | 26 +++++++++ src/services/FeatureFlagService.ts | 7 ++- 11 files changed, 154 insertions(+), 74 deletions(-) create mode 100644 src/postProcessJob.ts diff --git a/action.yml b/action.yml index cda482a0..fdcaf7a6 100644 --- a/action.yml +++ b/action.yml @@ -35,3 +35,4 @@ branding: runs: using: 'node20' main: 'lib/main.js' + post: 'lib/postProcessJob.js' diff --git a/src/main.ts b/src/main.ts index 16d992aa..40dbcac0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import * as util from './models/FileUtils'; -import { OutputVariableInterface, OutPutVariablesConstants, 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.setOutput(PostTaskParameters.baseUri, apiSupport.baseURL); await apiSupport.getTestAPI(false); if (fs.existsSync(resultFolder)){ util.deleteFile(resultFolder); diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index 1e4a8e75..327948a9 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -1,6 +1,6 @@ import { isNull, 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'; @@ -29,7 +29,7 @@ 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 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.`; @@ -47,8 +47,8 @@ export class APISupport { async getTestAPI(validate:boolean, returnTestObj:boolean = false) : Promise<[string | undefined, TestModel] | string | undefined> { 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 " @@ -102,8 +102,8 @@ 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(CallTypeForDP.get); - let appComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,'get',3,""); + 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){ @@ -122,8 +122,8 @@ export class APISupport { async getServerMetricsConfig() { let urlSuffix = "tests/"+this.testId+"/server-metrics-config/"+"?api-version="+ ApiVersionConstants.latestVersion; urlSuffix = this.baseURL+urlSuffix; - let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.get); - let serverComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,'get',3,""); + 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); @@ -133,8 +133,8 @@ export class APISupport { async deleteFileAPI(filename:string) { 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 errorObj:any=await Util.getResultObj(delFileResult); let Message: string = errorObj ? errorObj.message : Util.ErrorCorrection(delFileResult); @@ -146,8 +146,8 @@ export class APISupport { 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 errorObj:any=await Util.getResultObj(createTestresult); console.log(errorObj ? errorObj : Util.ErrorCorrection(createTestresult)); @@ -207,8 +207,11 @@ export class APISupport { 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(CallTypeForDP.patch); - let appComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,'patch',3,JSON.stringify(appComponentsData)); + let header = await this.authContext.getDataPlaneHeader(FetchCallType.patch); + let appComponentsResult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.patch,3,JSON.stringify(appComponentsData)); + if(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)); @@ -227,8 +230,11 @@ export class APISupport { let serverMetricsData : ServerMetricConfig = { metrics: this.yamlModel.serverMetricsConfig } - let header = await this.authContext.getDataPlaneHeader(CallTypeForDP.patch); - let serverMetricsResult = await FetchUtil.httpClientRetries(urlSuffix,header,'patch',3,JSON.stringify(serverMetricsData)); + if(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)); @@ -251,8 +257,8 @@ export class APISupport { } 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); + 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)); @@ -291,7 +297,7 @@ export class APISupport { } throw new Error("Validation of one or more files failed. Please correct the errors and try again."); } - + await this.patchAppComponents(); await this.createTestRun(); } @@ -310,8 +316,8 @@ export class APISupport { let filename = this.yamlModel.getFileName(filepath); 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 errorObj:any = await Util.getResultObj(uploadresult); console.log(errorObj ? errorObj : Util.ErrorCorrection(uploadresult)); @@ -332,8 +338,8 @@ export class APISupport { let filename = this.yamlModel.getFileName(filepath); 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 errorObj:any = await Util.getResultObj(uploadresult); console.log(errorObj ? errorObj : Util.ErrorCorrection(uploadresult)); @@ -354,8 +360,8 @@ export class APISupport { let filename = this.yamlModel.getFileName(propertyFile); 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 errorObj:any = await Util.getResultObj(uploadresult); console.log(errorObj ? errorObj : Util.ErrorCorrection(uploadresult)); @@ -372,10 +378,10 @@ export class APISupport { const testRunId = this.yamlModel.runTimeParams.testRunId; let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.latestVersion; urlSuffix = this.baseURL+urlSuffix; - + core.setTaskVariable(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)); @@ -404,14 +410,14 @@ export class APISupport { 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; @@ -420,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)); @@ -449,10 +455,10 @@ export class APISupport { Util.printCriteria(testRunObj.passFailCriteria.passFailMetrics) if(testRunObj.testRunStatistics != null && testRunObj.testRunStatistics != undefined) Util.printClientMetrics(testRunObj.testRunStatistics); - + core.setTaskVariable(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)); @@ -464,7 +470,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)); @@ -475,11 +481,11 @@ export class APISupport { } } - if(!isNull(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { + if(!isNull(testRunObj.testResult) && !isNullOrUndefined(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { core.setFailed("TestResult: "+ testRunObj.testResult); return; } - if(!isNull(testRunObj.status) && Util.isStatusFailed(testRunObj.status)) { + if(!isNull(testRunObj.status) && !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; @@ -498,4 +504,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 6bed3b4f..28472acd 100644 --- a/src/models/AuthenticationUtils.ts +++ b/src/models/AuthenticationUtils.ts @@ -1,7 +1,7 @@ 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"; @@ -111,7 +111,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; diff --git a/src/models/FetchHelper.ts b/src/models/FetchHelper.ts index 0759dfc1..44cb76d7 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 organization = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI; + const project = process.env.SYSTEM_TEAMPROJECT; + const buildId = process.env.BUILD_BUILDID; + console.log(organization, project, buildId); + const pipelineName = tl.getVariable("Build.DefinitionName") || tl.getVariable("Release.DefinitionName") || "Unavailable"; + const pipelineUrl = `${organization}${project}/_build/results?buildId=${buildId}`; + + header['x-ms-pipelineUrl'] = pipelineUrl; + header['x-ms-pipelineName'] = pipelineName; // setting these for patch calls. + console.log(pipelineUrl, pipelineName, tl.getVariable("Build.DefinitionName"), tl.getVariable("Release.DefinitionName")); + 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/PayloadModels.ts b/src/models/PayloadModels.ts index 599ed295..b33996fb 100644 --- a/src/models/PayloadModels.ts +++ b/src/models/PayloadModels.ts @@ -86,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; diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts index 19844a16..6b793ccc 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -6,8 +6,8 @@ import { TestKind } from "./engine/TestKind"; import { BaseLoadTestFrameworkModel } from "./engine/BaseLoadTestFrameworkModel"; const yaml = require('js-yaml'); import * as fs from 'fs'; -import { AppComponentDefinition, AppComponents, AutoStopCriteria, AutoStopCriteria as autoStopCriteriaObjOut, ManagedIdentityTypeForAPI, ResourceMetricModel, ServerMetricConfig } from "./PayloadModels"; -import { AllManagedIdentitiesSegregated, AutoStopCriteriaObjYaml, ParamType, ReferenceIdentityKinds, RunTimeParams, ServerMetricsClientModel, ValidationModel } 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"; @@ -51,7 +51,10 @@ export class YamlConfig { 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() { + constructor(isPostProcess: boolean = false) { + if(isPostProcess) { + return; + } let yamlFile = core.getInput(InputConstants.loadTestConfigFile) ?? ''; if(isNullOrUndefined(yamlFile) || yamlFile == ''){ throw new Error(`The input field "${InputConstants.loadTestConfigFileLabel}" is empty. Provide the path to load test yaml file.`); @@ -288,7 +291,7 @@ export class YamlConfig { const runDescription = core.getInput('loadTestRunDescription') ?? Util.getDefaultRunDescription(); let runTimeParams : RunTimeParams = {env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: ''}; - + this.runTimeParams = runTimeParams; let overRideParams = core.getInput(InputConstants.overRideParameters); let outputVarName = core.getInput(InputConstants.outputVariableName) ?? OutputVariableName; @@ -410,10 +413,18 @@ 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 { diff --git a/src/models/UtilModels.ts b/src/models/UtilModels.ts index cc90b0ba..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{ @@ -112,6 +114,12 @@ 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'; -} \ No newline at end of file +} diff --git a/src/models/util.ts b/src/models/util.ts index a728828d..239f13b2 100644 --- a/src/models/util.ts +++ b/src/models/util.ts @@ -8,6 +8,7 @@ import { TestKind } from "./engine/TestKind"; import { PassFailMetric, Statistics, TestRunArtifacts, TestRunModel, TestModel, ManagedIdentityTypeForAPI } from './PayloadModels'; 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)){ @@ -781,7 +782,8 @@ export function getDefaultTestRunName() export function getDefaultRunDescription() { - return "Started using GitHub Actions" + const pipelineName = core.getVariable("Build.DefinitionName") || core.getVariable("Release.DefinitionName"); + 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..6e259446 --- /dev/null +++ b/src/postProcessJob.ts @@ -0,0 +1,26 @@ +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 { isNull, isNullOrUndefined } from "util"; + +async function run() { + try { + + const runId = core.getTaskVariable(PostTaskParameters.runId); + const baseUri = core.getTaskVariable(PostTaskParameters.baseUri); + const isRunCompleted = core.getTaskVariable(PostTaskParameters.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; From 271bb43093b2518e0bc73a388fe4a07f35336a68 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 09:49:16 +0530 Subject: [PATCH 12/29] pr comments. --- src/models/APISupport.ts | 10 +++++----- src/postProcessJob.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index 327948a9..4d1ad4f1 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -1,4 +1,4 @@ -import { isNull, isNullOrUndefined } from "util"; +import { isNullOrUndefined } from "util"; import { AuthenticationUtils } from "./AuthenticationUtils"; import { ApiVersionConstants, FetchCallType, FileType, PostTaskParameters, reportZipFileName, resultZipFileName } from "./UtilModels"; import { TestKind } from "./engine/TestKind"; @@ -209,7 +209,7 @@ export class APISupport { 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(Object.keys(appComponentsData.components).length == 0) { + if(!isNullOrUndefined(appComponentsData?.components) && Object.keys(appComponentsData.components).length == 0) { return; } if(appComponentsResult.message.statusCode != 200 && appComponentsResult.message.statusCode != 201) { @@ -230,7 +230,7 @@ export class APISupport { let serverMetricsData : ServerMetricConfig = { metrics: this.yamlModel.serverMetricsConfig } - if(Object.keys(serverMetricsData.metrics).length == 0) { + if(!isNullOrUndefined(serverMetricsData?.metrics) && Object.keys(serverMetricsData.metrics).length == 0) { return; } let header = await this.authContext.getDataPlaneHeader(FetchCallType.patch); @@ -481,11 +481,11 @@ export class APISupport { } } - if(!isNull(testRunObj.testResult) && !isNullOrUndefined(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { + if(!isNullOrUndefined(testRunObj.testResult) && Util.isStatusFailed(testRunObj.testResult)) { core.setFailed("TestResult: "+ testRunObj.testResult); return; } - if(!isNull(testRunObj.status) && !isNullOrUndefined(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; diff --git a/src/postProcessJob.ts b/src/postProcessJob.ts index 6e259446..ecf393a2 100644 --- a/src/postProcessJob.ts +++ b/src/postProcessJob.ts @@ -3,7 +3,7 @@ import * as core from '@actions/core'; import { AuthenticationUtils } from "./models/AuthenticationUtils"; import { YamlConfig } from "./models/TaskModels"; import { APISupport } from "./models/APISupport"; -import { isNull, isNullOrUndefined } from "util"; +import { isNullOrUndefined } from "util"; async function run() { try { From 0e6bb611d2432d463a0c07b146d13edc569cb225 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 11:09:39 +0530 Subject: [PATCH 13/29] corrections for errors. --- src/models/APISupport.ts | 5 +++-- src/models/FetchHelper.ts | 17 +++++++++-------- src/models/util.ts | 2 +- src/postProcessJob.ts | 7 ++++--- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index 4d1ad4f1..1ae4e88f 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -378,7 +378,7 @@ export class APISupport { const testRunId = this.yamlModel.runTimeParams.testRunId; let urlSuffix = "test-runs/"+testRunId+"?api-version=" + ApiVersionConstants.latestVersion; urlSuffix = this.baseURL+urlSuffix; - core.setTaskVariable(PostTaskParameters.runId, testRunId); + core.exportVariable(PostTaskParameters.runId, testRunId); console.log("Creating and running a testRun for the test"); let header = await this.authContext.getDataPlaneHeader(FetchCallType.patch); let startTestresult = await FetchUtil.httpClientRetries(urlSuffix,header,FetchCallType.patch,3,JSON.stringify(startData)); @@ -455,7 +455,8 @@ export class APISupport { Util.printCriteria(testRunObj.passFailCriteria.passFailMetrics) if(testRunObj.testRunStatistics != null && testRunObj.testRunStatistics != undefined) Util.printClientMetrics(testRunObj.testRunStatistics); - core.setTaskVariable(PostTaskParameters.isRunCompleted, 'true'); + core.exportVariable(PostTaskParameters.isRunCompleted, 'true'); + console.log(process.env[PostTaskParameters.runId], process.env[PostTaskParameters.isRunCompleted]); let testResultUrl = Util.getResultFolder(testRunObj.testArtifacts); if(testResultUrl != null) { const response = await FetchUtil.httpClientRetries(testResultUrl,{},FetchCallType.get,3,""); diff --git a/src/models/FetchHelper.ts b/src/models/FetchHelper.ts index 44cb76d7..7f3ecadc 100644 --- a/src/models/FetchHelper.ts +++ b/src/models/FetchHelper.ts @@ -30,16 +30,17 @@ export async function httpClientRetries(urlSuffix : string, header : IHeaders, m let fileContent = uploadFileData(data); httpResponse = await httpClient.request(methodEnumToString[method], urlSuffix, fileContent, header); } else{ - const organization = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI; - const project = process.env.SYSTEM_TEAMPROJECT; - const buildId = process.env.BUILD_BUILDID; - console.log(organization, project, buildId); - const pipelineName = tl.getVariable("Build.DefinitionName") || tl.getVariable("Release.DefinitionName") || "Unavailable"; - const pipelineUrl = `${organization}${project}/_build/results?buildId=${buildId}`; - + 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}`; + + console.log(pipelineUrl, pipelineName); header['x-ms-pipelineUrl'] = pipelineUrl; header['x-ms-pipelineName'] = pipelineName; // setting these for patch calls. - console.log(pipelineUrl, pipelineName, tl.getVariable("Build.DefinitionName"), tl.getVariable("Release.DefinitionName")); + httpResponse = await httpClient.request(methodEnumToString[method], urlSuffix, data, header); } if(httpResponse.message.statusCode!= undefined && httpResponse.message.statusCode >= 300){ diff --git a/src/models/util.ts b/src/models/util.ts index 239f13b2..702e9e95 100644 --- a/src/models/util.ts +++ b/src/models/util.ts @@ -782,7 +782,7 @@ export function getDefaultTestRunName() export function getDefaultRunDescription() { - const pipelineName = core.getVariable("Build.DefinitionName") || core.getVariable("Release.DefinitionName"); + const pipelineName = process.env.GITHUB_WORKFLOW || "Unknown Pipeline"; return "Started using GH workflows" + (pipelineName ? "-" + pipelineName : ""); } diff --git a/src/postProcessJob.ts b/src/postProcessJob.ts index ecf393a2..53d346aa 100644 --- a/src/postProcessJob.ts +++ b/src/postProcessJob.ts @@ -8,9 +8,10 @@ import { isNullOrUndefined } from "util"; async function run() { try { - const runId = core.getTaskVariable(PostTaskParameters.runId); - const baseUri = core.getTaskVariable(PostTaskParameters.baseUri); - const isRunCompleted = core.getTaskVariable(PostTaskParameters.isRunCompleted); + const runId = process.env[PostTaskParameters.runId]; + const baseUri = process.env[PostTaskParameters.baseUri]; + const isRunCompleted = process.env[PostTaskParameters.isRunCompleted]; + if(!isNullOrUndefined(runId) && !isNullOrUndefined(baseUri) && (isNullOrUndefined(isRunCompleted) || isRunCompleted != 'true')) { const yamlConfig = new YamlConfig(true); const authContext = new AuthenticationUtils(); From 07dcb626df1c6d5fabd7833858669c86f50ff8b9 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 11:11:36 +0530 Subject: [PATCH 14/29] commit to trigger workflow. --- src/models/APISupport.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index 1ae4e88f..a4805c88 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -443,6 +443,7 @@ export class APISupport { reportsAvailable = true; } } + if(testRunObj && testRunObj.startDateTime){ startTime = new Date(testRunObj.startDateTime); } From 215b418a0d6dcb5d6abb16f7ae47fabf73a182b3 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 11:24:29 +0530 Subject: [PATCH 15/29] some fix. --- src/models/APISupport.ts | 6 +++--- src/models/TaskModels.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index a4805c88..758472d0 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -390,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("\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 '"+Util.getSubscriptionIdFromResourceId(this.authContext.resourceId)+"'") - console.log("2. On the Tests page, go to test '"+this.testId+"'") + 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); } @@ -443,7 +443,7 @@ export class APISupport { reportsAvailable = true; } } - + if(testRunObj && testRunObj.startDateTime){ startTime = new Date(testRunObj.startDateTime); } diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts index 6b793ccc..fafb4bfd 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -292,7 +292,7 @@ export class YamlConfig { let runTimeParams : RunTimeParams = {env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: ''}; this.runTimeParams = runTimeParams; - let overRideParams = core.getInput(InputConstants.overRideParameters); + let overRideParams = core.getInput(InputConstants.overRideParameters) ?? undefined; let outputVarName = core.getInput(InputConstants.outputVariableName) ?? OutputVariableName; let validation = Util.validateOverRideParameters(overRideParams); From deb0a0798b8aa20677aefdf188ef34d947265548 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 11:29:16 +0530 Subject: [PATCH 16/29] try. --- src/models/TaskModels.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts index fafb4bfd..4fa693f3 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -292,8 +292,10 @@ export class YamlConfig { let runTimeParams : RunTimeParams = {env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: ''}; this.runTimeParams = runTimeParams; - let overRideParams = core.getInput(InputConstants.overRideParameters) ?? undefined; + let overRideParamsInput = core.getInput(InputConstants.overRideParameters); + let overRideParams = !isNullOrUndefined(overRideParamsInput) && overRideParamsInput != '' ? overRideParamsInput : undefined; let outputVarName = core.getInput(InputConstants.outputVariableName) ?? OutputVariableName; + console.log(overRideParams, outputVarName, eRun, secretRun); let validation = Util.validateOverRideParameters(overRideParams); if(validation.valid == false) { From 6567d10f1b271b910cff894840872b7c3d864769 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 11:33:12 +0530 Subject: [PATCH 17/29] adding checks on empty strings. --- src/models/TaskModels.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts index 4fa693f3..3bbd818d 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -293,9 +293,9 @@ export class YamlConfig { 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 = core.getInput(InputConstants.outputVariableName) ?? OutputVariableName; - console.log(overRideParams, outputVarName, eRun, secretRun); + let outputVarName = !isNullOrUndefined(outputVariableNameInput) && outputVariableNameInput != '' ? outputVariableNameInput : OutputVariableName; let validation = Util.validateOverRideParameters(overRideParams); if(validation.valid == false) { From 76dd23e9852f1321dee3b496deeb87ba81998360 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 14:11:43 +0530 Subject: [PATCH 18/29] pr comments. --- .github/workflows/pr_check_load_test.yml | 4 +- .gitignore | 1 - lib/main.js | 67 ++ lib/models/APIResponseModel.js | 2 + lib/models/APISupport.js | 546 ++++++++++++ lib/models/AuthenticationUtils.js | 166 ++++ lib/models/FetchHelper.js | 104 +++ lib/models/FileUtils.js | 97 ++ lib/models/InputConstants.js | 24 + lib/models/PayloadModels.js | 21 + lib/models/TaskModels.js | 510 +++++++++++ lib/models/UtilModels.js | 83 ++ lib/models/constants.js | 61 ++ .../engine/BaseLoadTestFrameworkModel.js | 2 + lib/models/engine/JMeterFrameworkModel.js | 40 + lib/models/engine/LocustFrameworkModel.js | 40 + lib/models/engine/TestKind.js | 12 + lib/models/engine/Util.js | 103 +++ lib/models/util.js | 835 ++++++++++++++++++ lib/postProcessJob.js | 59 ++ lib/services/FeatureFlagService.js | 74 ++ lib/services/FeatureFlags.js | 7 + src/models/APISupport.ts | 2 +- src/models/AuthenticationUtils.ts | 17 +- 24 files changed, 2871 insertions(+), 6 deletions(-) create mode 100644 lib/main.js create mode 100644 lib/models/APIResponseModel.js create mode 100644 lib/models/APISupport.js create mode 100644 lib/models/AuthenticationUtils.js create mode 100644 lib/models/FetchHelper.js create mode 100644 lib/models/FileUtils.js create mode 100644 lib/models/InputConstants.js create mode 100644 lib/models/PayloadModels.js create mode 100644 lib/models/TaskModels.js create mode 100644 lib/models/UtilModels.js create mode 100644 lib/models/constants.js create mode 100644 lib/models/engine/BaseLoadTestFrameworkModel.js create mode 100644 lib/models/engine/JMeterFrameworkModel.js create mode 100644 lib/models/engine/LocustFrameworkModel.js create mode 100644 lib/models/engine/TestKind.js create mode 100644 lib/models/engine/Util.js create mode 100644 lib/models/util.js create mode 100644 lib/postProcessJob.js create mode 100644 lib/services/FeatureFlagService.js create mode 100644 lib/services/FeatureFlags.js diff --git a/.github/workflows/pr_check_load_test.yml b/.github/workflows/pr_check_load_test.yml index 2a1973dd..ef39205f 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 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/lib/main.js b/lib/main.js new file mode 100644 index 00000000..3f944ce9 --- /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.setOutput(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..fa2e1d4e --- /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 '" + Util.getSubscriptionIdFromResourceId(this.authContext.resourceId) + "'"); + console.log("2. On the Tests page, go to test '" + this.testId + "'"); + 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..2fcef76d --- /dev/null +++ b/lib/models/AuthenticationUtils.js @@ -0,0 +1,166 @@ +"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 = ''; + } + authorize() { + return __awaiter(this, void 0, void 0, function* () { + // NOTE: This will set the subscription id + yield this.getTokenAPI(UtilModels_1.TokenScope.ControlPlane); + const rg = core.getInput(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; + }); + } + 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..c6b1263c --- /dev/null +++ b/lib/models/FetchHelper.js @@ -0,0 +1,104 @@ +"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}`; + console.log(pipelineUrl, pipelineName); + 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..615d7370 --- /dev/null +++ b/lib/models/TaskModels.js @@ -0,0 +1,510 @@ +"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 _a, _b, _c; + 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`); + } + } + const runDisplayName = (_a = core.getInput('loadTestRunName')) !== null && _a !== void 0 ? _a : Util.getDefaultTestRunName(); + const runDescription = (_b = core.getInput('loadTestRunDescription')) !== null && _b !== void 0 ? _b : Util.getDefaultRunDescription(); + let runTimeParams = { env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: '' }; + this.runTimeParams = runTimeParams; + let overRideParams = core.getInput(InputConstants.overRideParameters); + let outputVarName = (_c = core.getInput(InputConstants.outputVariableName)) !== null && _c !== void 0 ? _c : constants_1.OutputVariableName; + 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..05fcf31a --- /dev/null +++ b/lib/postProcessJob.js @@ -0,0 +1,59 @@ +"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]; + 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/models/APISupport.ts b/src/models/APISupport.ts index 758472d0..2638ddd8 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -391,7 +391,7 @@ export class APISupport { 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 '"+Util.getSubscriptionIdFromResourceId(this.authContext.resourceId)+"'") + 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); diff --git a/src/models/AuthenticationUtils.ts b/src/models/AuthenticationUtils.ts index 28472acd..45d269a7 100644 --- a/src/models/AuthenticationUtils.ts +++ b/src/models/AuthenticationUtils.ts @@ -16,13 +16,14 @@ 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); - + 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 == ''){ @@ -122,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. From e3aa8e3281e708df2c2446badc3a7f02b3d77272 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 14:12:09 +0530 Subject: [PATCH 19/29] tsc --- lib/models/APISupport.js | 5 +++-- lib/models/AuthenticationUtils.js | 17 +++++++++++++++++ lib/models/TaskModels.js | 8 +++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/models/APISupport.js b/lib/models/APISupport.js index fa2e1d4e..c48c511d 100644 --- a/lib/models/APISupport.js +++ b/lib/models/APISupport.js @@ -423,8 +423,8 @@ class APISupport { 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 '" + Util.getSubscriptionIdFromResourceId(this.authContext.resourceId) + "'"); - console.log("2. On the Tests page, go to test '" + this.testId + "'"); + 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); } @@ -488,6 +488,7 @@ class APISupport { if (testRunObj.testRunStatistics != null && testRunObj.testRunStatistics != undefined) Util.printClientMetrics(testRunObj.testRunStatistics); core.exportVariable(UtilModels_1.PostTaskParameters.isRunCompleted, 'true'); + console.log(process.env[UtilModels_1.PostTaskParameters.runId], process.env[UtilModels_1.PostTaskParameters.isRunCompleted]); let testResultUrl = Util.getResultFolder(testRunObj.testArtifacts); if (testResultUrl != null) { const response = yield FetchUtil.httpClientRetries(testResultUrl, {}, UtilModels_1.FetchCallType.get, 3, ""); diff --git a/lib/models/AuthenticationUtils.js b/lib/models/AuthenticationUtils.js index 2fcef76d..df3be57b 100644 --- a/lib/models/AuthenticationUtils.js +++ b/lib/models/AuthenticationUtils.js @@ -49,11 +49,13 @@ class AuthenticationUtils { 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 == '') { @@ -151,6 +153,21 @@ class AuthenticationUtils { 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. diff --git a/lib/models/TaskModels.js b/lib/models/TaskModels.js index 615d7370..d99d1fd2 100644 --- a/lib/models/TaskModels.js +++ b/lib/models/TaskModels.js @@ -252,7 +252,7 @@ class YamlConfig { this.outputVariableName = outputVarName; } getRunTimeParams() { - var _a, _b, _c; + var _a, _b; var secretRun = core.getInput(InputConstants.secrets); let secretsParsed = {}; let envParsed = {}; @@ -295,8 +295,10 @@ class YamlConfig { const runDescription = (_b = core.getInput('loadTestRunDescription')) !== null && _b !== void 0 ? _b : Util.getDefaultRunDescription(); let runTimeParams = { env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: '' }; this.runTimeParams = runTimeParams; - let overRideParams = core.getInput(InputConstants.overRideParameters); - let outputVarName = (_c = core.getInput(InputConstants.outputVariableName)) !== null && _c !== void 0 ? _c : constants_1.OutputVariableName; + 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; let validation = Util.validateOverRideParameters(overRideParams); if (validation.valid == false) { console.log(validation.error); From a50b8dbd7aa6b245a7dcfed68b258e0fbd47a691 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 14:21:35 +0530 Subject: [PATCH 20/29] adding check for empty string too. --- src/models/TaskModels.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts index 3bbd818d..28ff6433 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -287,8 +287,10 @@ export class YamlConfig { 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('loadTestRunName'); + const runDisplayName = runDisplayNameInput ? runDisplayNameInput : Util.getDefaultTestRunName(); + let runDescriptionInput = core.getInput('loadTestRunName'); + const runDescription = runDescriptionInput ? runDescriptionInput : Util.getDefaultRunDescription(); let runTimeParams : RunTimeParams = {env: envParsed, secrets: secretsParsed, runDisplayName, runDescription, testId: '', testRunId: ''}; this.runTimeParams = runTimeParams; From 67651b30f3c878ca4ac5913981784c42a64cf400 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 14:31:14 +0530 Subject: [PATCH 21/29] tsc --- lib/models/TaskModels.js | 7 ++++--- src/models/TaskModels.ts | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/models/TaskModels.js b/lib/models/TaskModels.js index d99d1fd2..4be3f8de 100644 --- a/lib/models/TaskModels.js +++ b/lib/models/TaskModels.js @@ -252,7 +252,6 @@ class YamlConfig { this.outputVariableName = outputVarName; } getRunTimeParams() { - var _a, _b; var secretRun = core.getInput(InputConstants.secrets); let secretsParsed = {}; let envParsed = {}; @@ -291,8 +290,10 @@ class YamlConfig { 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 = (_a = core.getInput('loadTestRunName')) !== null && _a !== void 0 ? _a : Util.getDefaultTestRunName(); - const runDescription = (_b = core.getInput('loadTestRunDescription')) !== null && _b !== void 0 ? _b : Util.getDefaultRunDescription(); + let 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); diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts index 28ff6433..5b552fde 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -287,10 +287,10 @@ export class YamlConfig { 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('loadTestRunName'); - const runDisplayName = runDisplayNameInput ? runDisplayNameInput : Util.getDefaultTestRunName(); - let runDescriptionInput = core.getInput('loadTestRunName'); - const runDescription = runDescriptionInput ? runDescriptionInput : 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; From b96fdf89b1b8014b8323f4219d57d0f30d0a1ede Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 14:43:56 +0530 Subject: [PATCH 22/29] treiggering workflow to check postprocess. --- src/models/FetchHelper.ts | 1 - src/postProcessJob.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/FetchHelper.ts b/src/models/FetchHelper.ts index 7f3ecadc..b733319d 100644 --- a/src/models/FetchHelper.ts +++ b/src/models/FetchHelper.ts @@ -37,7 +37,6 @@ export async function httpClientRetries(urlSuffix : string, header : IHeaders, m const pipelineName = process.env.GITHUB_WORKFLOW || "Unknown Pipeline"; const pipelineUrl = `${githubBaseUrl}/${repository}/actions/runs/${runId}`; - console.log(pipelineUrl, pipelineName); header['x-ms-pipelineUrl'] = pipelineUrl; header['x-ms-pipelineName'] = pipelineName; // setting these for patch calls. diff --git a/src/postProcessJob.ts b/src/postProcessJob.ts index 53d346aa..18b52d48 100644 --- a/src/postProcessJob.ts +++ b/src/postProcessJob.ts @@ -19,6 +19,7 @@ async function run() { await apiSupport.stopTestRunPostProcess(baseUri, runId); } } + catch(err : any) { core.debug("Failed to stop the test run:" + err.message); } From d73b517e821ac09e97c1db2ff3d1669fa3f3d316 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 14:56:09 +0530 Subject: [PATCH 23/29] adding baseUri to env's --- lib/main.js | 2 +- lib/models/FetchHelper.js | 1 - lib/postProcessJob.js | 1 + src/main.ts | 2 +- src/models/APISupport.ts | 1 - src/postProcessJob.ts | 4 ++-- 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/main.js b/lib/main.js index 3f944ce9..956d4c83 100644 --- a/lib/main.js +++ b/lib/main.js @@ -47,7 +47,7 @@ function run() { let apiSupport = new APISupport_1.APISupport(authContext, yamlConfig); yield authContext.authorize(); yield apiSupport.getResource(); - core.setOutput(UtilModels_1.PostTaskParameters.baseUri, apiSupport.baseURL); + core.exportVariable(UtilModels_1.PostTaskParameters.baseUri, apiSupport.baseURL); yield apiSupport.getTestAPI(false); if (fs.existsSync(UtilModels_1.resultFolder)) { util.deleteFile(UtilModels_1.resultFolder); diff --git a/lib/models/FetchHelper.js b/lib/models/FetchHelper.js index c6b1263c..7c8c9f72 100644 --- a/lib/models/FetchHelper.js +++ b/lib/models/FetchHelper.js @@ -72,7 +72,6 @@ function httpClientRetries(urlSuffix, header, method, retries = 1, data, isUploa const runId = process.env.GITHUB_RUN_ID; const pipelineName = process.env.GITHUB_WORKFLOW || "Unknown Pipeline"; const pipelineUrl = `${githubBaseUrl}/${repository}/actions/runs/${runId}`; - console.log(pipelineUrl, pipelineName); header['x-ms-pipelineUrl'] = pipelineUrl; header['x-ms-pipelineName'] = pipelineName; // setting these for patch calls. httpResponse = yield httpClient.request(methodEnumToString[method], urlSuffix, data, header); diff --git a/lib/postProcessJob.js b/lib/postProcessJob.js index 05fcf31a..b951cbad 100644 --- a/lib/postProcessJob.js +++ b/lib/postProcessJob.js @@ -44,6 +44,7 @@ function run() { 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(); diff --git a/src/main.ts b/src/main.ts index 40dbcac0..cf891623 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,7 +15,7 @@ async function run() { await authContext.authorize(); await apiSupport.getResource(); - core.setOutput(PostTaskParameters.baseUri, apiSupport.baseURL); + core.exportVariable(PostTaskParameters.baseUri, apiSupport.baseURL); await apiSupport.getTestAPI(false); if (fs.existsSync(resultFolder)){ util.deleteFile(resultFolder); diff --git a/src/models/APISupport.ts b/src/models/APISupport.ts index 2638ddd8..de71b0fa 100644 --- a/src/models/APISupport.ts +++ b/src/models/APISupport.ts @@ -457,7 +457,6 @@ export class APISupport { if(testRunObj.testRunStatistics != null && testRunObj.testRunStatistics != undefined) Util.printClientMetrics(testRunObj.testRunStatistics); core.exportVariable(PostTaskParameters.isRunCompleted, 'true'); - console.log(process.env[PostTaskParameters.runId], process.env[PostTaskParameters.isRunCompleted]); let testResultUrl = Util.getResultFolder(testRunObj.testArtifacts); if(testResultUrl != null) { const response = await FetchUtil.httpClientRetries(testResultUrl,{},FetchCallType.get,3,""); diff --git a/src/postProcessJob.ts b/src/postProcessJob.ts index 18b52d48..496496e0 100644 --- a/src/postProcessJob.ts +++ b/src/postProcessJob.ts @@ -11,7 +11,7 @@ async function run() { 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(); @@ -19,7 +19,7 @@ async function run() { await apiSupport.stopTestRunPostProcess(baseUri, runId); } } - + catch(err : any) { core.debug("Failed to stop the test run:" + err.message); } From 0bc788ce9961052eecb9b5378acf856cb2d0829f Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 14:56:50 +0530 Subject: [PATCH 24/29] tsc --- lib/models/APISupport.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/models/APISupport.js b/lib/models/APISupport.js index c48c511d..c49053d4 100644 --- a/lib/models/APISupport.js +++ b/lib/models/APISupport.js @@ -488,7 +488,6 @@ class APISupport { if (testRunObj.testRunStatistics != null && testRunObj.testRunStatistics != undefined) Util.printClientMetrics(testRunObj.testRunStatistics); core.exportVariable(UtilModels_1.PostTaskParameters.isRunCompleted, 'true'); - console.log(process.env[UtilModels_1.PostTaskParameters.runId], process.env[UtilModels_1.PostTaskParameters.isRunCompleted]); let testResultUrl = Util.getResultFolder(testRunObj.testArtifacts); if (testResultUrl != null) { const response = yield FetchUtil.httpClientRetries(testResultUrl, {}, UtilModels_1.FetchCallType.get, 3, ""); From 9e564f59198db6c534943df4aaff08c5e3dfca15 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 15:06:59 +0530 Subject: [PATCH 25/29] trry override params. --- .github/workflows/pr_check_load_test.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr_check_load_test.yml b/.github/workflows/pr_check_load_test.yml index ef39205f..646b4b99 100644 --- a/.github/workflows/pr_check_load_test.yml +++ b/.github/workflows/pr_check_load_test.yml @@ -72,20 +72,18 @@ jobs: 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: Set Override Parameters + run: echo "OVERRIDE_PARAMS={\"testId\":\"${{ steps.guid.outputs.GUID }}\"}" >> $GITHUB_ENV + - name: 'Azure Load Testing' uses: ./ with: - loadTestConfigFile: test-config-${{ steps.guid.outputs.GUID }}.yaml + loadTestConfigFile: ${{ matrix.configFile }} loadTestResource: ${{ env.LOAD_TEST_RESOURCE }} resourceGroup: ${{ env.LOAD_TEST_RESOURCE_GROUP }} + overRideParameters: ${{ env.OVERRIDE_PARAMS }} + continue-on-error: true - name: Check for results and report files From 9927b053d48069bdf521b13c937d4f96147935c7 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 15:21:24 +0530 Subject: [PATCH 26/29] try --- .github/workflows/pr_check_load_test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr_check_load_test.yml b/.github/workflows/pr_check_load_test.yml index 646b4b99..d201a298 100644 --- a/.github/workflows/pr_check_load_test.yml +++ b/.github/workflows/pr_check_load_test.yml @@ -78,14 +78,18 @@ jobs: - name: 'Azure Load Testing' uses: ./ + id: alt with: loadTestConfigFile: ${{ matrix.configFile }} loadTestResource: ${{ env.LOAD_TEST_RESOURCE }} resourceGroup: ${{ env.LOAD_TEST_RESOURCE_GROUP }} overRideParameters: ${{ env.OVERRIDE_PARAMS }} - + 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 From 6ca72129441da320d00f1e5b1185d31fd86d98d5 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 15:28:52 +0530 Subject: [PATCH 27/29] printing over-rde params. --- lib/models/TaskModels.js | 1 + src/models/TaskModels.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/models/TaskModels.js b/lib/models/TaskModels.js index 4be3f8de..0dce855c 100644 --- a/lib/models/TaskModels.js +++ b/lib/models/TaskModels.js @@ -300,6 +300,7 @@ class YamlConfig { 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); diff --git a/src/models/TaskModels.ts b/src/models/TaskModels.ts index 5b552fde..caee0a05 100644 --- a/src/models/TaskModels.ts +++ b/src/models/TaskModels.ts @@ -298,7 +298,7 @@ export class YamlConfig { 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); From 94bab5617d44583526df3b96a97f039bb01c535c Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 15:46:20 +0530 Subject: [PATCH 28/29] iosolating the over-ride params. --- .github/workflows/pr_check_load_test.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr_check_load_test.yml b/.github/workflows/pr_check_load_test.yml index d201a298..d5f70fd3 100644 --- a/.github/workflows/pr_check_load_test.yml +++ b/.github/workflows/pr_check_load_test.yml @@ -73,9 +73,8 @@ jobs: shell : bash - name: Set Override Parameters - run: echo "OVERRIDE_PARAMS={\"testId\":\"${{ steps.guid.outputs.GUID }}\"}" >> $GITHUB_ENV - - + run: echo "OVERRIDE_PARAMS_${{ matrix.os }}_${{ matrix.configFile }}={\"testId\":\"${{ steps.guid.outputs.GUID }}\"}" >> $GITHUB_ENV + - name: 'Azure Load Testing' uses: ./ id: alt @@ -83,12 +82,12 @@ jobs: loadTestConfigFile: ${{ matrix.configFile }} loadTestResource: ${{ env.LOAD_TEST_RESOURCE }} resourceGroup: ${{ env.LOAD_TEST_RESOURCE_GROUP }} - overRideParameters: ${{ env.OVERRIDE_PARAMS }} + overRideParameters: ${{ env[format('OVERRIDE_PARAMS_{0}_{1}', matrix.os, matrix.configFile)] }} outputVariableName: 'loadTestRunId' continue-on-error: true - name: Print the Output - run: echo "The Test ID is ${{ steps.alt.outputs.loadTestRunId.testRunId }}" + run: echo "The Test ID is ${{ steps.alt.outputs['loadTestRunId.testRunId'] }}" - name: Check for results and report files run: | From 86864caec1dca73d6501a664883585e4e0c71851 Mon Sep 17 00:00:00 2001 From: kumarmoh Date: Fri, 21 Feb 2025 15:50:22 +0530 Subject: [PATCH 29/29] changing the way to interpret over-ride params --- .github/workflows/pr_check_load_test.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/pr_check_load_test.yml b/.github/workflows/pr_check_load_test.yml index d5f70fd3..c750259a 100644 --- a/.github/workflows/pr_check_load_test.yml +++ b/.github/workflows/pr_check_load_test.yml @@ -71,9 +71,6 @@ jobs: echo "::set-output name=GUID::$(uuidgen)" fi shell : bash - - - name: Set Override Parameters - run: echo "OVERRIDE_PARAMS_${{ matrix.os }}_${{ matrix.configFile }}={\"testId\":\"${{ steps.guid.outputs.GUID }}\"}" >> $GITHUB_ENV - name: 'Azure Load Testing' uses: ./ @@ -82,7 +79,7 @@ jobs: loadTestConfigFile: ${{ matrix.configFile }} loadTestResource: ${{ env.LOAD_TEST_RESOURCE }} resourceGroup: ${{ env.LOAD_TEST_RESOURCE_GROUP }} - overRideParameters: ${{ env[format('OVERRIDE_PARAMS_{0}_{1}', matrix.os, matrix.configFile)] }} + overRideParameters: "{\"testId\":\"${{ steps.guid.outputs.GUID }}\"}" outputVariableName: 'loadTestRunId' continue-on-error: true