UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

307 lines 14.6 kB
/** * Copyright IBM Corp. 2024, 2025 */ import JSZip from 'jszip'; import yaml from 'js-yaml'; import { addErrorToResponse, isValidAsset, processRef, updateMapWithMetadata, validateMinAssets, checkFileExtension, errorsArray, validateSoapGatewayRestriction, } from '../utils.js'; import { Logger } from '@apic/studio-shared'; import { AppConstants } from '../constants/app.constants.js'; import path from 'path'; import { BuildProjectAssets } from '../build-project-assets.js'; export class ProjectAssetValidator { isYamlFileForFolder(entry, folderName) { return (!entry.dir && entry.name.startsWith(folderName + path.sep) && checkFileExtension(entry.name)); } async loadZipFromBuffer(fileBuffer) { Logger.info('Loading ZIP from buffer'); const zip = new JSZip(); return zip.loadAsync(fileBuffer); } async validateAssetUniqueness(fileBuffer) { const zipContent = await this.loadZipFromBuffer(fileBuffer); const newMap = new Map(); let allValid = true; await Promise.all(Object.keys(zipContent.files).map(async (fileName) => { const entry = zipContent.files[fileName]; if (entry && checkFileExtension(entry.name)) { const content = await entry.async('string'); const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { const namespace = yamlContent.metadata.namespace || 'default'; const name = yamlContent.metadata.name; const version = yamlContent.metadata.version; const kind = yamlContent.kind; const ref = `${namespace}:${name}:${version}`; if (newMap.has(ref)) { const existing = newMap.get(ref); addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, fileName, `Duplicate asset detected: '${name}' in namespace '${namespace}' with version '${version}' and kind '${kind}'. First seen in file '${existing?.fileName}'.`); allValid = false; } else { newMap.set(ref, { fileName, kind }); } } } } })); return allValid; } async createProjectAssetReferenceMap(buffer, folderName, allFolderNames) { Logger.info(`Creating asset reference map for folder: ${folderName}`); const zipContent = await this.loadZipFromBuffer(buffer); const refMap = new Map(); try { const obj = new BuildProjectAssets(); const versionMap = await obj.createVersionProcessingMap(buffer); await this.processYamlFiles(zipContent, folderName, refMap, versionMap); await this.processYamlFiles(zipContent, 'dependencies', refMap, versionMap, false); // if unresolved refs are identified after checking in the project and dependencies folders then check in other folders const hasUnresolvedRefs = Array.from(refMap.values()).some((value) => !value); if (hasUnresolvedRefs) { for (const otherFolderName of allFolderNames) { if (otherFolderName !== folderName && otherFolderName !== 'dependencies') { await this.processYamlFiles(zipContent, otherFolderName, refMap, versionMap, false); } } } Logger.info('Successfully processed YAML files'); } catch (err) { Logger.error('Error processing ZIP', err instanceof Error ? err : new Error(String(err))); } return refMap; } async processYamlFiles(zipContent, folderName, refMap, versionMap, processDependencies = true) { Logger.info(`Processing YAML files in folder: ${folderName}`); await Promise.all(Object.keys(zipContent.files).map(async (fileName) => { const entry = zipContent.files[fileName]; if (entry && this.isYamlFileForFolder(entry, folderName)) { await this.processYamlFile(entry, refMap, processDependencies, versionMap); } })); } async processYamlFile(entry, refMap, processDependencies, versionMap) { Logger.info(`Processing YAML file: ${entry.name}`); const content = await entry.async('string'); try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent) && !AppConstants.IGNORE_ASSETS_DURING_DEPLOY.includes(yamlContent.kind.toLowerCase())) { this.updateReferenceMap(yamlContent, refMap, processDependencies, versionMap); } } Logger.info(`Successfully processed YAML content in file: ${entry.name}`); } catch (err) { Logger.error(`Error parsing YAML in file ${entry.name}`, err instanceof Error ? err : new Error(String(err))); } } updateReferenceMap(yamlContent, refMap, processDependencies, versionMap) { if (processDependencies) { this.extractKey(yamlContent, refMap, '$ref', versionMap, processRef); } updateMapWithMetadata(yamlContent, refMap); } extractKey(yamlContent, refMap, keyToExtract, versionMap, transformValue) { const extract = (obj) => { for (const key in obj) { const value = obj[key]; if (key === keyToExtract && typeof value === 'string') { const transformedValue = transformValue ? transformValue(value) : value; if (versionMap.has(value)) { if (!refMap.has(value)) { refMap.set(value, true); } } else { if (!refMap.has(transformedValue)) { refMap.set(transformedValue, false); } } } else if (typeof value === 'object' && value !== null) { extract(value); } } }; const specOb = JSON.stringify(yamlContent.spec); extract(yaml.load(specOb)); } async createProjectPathReferenceMap(buffer, folderName) { Logger.info(`Creating path reference map for folder: ${folderName}`); const zipContent = await this.loadZipFromBuffer(buffer); const refMap = new Map(); try { const obj = new BuildProjectAssets(); const versionMap = await obj.createVersionProcessingMap(buffer); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (entry && this.isYamlFileForFolder(entry, folderName)) { const content = await entry.async('string'); try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { this.extractKey(yamlContent, refMap, '$path', versionMap, path.normalize); } } Logger.info(`Path extraction completed for file: ${fileName}`); } catch (err) { Logger.error(`Error parsing YAML in file ${fileName}`, err instanceof Error ? err : new Error(String(err))); } } } } catch (err) { Logger.error('Error loading ZIP', err instanceof Error ? err : new Error(String(err))); } return refMap; } async validateApiSpecVaraible(buffer, folderName) { Logger.info(`Validating API Spec variable in folder: ${folderName}`); const zipContent = await this.loadZipFromBuffer(buffer); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (entry && !entry.dir && fileName.startsWith(folderName + path.sep) && checkFileExtension(fileName)) { const content = await entry.async('string'); const isValid = await this.checkYamlContent(content, fileName); if (!isValid) { return false; } } } return true; } async checkYamlContent(content, fileName) { Logger.info(`Checking YAML content in file: ${fileName}`); try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent) && yamlContent.kind?.toLowerCase() === 'api') { const specObj = JSON.stringify(yamlContent.spec); const apiSpec = yaml.load(specObj); if (this.isInvalidApiSpec(apiSpec)) { addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, fileName, `Validation failed for api - spec field in file ${fileName} `); return false; } } } Logger.info(`Successfully validated YAML content in file: ${fileName}`); } catch (err) { Logger.error(`Error parsing YAML in file ${fileName}`, err instanceof Error ? err : new Error(String(err))); return false; } return true; } isInvalidApiSpec(apiSpec) { const apiSpecField = AppConstants.apiSpec; const apiSpecPathLength = apiSpec[apiSpecField]?.$path?.length ?? 0; return (!apiSpec || !apiSpec[apiSpecField] || !apiSpec[apiSpecField].$path || apiSpecPathLength <= 0); } async validateProjectAssetReference(buffer, folderName, allFolderNames) { Logger.info(`Validating project asset references in folder: ${folderName}`); let refMap = new Map(); try { refMap = await this.createProjectAssetReferenceMap(buffer, folderName, allFolderNames); const allRefsValid = Array.from(refMap.entries()).every(([key, value]) => { if (!value) { addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, key, `Validation failed for reference ${key}`); } return value; }); if (!allRefsValid) { Logger.error('Some references are not valid'); return { isValid: false, refMap, errors: errorsArray.map((err) => err.description), }; } return { isValid: true, refMap, errors: [] }; } catch (err) { Logger.error('Error validating asset', err instanceof Error ? err : new Error(String(err))); return { isValid: false, refMap, errors: errorsArray.map((err) => err.description), }; } } async validateProjectPathReference(buffer, folderName, filePathsInFolder) { Logger.info(`Validating project path references in folder: ${folderName}`); try { const refMap = await this.createProjectPathReferenceMap(buffer, folderName); refMap.forEach((_, key) => { if (filePathsInFolder.has(path.normalize(`${folderName}/${key}`))) { refMap.set(key, true); } }); const allRefsValid = Array.from(refMap.entries()).every(([key, value]) => { if (!value) { Logger.error(`Validation failed for path ${key} in ${folderName}`); addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, key, `Validation failed for path ${key} in ${folderName}`); } return value; }); if (!allRefsValid) { Logger.error('Some references are not valid'); return false; } return true; } catch (err) { Logger.error('Error validating asset', err instanceof Error ? err : new Error(String(err))); return false; } } async validateDeploymentAsset(buffer) { try { const zipContent = await this.loadZipFromBuffer(buffer); Logger.debug(`Processing ${Object.keys(zipContent.files).length} files`); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (entry && checkFileExtension(fileName)) { const content = await entry.async('string'); const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { return true; } } } } } catch (err) { addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'ZIP_FILE', `Error loading zip with minimum assets: ${err}`); } addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'ZIP_FILE', 'Error loading zip with minimum assets required for deploymnet'); return false; } async validateProjectHasMinimumAssets(buffer) { Logger.info('Validating project has minimum assets'); return (await validateMinAssets(buffer)) && (await this.validateDeploymentAsset(buffer)); } async validateProjectApiSpecVariable(buffer, folderName) { Logger.info(`Validating project API spec variables in folder: ${folderName}`); return this.validateApiSpecVaraible(buffer, folderName); } async validateSoapApiGatewayRestriction(buffer) { Logger.info('Validating SOAP API gateway restrictions'); const result = await validateSoapGatewayRestriction(buffer); if (!result.isValid && result.errors.length > 0) { result.errors.forEach((error) => { addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'SOAP_API_TO_GATEWAY', error); }); } return result.isValid; } } //# sourceMappingURL=asset-validator.js.map