Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
20ec72d
Update README.md
mohitpavan Nov 20, 2025
9012764
Refactor cloud token scopes to use TaskParameters
mohitpavan Nov 20, 2025
3d89755
Add token scopes for Azure management
mohitpavan Nov 20, 2025
c4904b3
Simplify token retrieval in AuthenticatorService
mohitpavan Nov 20, 2025
2d78928
Update AzCliUtility.js
mohitpavan Nov 20, 2025
f227175
Refactor httpClientRetries for improved readability
mohitpavan Nov 20, 2025
92db3c3
Refactor getTaskParameters and update subscription logic
mohitpavan Nov 20, 2025
2fc6042
Refactor token scopes to use enums
mohitpavan Nov 20, 2025
4fa0cc4
Refactor token scopes in TaskParameters interface
mohitpavan Nov 20, 2025
00a1b86
Simplify token retrieval in getTokenAPI
mohitpavan Nov 20, 2025
c6bfceb
Refactor execAz function to accept token scope
mohitpavan Nov 20, 2025
76ef294
Refactor httpClientRetries for improved error handling
mohitpavan Nov 20, 2025
72dd8fa
Refactor subscription parameter retrieval methods
mohitpavan Nov 20, 2025
d2639bc
Refactor test stubs for AzCliUtility calls
mohitpavan Nov 20, 2025
93ea47f
Increase timeout for deleteFileAPI test
mohitpavan Nov 20, 2025
fde2f27
Add conditional for installing dependencies on main branch
mohitpavan Nov 20, 2025
eb7f825
correcting UTs.
mohitpavan Nov 20, 2025
f4708d8
Merge branch 'users/mohit/aud-fix' of https://github.com/Azure/load-t…
mohitpavan Nov 20, 2025
51a8e1d
fixing installation step.
mohitpavan Nov 20, 2025
8304585
updating redme.
mohitpavan Nov 20, 2025
130b7b1
printing the platform
mohitpavan Nov 20, 2025
ffcd028
account show is not working in the cmd.
mohitpavan Nov 20, 2025
17e359c
changed back to true.
mohitpavan Nov 20, 2025
6ee63c5
changing js too.
mohitpavan Nov 20, 2025
7cefaa4
change.
mohitpavan Nov 20, 2025
d76f3d9
updating function name and removing console.
mohitpavan Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr_check_load_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
node-version: '20'

- name: Installing dependencies and building latest changes
if: github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main')
run: |
npm install --include=dev -f
npm ci
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ For using any credentials like Azure Service Principal in your workflow, add the
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
```


## Breaking changes

All minor versions under `1.1` (e.g., `1.1.x`) will stop working due to a breaking change.
To ensure continued compatibility, we recommend switching to the `v1` tag, which we will continue to support.
We are also bumping the minor version to `1.2.1`. If you prefer to pin to a specific version, please update your workflows to use `1.2.1`.

## Azure Load Testing Action

This section describes the Azure Load Testing GitHub action. You can use this action by referencing `azure/load-testing@v1` action in your workflow. The action runs on Windows, Linux, and Mac runners.
Expand Down
9 changes: 5 additions & 4 deletions lib/Constants/EnvironmentConstants.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AzureUSGovernmentCloud = exports.AzurePublicCloud = void 0;
const TaskParameters_1 = require("../models/TaskParameters");
exports.AzurePublicCloud = {
cloudName: "AzureCloud",
armTokenScope: "https://management.core.windows.net",
dataPlaneTokenScope: "https://loadtest.azure-dev.com",
armTokenScope: TaskParameters_1.armPublicTokenScope,
dataPlaneTokenScope: TaskParameters_1.publicTokenScope,
armEndpoint: "https://management.azure.com",
};
exports.AzureUSGovernmentCloud = {
cloudName: "AzureUSGovernment",
armTokenScope: "https://management.usgovcloudapi.net",
dataPlaneTokenScope: "https://cnt-prod.loadtesting.azure.us",
armTokenScope: TaskParameters_1.armUsGovernmentTokenScope,
dataPlaneTokenScope: TaskParameters_1.usGovernmentTokenScope,
};
19 changes: 16 additions & 3 deletions lib/Utils/AzCliUtility.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,27 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.execAz = void 0;
exports.getDPTokens = getDPTokens;
exports.getAccounts = getAccounts;
const child_process_1 = require("child_process");
function getDPTokens(tokenScope) {
return __awaiter(this, void 0, void 0, function* () {
const cmdArguments = ["account", "get-access-token", "--resource"];
cmdArguments.push(tokenScope);
return execAz(cmdArguments);
});
}
function getAccounts(accountType) {
return __awaiter(this, void 0, void 0, function* () {
const cmdArguments = accountType === 'Subscription' ? ["account", "show"] : ["cloud", "show"];
return execAz(cmdArguments);
});
}
function execAz(cmdArguments) {
return __awaiter(this, void 0, void 0, function* () {
const azCmd = process.platform === "win32" ? "az.cmd" : "az";
return new Promise((resolve, reject) => {
(0, child_process_1.execFile)(azCmd, [...cmdArguments, "--out", "json"], { encoding: "utf8", shell: true }, (error, stdout) => {
(0, child_process_1.execFile)(azCmd, [...cmdArguments, "--out", "json"], { encoding: "utf8", shell: process.platform === "win32" }, (error, stdout) => {
if (error) {
return reject(error);
}
Expand All @@ -31,4 +45,3 @@ function execAz(cmdArguments) {
});
});
}
exports.execAz = execAz;
16 changes: 11 additions & 5 deletions lib/Utils/FetchUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ const methodEnumToString = {
[UtilModels_1.FetchCallType.patch]: "patch"
};
// (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;
const retrriableCodes = [408, 429, 500, 502, 503, 504]; // 408 - Request Timeout, 429 - Too Many Requests, 500 - Internal Server Error, 502 - Bad Gateway, 503 - Service Unavailable, 504 - Gateway Timeout
let backOffTimeForRetry = 5; // seconds
let correlationId = `gh-actions-${(0, CommonUtils_1.getUniqueId)()}`;
try {
let correlationId = `gh-actions-${(0, CommonUtils_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);
Expand Down Expand Up @@ -79,22 +81,26 @@ function httpClientRetries(urlSuffix_1, header_1, method_1) {
if (httpResponse.message.statusCode != undefined && httpResponse.message.statusCode >= 300) {
CoreUtils.debug(`correlation id : ${correlationId}`);
}
if (httpResponse.message.statusCode != undefined && [408, 429, 502, 503, 504].includes(httpResponse.message.statusCode)) {
if (httpResponse.message.statusCode != undefined && retrriableCodes.includes(httpResponse.message.statusCode)) {
if (method == UtilModels_1.FetchCallType.patch) {
backOffTimeForRetry += 60; // extra 60 seconds for patch, basically this happens when the service didnot handle some of the external service dependencies, and the external can take time to recover.
}
let err = yield (0, CommonUtils_1.getResultObj)(httpResponse);
throw { message: (err && err.error && err.error.message) ? err.error.message : (0, CommonUtils_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);
let sleeptime = backOffTimeForRetry * 1000;
if (log) {
console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime / 1000} seconds`);
}
yield (0, CommonUtils_1.sleep)(sleeptime);
return yield httpClientRetries(urlSuffix, header, method, retries - 1, data);
}
else {
console.log(err, "\ncorrelationId:" + correlationId);
throw new Error(`Operation did not succeed after 3 retries. Pipeline failed with error : ${err.message}`);
}
}
Expand Down
30 changes: 19 additions & 11 deletions lib/Utils/TaskParametersUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
Expand Down Expand Up @@ -73,8 +83,7 @@ class TaskParametersUtil {
static setSubscriptionParameters(taskParameters) {
return __awaiter(this, void 0, void 0, function* () {
try {
const cmdArguments = ["account", "show"];
var result = yield AzCliUtility.execAz(cmdArguments);
var result = yield AzCliUtility.getAccounts('Subscription');
taskParameters.subscriptionId = result.id;
taskParameters.subscriptionName = result.name;
}
Expand All @@ -88,8 +97,7 @@ class TaskParametersUtil {
static setEndpointAndScopeParameters(taskParameters) {
return __awaiter(this, void 0, void 0, function* () {
try {
const cmdArguments = ["cloud", "show"];
var result = yield AzCliUtility.execAz(cmdArguments);
var result = yield AzCliUtility.getAccounts('Cloud');
let env = result ? result.name : null;
taskParameters.environment = env !== null && env !== void 0 ? env : EnvironmentConstants.AzurePublicCloud.cloudName;
let endpointUrl = (result && result.endpoints) ? result.endpoints.resourceManager : null;
Expand Down
5 changes: 5 additions & 0 deletions lib/models/TaskParameters.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.armUsGovernmentTokenScope = exports.armPublicTokenScope = exports.usGovernmentTokenScope = exports.publicTokenScope = void 0;
exports.publicTokenScope = "https://cnt-prod.loadtesting.azure.com";
exports.usGovernmentTokenScope = "https://cnt-prod.loadtesting.azure.us";
exports.armPublicTokenScope = "https://management.core.windows.net";
exports.armUsGovernmentTokenScope = "https://management.usgovcloudapi.net";
28 changes: 18 additions & 10 deletions lib/services/AuthenticatorService.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
Expand Down Expand Up @@ -71,9 +81,7 @@ class AuthenticatorService {
return __awaiter(this, void 0, void 0, function* () {
let tokenScopeDecoded = scope == UtilModels_1.TokenScope.Dataplane ? this.taskParameters.dataPlaneTokenScope : this.taskParameters.armTokenScope;
try {
const cmdArguments = ["account", "get-access-token", "--resource"];
cmdArguments.push(tokenScopeDecoded);
let result = yield AzCliUtility.execAz(cmdArguments);
let result = yield AzCliUtility.getDPTokens(tokenScopeDecoded);
let token = result.accessToken;
scope == UtilModels_1.TokenScope.ControlPlane ? this.controlPlaneToken = token : this.dataPlanetoken = token;
return token;
Expand Down
16 changes: 9 additions & 7 deletions src/Constants/EnvironmentConstants.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { ControlPlaneTokenScope, DataPlaneTokenScope, publicTokenScope, usGovernmentTokenScope, armUsGovernmentTokenScope, armPublicTokenScope } from "../models/TaskParameters";

type EnvironmentSettings = {
cloudName: string;
armTokenScope: string;
dataPlaneTokenScope: string;
armTokenScope: ControlPlaneTokenScope;
dataPlaneTokenScope: DataPlaneTokenScope;
armEndpoint?: string;
}

export const AzurePublicCloud: EnvironmentSettings = {
cloudName: "AzureCloud",
armTokenScope: "https://management.core.windows.net",
dataPlaneTokenScope: "https://loadtest.azure-dev.com",
armTokenScope: armPublicTokenScope,
dataPlaneTokenScope: publicTokenScope,
armEndpoint: "https://management.azure.com",
}

export const AzureUSGovernmentCloud: EnvironmentSettings = {
cloudName: "AzureUSGovernment",
armTokenScope: "https://management.usgovcloudapi.net",
dataPlaneTokenScope: "https://cnt-prod.loadtesting.azure.us",
}
armTokenScope: armUsGovernmentTokenScope,
dataPlaneTokenScope: usGovernmentTokenScope,
}
18 changes: 15 additions & 3 deletions src/Utils/AzCliUtility.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { execFile } from "child_process";
import { AccountType, ControlPlaneTokenScope, DataPlaneTokenScope } from "../models/TaskParameters";

export async function execAz(cmdArguments: string[]): Promise<any> {
export async function getDPTokens(tokenScope: ControlPlaneTokenScope| DataPlaneTokenScope): Promise<any> {
const cmdArguments = ["account", "get-access-token", "--resource"];
cmdArguments.push(tokenScope);
return execAz(cmdArguments);
}

export async function getAccounts(accountType: AccountType): Promise<any> {
const cmdArguments = accountType === 'Subscription' ? ["account", "show"] : ["cloud", "show"];
return execAz(cmdArguments);
}

async function execAz(cmdArguments: string[]): Promise<any> {
const azCmd = process.platform === "win32" ? "az.cmd" : "az";
return new Promise<any>((resolve, reject) => {
execFile(azCmd, [...cmdArguments, "--out", "json"], { encoding: "utf8", shell : true }, (error:any, stdout:any) => {
execFile(azCmd, [...cmdArguments, "--out", "json"], { encoding: "utf8", shell : process.platform === "win32" }, (error:any, stdout:any) => {
if (error) {
return reject(error);
}
Expand All @@ -17,4 +29,4 @@ export async function execAz(cmdArguments: string[]): Promise<any> {
}
});
});
}
}
14 changes: 10 additions & 4 deletions src/Utils/FetchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ const methodEnumToString : { [key in FetchCallType] : string } = {
// (note mohit): shift to the enum later.
export async function httpClientRetries(urlSuffix : string, header : IHeaders, method : FetchCallType , retries : number = 1, data : string , isUploadCall : boolean = true, log: boolean = true) : Promise<IHttpClientResponse>{
let httpResponse : IHttpClientResponse;
const retrriableCodes = [408,429,500,502,503,504]; // 408 - Request Timeout, 429 - Too Many Requests, 500 - Internal Server Error, 502 - Bad Gateway, 503 - Service Unavailable, 504 - Gateway Timeout
let backOffTimeForRetry = 5; // seconds
let correlationId = `gh-actions-${getUniqueId()}`;
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 == FetchCallType.get){
httpResponse = await httpClient.get(urlSuffix, header);
Expand All @@ -45,23 +47,27 @@ export async function httpClientRetries(urlSuffix : string, header : IHeaders, m
if(httpResponse.message.statusCode!= undefined && httpResponse.message.statusCode >= 300){
CoreUtils.debug(`correlation id : ${correlationId}`);
}
if(httpResponse.message.statusCode!=undefined && [408,429,502,503,504].includes(httpResponse.message.statusCode)){
if(httpResponse.message.statusCode!=undefined && retrriableCodes.includes(httpResponse.message.statusCode)){
if(method == FetchCallType.patch){
backOffTimeForRetry += 60; // extra 60 seconds for patch, basically this happens when the service didnot handle some of the external service dependencies, and the external can take time to recover.
}
let err = await getResultObj(httpResponse);
throw {message : (err && err.error && err.error.message) ? err.error.message : errorCorrection(httpResponse)}; // throwing as message to catch it as err.message
}
return httpResponse;
}
catch(err:any){
if(retries){
let sleeptime = (5-retries)*1000 + Math.floor(Math.random() * 5001);
let sleeptime = backOffTimeForRetry * 1000;
if (log) {
console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime/1000} seconds`);
}
await sleep(sleeptime);
return await httpClientRetries(urlSuffix,header,method,retries-1,data);
}
else{
console.log(err, "\ncorrelationId:" + correlationId);
throw new Error(`Operation did not succeed after 3 retries. Pipeline failed with error : ${err.message}`);
}
}
}
}
8 changes: 3 additions & 5 deletions src/Utils/TaskParametersUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ export class TaskParametersUtil {

private static async setSubscriptionParameters(taskParameters: TaskParameters) {
try {
const cmdArguments = ["account", "show"];
var result: any = await AzCliUtility.execAz(cmdArguments);
var result: any = await AzCliUtility.getAccounts('Subscription');

taskParameters.subscriptionId = result.id;
taskParameters.subscriptionName = result.name;
Expand All @@ -60,8 +59,7 @@ export class TaskParametersUtil {
private static async setEndpointAndScopeParameters(taskParameters: TaskParameters) {
try
{
const cmdArguments = ["cloud", "show"];
var result: any = await AzCliUtility.execAz(cmdArguments);
var result: any = await AzCliUtility.getAccounts('Cloud');
let env = result ? result.name : null;
taskParameters.environment = env ?? EnvironmentConstants.AzurePublicCloud.cloudName;

Expand All @@ -80,4 +78,4 @@ export class TaskParametersUtil {
throw new Error(message);
}
}
}
}
15 changes: 12 additions & 3 deletions src/models/TaskParameters.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
export const publicTokenScope = "https://cnt-prod.loadtesting.azure.com";
export const usGovernmentTokenScope = "https://cnt-prod.loadtesting.azure.us";
export const armPublicTokenScope = "https://management.core.windows.net";
export const armUsGovernmentTokenScope = "https://management.usgovcloudapi.net";
export type ControlPlaneTokenScope = typeof armPublicTokenScope | typeof armUsGovernmentTokenScope;
export type DataPlaneTokenScope = typeof publicTokenScope | typeof usGovernmentTokenScope;

export type AccountType = 'Subscription' | 'Cloud'; // cloud is what the service principal logged into, should be only 1.

export interface TaskParameters {
subscriptionId: string;
subscriptionName: string;
environment: string;
armTokenScope: string;
dataPlaneTokenScope: string;
armTokenScope: ControlPlaneTokenScope;
dataPlaneTokenScope: DataPlaneTokenScope;
resourceId: string;
armEndpoint?: string;
}
}
Loading
Loading