Skip to content
237 changes: 155 additions & 82 deletions src/models/APISupport.ts

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions src/models/PayloadModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -139,6 +165,7 @@ export interface ExistingParams {
secrets: { [key: string]: SecretMetadata | null };
env: { [key: string]: string | null };
passFailCriteria: { [key: string]: PassFailMetric | null };
appComponents: Map<string, string[]>; // 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 {
Expand Down
92 changes: 83 additions & 9 deletions src/models/TaskModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 == ''){
Expand Down Expand Up @@ -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<any>;
this.getAppComponentsAndServerMetricsConfig(appcomponents);
}

if(config.keyVaultReferenceIdentity != undefined || config.keyVaultReferenceIdentityType != undefined) {
this.keyVaultReferenceIdentityType = config.keyVaultReferenceIdentity ? ManagedIdentityTypeForAPI.UserAssigned : ManagedIdentityTypeForAPI.SystemAssigned;
this.keyVaultReferenceIdentity = config.keyVaultReferenceIdentity ?? null;
Expand All @@ -137,20 +148,52 @@ 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+".");
}
this.runTimeParams = this.getRunTimeParams();
Util.validateTestRunParamsFromPipeline(this.runTimeParams);
}

getAppComponentsAndServerMetricsConfig(appComponents: Array<any>) {
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<ServerMetricsClientModel>;

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);
Expand Down Expand Up @@ -255,6 +298,37 @@ 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) {
Expand Down
14 changes: 12 additions & 2 deletions src/models/UtilModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand All @@ -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;
}
31 changes: 31 additions & 0 deletions src/models/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
92 changes: 91 additions & 1 deletion src/models/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)){
Expand Down Expand Up @@ -266,13 +266,41 @@ 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;
}
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.`};
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -472,6 +509,58 @@ export function validateAndGetSegregatedManagedIdentities(referenceIdentities: {
}
return {referenceIdentityValuesUAMIMap, referenceIdentiesSystemAssignedCount};
}
function validateAppComponentAndServerComponents(appComponents: Array<any>) : 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<any>) : {valid : boolean, error : string} {
for(let referenceIdentity of referenceIdentities){
Expand Down Expand Up @@ -524,6 +613,7 @@ export function getPassFailCriteriaFromString(passFailCriteria: (string | {[key:
criteriaString = criteria[request]
}
let tempStr: string = "";

for(let i=0; i<criteriaString.length; i++){
if(criteriaString[i] == '('){
data.aggregate = tempStr.trim();
Expand Down
Loading
Loading