@apistudio/apim-cli
Version:
CLI for API Management Products
320 lines (281 loc) • 10.9 kB
text/typescript
import { GatewayLabels } from '@apic/studio-client-model';
import JSZip from 'jszip';
import yaml from 'js-yaml';
import { checkFileExtension } from '../utils.js';
import { isValidAsset, YamlContent, SchemaHandler, Logger, toError } from '@apic/studio-shared';
import { AssetValidator } from '../service/validation-service.js';
export class AssetSchemaValidator {
private commonAssetKinds = [
'API',
'CORS',
'Properties',
'Scope',
'URISchemes',
'Product',
'Plan',
'Quota',
'DataPowerAssembly',
];
public async validateApiFile(
buffer: Buffer,
gatewayTypes: GatewayLabels[]
): Promise<{
valid: boolean;
errors: string[];
}> {
const errors: string[] = [];
try {
const { lwgwReferences, wmgwReferences, dpgwReferences, dpgwv5References } =
await this.categorizePolicySequences(buffer);
if (gatewayTypes.includes(GatewayLabels.WMGW) && wmgwReferences.size === 0) {
const msg = 'WebMethods gateway is selected but no staged policy sequences are referenced';
Logger.error(msg);
errors.push(msg);
}
if (gatewayTypes.includes(GatewayLabels.LWGW) && lwgwReferences.size === 0) {
const msg =
'DataPower Nano gateway is selected but no free flow policy sequences are referenced';
Logger.error(msg);
errors.push(msg);
}
if (gatewayTypes.includes(GatewayLabels.DPGW) && dpgwReferences.size === 0) {
const msg = 'DataPower API gateway is selected but no DataPowerAssembly is referenced';
Logger.error(msg);
errors.push(msg);
}
if (gatewayTypes.includes(GatewayLabels.DPv5GW) && dpgwv5References.size === 0) {
const msg =
'Publishing failed. The policy sequence is not compatible with the selected gateway.';
Logger.error(msg);
errors.push(msg);
}
return {
valid: errors.length === 0,
errors,
};
} catch (err) {
const msg = `Error validating API file: ${err instanceof Error ? err.message : String(err)}`;
Logger.error(msg);
return { valid: false, errors: [msg] };
}
}
private async readAndConsolidateYamlFiles(buffer: Buffer): Promise<YamlContent[]> {
try {
const zip = new JSZip();
const zipContent = await zip.loadAsync(buffer);
const validYamlContents: YamlContent[] = [];
await Promise.all(
Object.keys(zipContent.files).map(async (fileName) => {
const entry = zipContent.files[fileName];
if (!entry || entry.dir || !checkFileExtension(entry.name)) {
return;
}
const content = await entry.async('string');
try {
const yamlContents = yaml.loadAll(content) as YamlContent[];
for (const yamlContent of yamlContents) {
if (isValidAsset(yamlContent, true)) {
validYamlContents.push(yamlContent);
} else {
Logger.warn(`Invalid asset found in ${entry.name}`);
}
}
} catch (yamlError) {
Logger.error(`Error parsing YAML in ${entry.name}:`, toError(yamlError));
}
})
);
return validYamlContents;
} catch (error) {
Logger.error('Error processing buffer:', toError(error));
return [];
}
}
public async categorizePolicySequences(buffer: Buffer): Promise<{
lwgwReferences: Set<string>;
wmgwReferences: Set<string>;
dpgwReferences: Set<string>;
dpgwv5References: Set<string>;
}> {
const lwgwReferences = new Set<string>();
const wmgwReferences = new Set<string>();
const dpgwReferences = new Set<string>();
const dpgwv5References = new Set<string>();
const consolidatedYaml = await this.readAndConsolidateYamlFiles(buffer);
const apiAssets = consolidatedYaml.filter(
(asset) =>
(asset.kind.toUpperCase() === 'API' || asset.kind.toUpperCase() === 'GLOBALPOLICY') &&
asset.spec &&
asset.spec['policy-sequence'] &&
Array.isArray(asset.spec['policy-sequence']) &&
asset.spec['policy-sequence'].length > 0
);
for (const apiAsset of apiAssets) {
const firstPolicyRef = apiAsset.spec['policy-sequence'][0].$ref;
if (typeof firstPolicyRef === 'string') {
const parts = firstPolicyRef.split(':');
const namespace = parts[0];
const name = parts[1];
const version = parts[2];
if (!namespace || !name || !version) continue;
const matchingAsset = consolidatedYaml.find(
(asset) =>
asset.metadata &&
asset.metadata.namespace === namespace &&
asset.metadata.name === name &&
asset.metadata.version === version
);
if (matchingAsset) {
if (matchingAsset.kind.toLowerCase() === 'freeflowpolicysequence') {
lwgwReferences.add(firstPolicyRef);
this.findAllNestedReferences(matchingAsset, consolidatedYaml, lwgwReferences);
} else if (matchingAsset.kind.toLowerCase() === 'stagedpolicysequence') {
wmgwReferences.add(firstPolicyRef);
this.findAllNestedReferences(matchingAsset, consolidatedYaml, wmgwReferences);
} else if (matchingAsset?.spec?.["x-ibm-configuration"]?.gateway === 'datapower-api-gateway') {
dpgwReferences.add(firstPolicyRef);
this.findAllNestedReferences(matchingAsset, consolidatedYaml, dpgwReferences);
} else if (matchingAsset?.spec?.["x-ibm-configuration"]?.gateway === 'datapower-gateway') {
dpgwv5References.add(firstPolicyRef);
}
}
}
}
return { lwgwReferences, wmgwReferences, dpgwReferences, dpgwv5References };
}
private findAllNestedReferences(
asset: YamlContent,
allAssets: YamlContent[],
referenceSet: Set<string>
): void {
if (!asset || !asset.spec) return;
const currentRefs = new Set<string>();
this.findAllRefsRecursive(asset.spec, currentRefs);
for (const ref of currentRefs) {
if (!referenceSet.has(ref)) {
referenceSet.add(ref);
const parts = ref.split(':');
const namespace = parts[0];
const name = parts[1];
const version = parts[1];
if (!namespace || !name || !version) continue;
const referencedAsset = allAssets.find(
(a) =>
a.metadata &&
a.metadata.namespace === namespace &&
a.metadata.name === name &&
a.metadata.version === version
);
if (referencedAsset) {
this.findAllNestedReferences(referencedAsset, allAssets, referenceSet);
}
}
}
}
private findAllRefsRecursive(data: any, referenceSet: Set<string>): void {
if (data == null) return;
if (Array.isArray(data)) {
for (const item of data) {
this.findAllRefsRecursive(item, referenceSet);
}
} else if (typeof data === 'object') {
for (const key of Object.keys(data)) {
const value = data[key];
if (key === '$ref' && typeof value === 'string') {
referenceSet.add(value);
} else {
this.findAllRefsRecursive(value, referenceSet);
}
}
}
}
public async validateSchema(
buffer: Buffer,
gatewayTypes: GatewayLabels[]
): Promise<{
valid: boolean;
errors: string[];
}> {
const apiValidationResult = await this.validateApiFile(buffer, gatewayTypes);
if (!apiValidationResult.valid) {
return apiValidationResult;
}
const errors: string[] = [];
try {
const allAssets = await this.readAndConsolidateYamlFiles(buffer);
const commonSchemaHandler = new SchemaHandler();
const commonValidator = new AssetValidator(commonSchemaHandler);
for (const asset of allAssets) {
if (asset.kind && this.commonAssetKinds.includes(asset.kind)) {
const validationResult = commonValidator.validateAssets(asset);
if (!validationResult.valid) {
errors.push(...validationResult.errors);
}
}
}
if (
gatewayTypes.includes(GatewayLabels.WMGW) &&
gatewayTypes.includes(GatewayLabels.LWGW) &&
gatewayTypes.includes(GatewayLabels.DPGW)
) {
await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.WMGW, errors, buffer);
await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.LWGW, errors, buffer);
await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.DPGW, errors, buffer);
} else if (gatewayTypes.includes(GatewayLabels.WMGW)) {
await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.WMGW, errors, buffer);
} else if (gatewayTypes.includes(GatewayLabels.LWGW)) {
await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.LWGW, errors, buffer);
} else if (gatewayTypes.includes(GatewayLabels.DPGW)) {
await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.DPGW, errors, buffer);
}
return {
valid: errors.length === 0,
errors,
};
} catch (err) {
const msg = `Error validating schema: ${err instanceof Error ? err.message : String(err)}`;
Logger.error(msg);
return { valid: false, errors: [msg] };
}
}
private async validateGatewaySpecificAssets(
assets: YamlContent[],
gatewayType: GatewayLabels,
errors: string[],
buffer: Buffer
): Promise<void> {
const gatewayLabel =
gatewayType === GatewayLabels.WMGW
? 'webMethods'
: gatewayType === GatewayLabels.LWGW
? 'nano'
: gatewayType === GatewayLabels.DPGW
? 'datapower'
: undefined;
if (!gatewayLabel) return;
const { lwgwReferences, wmgwReferences, dpgwReferences } =
await this.categorizePolicySequences(buffer);
const referenceSet =
gatewayType === GatewayLabels.WMGW
? wmgwReferences
: gatewayType === GatewayLabels.LWGW
? lwgwReferences
: dpgwReferences;
const schemaHandler = new SchemaHandler(gatewayLabel);
const validator = new AssetValidator(schemaHandler);
const gatewaySpecificAssets = assets.filter((asset) => {
return !this.commonAssetKinds.includes(asset.kind);
});
for (const asset of gatewaySpecificAssets) {
if (!asset.kind || !asset.metadata) continue;
const assetRef = `${asset.metadata.namespace}:${asset.metadata.name}:${asset.metadata.version}`;
if (referenceSet.has(assetRef)) {
const validationResult = validator.validateAssets(asset);
if (!validationResult.valid) {
const gatewayErrors = validationResult.errors.map((err) => `${err}`);
errors.push(...gatewayErrors);
}
}
}
}
}