oav/lib/apiScenario/apiScenarioRunner.ts

379 строки
11 KiB
TypeScript

import { HttpMethods } from "@azure/core-http";
import { JsonLoader } from "../swagger/jsonLoader";
import { xmsParameterizedHost, xmsSkipUrlEncoding } from "../util/constants";
import { getRandomString } from "../util/utils";
import {
ArmTemplate,
ScenarioDefinition,
Scenario,
Step,
StepArmTemplate,
StepRestCall,
StepRoleAssignment,
} from "./apiScenarioTypes";
import { AzureBuiltInRoles } from "./azureBuiltInRoles";
import { DEFAULT_ROLE_ASSIGNMENT_API_VERSION } from "./constants";
import { EnvironmentVariables, VariableEnv } from "./variableEnv";
const pathVariableRegex = /{([^}]+)}/g;
export interface ApiScenarioRunnerOption {
env: EnvironmentVariables;
client: ApiScenarioRunnerClient;
jsonLoader: JsonLoader;
}
export interface ArmDeployment {
deploymentName: string;
step: StepArmTemplate;
details: {
scope: string;
subscriptionId: string;
resourceGroupName: string;
};
}
export interface Scope {
provisioned?: boolean;
type: ScenarioDefinition["scope"];
prepareSteps: Step[];
cleanUpSteps: Step[];
env: VariableEnv;
}
export interface ApiScenarioClientRequest {
host: string;
method: HttpMethods;
path: string;
pathParameters?: { [paramName: string]: string };
headers: { [headerName: string]: string };
query: { [key: string]: string };
body?: any;
formData?: { [key: string]: { type: string; value: string } };
file?: string;
}
export interface ApiScenarioRunnerClient {
provisionScope(scenarioDef: ScenarioDefinition, scope: Scope): Promise<void>;
prepareScenario(scenario: Scenario, env: VariableEnv): Promise<void>;
createResourceGroup(
armEndpoint: string,
subscriptionId: string,
resourceGroupName: string,
location: string
): Promise<void>;
deleteResourceGroup(
armEndpoint: string,
subscriptionId: string,
resourceGroupName: string
): Promise<void>;
sendRestCallRequest(
request: ApiScenarioClientRequest,
step: StepRestCall,
env: VariableEnv
): Promise<void>;
sendArmTemplateDeployment(
armEndpoint: string,
armTemplate: ArmTemplate,
armDeployment: ArmDeployment,
step: StepArmTemplate,
env: VariableEnv
): Promise<void>;
}
export class ApiScenarioRunner {
private jsonLoader: JsonLoader;
private client: ApiScenarioRunnerClient;
private env: EnvironmentVariables;
private scope: Scope;
private skipResourceGroupOperation: boolean = true;
public constructor(opts: ApiScenarioRunnerOption) {
this.env = opts.env;
this.client = opts.client;
this.jsonLoader = opts.jsonLoader;
}
private async prepareScope(scenarioDef: ScenarioDefinition) {
// Variable scope: ScenarioDef <= RuntimeScope <= Scenario <= Step
const scopeEnv =
// RuntimeScope
new VariableEnv(
// ScenarioDef
new VariableEnv().setBatch(scenarioDef.variables)
).setBatchEnv(this.env);
this.scope = {
type: scenarioDef.scope,
prepareSteps: scenarioDef.prepareSteps,
cleanUpSteps: scenarioDef.cleanUpSteps,
env: scopeEnv,
};
if (
scenarioDef.scope === "ResourceGroup" &&
this.scope.env.get("resourceGroupName") === undefined
) {
this.scope.env.set("resourceGroupName", {
type: "string",
prefix: "apiTest-",
});
this.skipResourceGroupOperation = false;
}
this.generateValueFromPrefix(this.scope.env);
await this.client.provisionScope(scenarioDef, this.scope);
if (!this.skipResourceGroupOperation) {
await this.client.createResourceGroup(
this.scope.env.getRequiredString("armEndpoint"),
this.scope.env.getRequiredString("subscriptionId"),
this.scope.env.getRequiredString("resourceGroupName"),
this.scope.env.getRequiredString("location")
);
}
for (const step of this.scope.prepareSteps) {
await this.executeStep(step, this.scope.env, this.scope);
}
}
private async cleanUpScope(): Promise<void> {
for (const step of this.scope.cleanUpSteps) {
await this.executeStep(step, this.scope.env, this.scope);
}
if (!this.skipResourceGroupOperation) {
await this.client.deleteResourceGroup(
this.scope.env.getRequiredString("armEndpoint"),
this.scope.env.getRequiredString("subscriptionId"),
this.scope.env.getRequiredString("resourceGroupName")
);
}
}
private generateValueFromPrefix(env: VariableEnv) {
for (const [_, v] of env.getVariables()) {
if (v.type === "string" || v.type === "secureString") {
if (v.prefix !== undefined && v.value === undefined) {
v.value = v.prefix + getRandomString();
}
}
}
}
public async execute(scenarioDef: ScenarioDefinition) {
if (this.scope === undefined) {
await this.prepareScope(scenarioDef);
}
for (const scenario of scenarioDef.scenarios) {
try {
const scenarioEnv = new VariableEnv(this.scope.env).setBatch(scenario.variables);
this.generateValueFromPrefix(scenarioEnv);
await this.client.prepareScenario(scenario, scenarioEnv);
for (const step of scenario.steps) {
await this.executeStep(step, scenarioEnv, this.scope);
}
} catch (e) {
throw new Error(
`Failed to execute scenario: ${scenario.scenario}: ${e.message} \n${e.stack}`
);
}
}
await this.cleanUpScope();
}
private async executeStep(step: Step, env: VariableEnv, scope: Scope) {
const stepEnv = new VariableEnv(env).setBatch(step.variables);
this.generateValueFromPrefix(stepEnv);
try {
switch (step.type) {
case "restCall":
await this.executeRestCallStep(step, stepEnv);
break;
case "armTemplateDeployment":
await this.executeArmTemplateStep(step, stepEnv, scope);
break;
case "armRoleAssignment":
await this.executeArmRoleAssignmentStep(step, stepEnv);
break;
}
} catch (e) {
throw new Error(`Failed to execute step ${step.step}: ${e.message} \n${e.stack}`);
}
}
private async executeArmRoleAssignmentStep(step: StepRoleAssignment, env: VariableEnv) {
const parameters = {
scope: step.roleAssignment.scope,
roleAssignmentName: "{{$guid}}",
"api-version": DEFAULT_ROLE_ASSIGNMENT_API_VERSION,
};
const roleDefinitionId =
step.roleAssignment.roleDefinitionId ??
AzureBuiltInRoles.find((r) => r.roleName === step.roleAssignment.roleName)?.roleDefinitionId;
if (roleDefinitionId === undefined) {
throw new Error(
`Cannot find role definition id for role name ${step.roleAssignment.roleName}`
);
}
const req: ApiScenarioClientRequest = {
host: env.getRequiredString("armEndpoint"),
method: "PUT",
path: "/$(scope)/providers/Microsoft.Authorization/roleAssignments/$(roleAssignmentName)",
pathParameters: parameters,
headers: {},
query: { "api-version": DEFAULT_ROLE_ASSIGNMENT_API_VERSION },
body: {
properties: {
roleDefinitionId: `/subscriptions/$(subscriptionId)/providers/Microsoft.Authorization/roleDefinitions/${roleDefinitionId}`,
principalId: step.roleAssignment.principalId,
principalType: step.roleAssignment.principalType ?? "ServicePrincipal",
},
},
};
const newStep: StepRestCall = {
operation: {
parameters: [
{ name: "scope", [xmsSkipUrlEncoding]: true, in: "path" },
{ name: "roleAssignmentName", [xmsSkipUrlEncoding]: true, in: "path" },
],
} as any,
isPrepareStep: step.isPrepareStep,
isCleanUpStep: step.isCleanUpStep,
step: step.step,
variables: step.variables,
secretVariables: step.secretVariables,
requiredVariables: step.requiredVariables,
type: "restCall",
operationId: "RoleAssignments_Create",
responses: {},
parameters: parameters,
authentication: step.authentication,
externalReference: true,
};
await this.client.sendRestCallRequest(req, newStep, env);
}
private async executeRestCallStep(step: StepRestCall, env: VariableEnv) {
let host: string | undefined;
let pathPrefix = "";
if (step.isManagementPlane) {
host = env.getRequiredString("armEndpoint");
} else {
const spec = step.operation!._path._spec;
if (spec.host) {
host = `https://${spec.host}`;
} else {
const xHost = spec[xmsParameterizedHost];
if (xHost) {
host = xHost.hostTemplate.replace(pathVariableRegex, (_, p1) => `$(${p1})`);
if (xHost.useSchemePrefix === undefined || xHost.useSchemePrefix) {
host = `https://${host}`;
}
// for cases where there're path prefix after host, e.g., "{Endpoint}/language"
const pathPrefixIndex = host.search(/[^\/](\/[^\/].*)/g);
if (pathPrefixIndex >= 0) {
pathPrefix = host.substring(pathPrefixIndex + 1);
host = host.substring(0, pathPrefixIndex + 1);
}
} else {
throw new Error("Unknown host");
}
}
}
let req: ApiScenarioClientRequest = {
host,
method: step.operation!._method.toUpperCase() as HttpMethods,
path:
pathPrefix +
step.operation!._path._pathTemplate.replace(pathVariableRegex, (_, p1) => `$(${p1})`),
pathParameters: {},
headers: {},
query: {},
};
for (const p of step.operation!.parameters ?? []) {
const param = this.jsonLoader.resolveRefObj(p);
const paramVal = step.parameters[param.name];
if (paramVal === undefined) {
if (param.required) {
throw new Error(`Parameter value for "${param.name}" is not found in step: ${step.step}`);
} else {
continue;
}
}
switch (param.in) {
case "path":
req.pathParameters![param.name] = paramVal;
break;
case "query":
req.query[param.name] = paramVal;
break;
case "header":
req.headers[param.name] = paramVal;
break;
case "body":
if (param.schema?.format === "binary") {
req.file = paramVal;
} else {
req.body = paramVal;
}
break;
case "formData":
if (req.formData === undefined) {
req.formData = {};
}
req.formData[param.name] = { type: param.type, value: paramVal };
break;
default:
throw new Error(`Unknown parameter: ${param}`);
}
}
await this.client.sendRestCallRequest(req, step, env);
}
private async executeArmTemplateStep(step: StepArmTemplate, env: VariableEnv, scope: Scope) {
const subscriptionId = env.getRequiredString("subscriptionId");
const resourceGroupName = env.getRequiredString("resourceGroupName");
const armDeployment: ArmDeployment = {
deploymentName: `${resourceGroupName}-deploy-${getRandomString()}`,
step,
details: {
scope: scope.type,
subscriptionId,
resourceGroupName,
},
};
await this.client.sendArmTemplateDeployment(
scope.env.getRequiredString("armEndpoint"),
step.armTemplatePayload,
armDeployment,
step,
env
);
}
}