UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

320 lines (281 loc) 10.9 kB
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); } } } } }