UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

922 lines (837 loc) 29.4 kB
import { getDefaultVersions } from "../resources/smith-defaultVersion.js"; import { getMasterContent } from "../resources/smith-master.js"; import { getCombinedRuleset } from "../resources/smith-ruleset.js"; import { getCombinedSource } from "../resources/smith-schemas-json.js"; import { IRuntimeInventory, MasterContent, PolicyInfo, AssetInfo, StagedPolicyGroup } from "./interfaces/IRuntimeInventory.js"; const POLICY_SEQUENCES = "policy-sequences"; const STAGED = "staged"; const FREE_FLOW = "free-flow"; const POLICIES = "policies"; const DEFAULT_VERSION_KEY = "defaultVersion"; export enum CommonKind { Api = "api", Plan = "plan", Product = "product", TestApi = "testapi", Tuple = "tuple", UriSchemes = "urischemes" } const kindEnums = [ "API", "Scope", "Project", "StagedPolicySequence", "InvokeAWSLambda", "ValidateAPISpecification", "CORS", "Quota", "Plan", "Product", "URISchemes", "properties", "Telemetry", "Properties", "LoadBalancer", "SetAuthorization", "Invoke", "GlobalPolicy", "InboundBulkHead", "SetMediaType", "InboundMessaging", "IAM", "AuthorizeUser", "SetContextVariable", "WebMethodsISService", "Log", "MonitorTraffic", "CacheServiceResult", "OutboundAlias", "OutboundAnonymous", "HTTPInvoke", "InvokeMessagingExtension", "DataMasking", "TransformRequest", "TransformResponse", "Route", "MessageConfig", "HTTPEndpoint", "MockEndpoint", "MockResponse", "ErrorProcessing", "Set", "RateLimitDef", "RateLimit", "Redact", "Remove", "Transform", "DataPowerAssembly", "Switch", "If", "OperationSwitch", "Try", "IBMCloudLogin", "WatsonXAIInvoke", "OpenAIInvoke", "FreeFlowPolicySequence", "Block", "TokenMediation", "EnforceCircuitBreaker", "JavaScript", "LuaScript", "Cache", "Antivirus", "SQLInjectionFilter", "CountLimit", "CountLimitDef", "Return", "Retry", "Throw", "HandlebarsTemplate", "ExtractIdentity", "Authorize", "Authenticate", "Parse", "test", "assertion", "environment" ]; const generic_rules = { "rules": { "invalid-kind-value-combined": { "description": `Kind must be one of ${kindEnums.map(k => `'${k}'`).join(' | ')}`, "severity": "error", "given": "$", "then": { "field": "kind", "function": "schema", "functionOptions": { "schema": { "type": "string", "enum": kindEnums } } } }, "kind-not-exist": { "description": `Kind does not exist.`, "severity": "error", "given": "$", "resolved": false, "then": { "field": "kind", "function": "truthy" } }, "invalid-kind-spl-character": { "description": "kind should not be having empty or special characters", "severity": "error", "given": "$", "resolved": false, "then": { "field": "kind", "function": "pattern", "functionOptions": { "match": "^(?![\\s\\W_]+$).+$" } } }, "invalid-api-version": { "description": "apiVersion must be one of the valid values 'api.ibm.com/v1'", "severity": "error", "given": "$", "resolved": false, "then": { "field": "apiVersion", "function": "schema", "functionOptions": { "schema": { "type": "string", "enum": [ "api.ibm.com/v1" ] } } } }, "api-version-not-exist": { "description": "apiVersion does not exist.", "severity": "error", "given": "$", "resolved": false, "then": { "field": "apiVersion", "function": "truthy" } }, "metadata-not-exist": { "description": "Metadata does not exist.", "severity": "error", "given": "$", "resolved": false, "then": { "field": "metadata", "function": "truthy" } }, "metadata-whitelist-check": { "description": "Metadata should not be having empty or special characters", "severity": "error", "given": "$", "resolved": false, "then": { "field": "metadata", "function": "pattern", "functionOptions": { "match": "^(?![\\s\\W_]+$).+$" } } }, "metadata-name-not-exist": { "description": "Metadata name does not exist", "severity": "error", "given": "$.metadata", "resolved": false, "then": { "field": "name", "function": "truthy" } }, "metadata-name-whitelist-check": { "description": "Metadata name should not be having empty or special characters", "severity": "error", "given": "$.metadata", "resolved": false, "then": { "field": "name", "function": "pattern", "functionOptions": { "match": "^(?![\\s\\W_]+$).+$" } } }, "metadata-version-not-exist": { "description": "Metadata version does not exist", "severity": "error", "given": "$.metadata", "resolved": false, "then": { "field": "version", "function": "truthy" } }, "metadata-version-whitelist-check": { "description": "Metadata version should not be having empty or special characters", "severity": "error", "given": "$.metadata", "resolved": false, "then": { "field": "version", "function": "pattern", "functionOptions": { "match": "^(?![\\s\\W_]+$).+$" } } }, "metadata-namespace-not-exist": { "description": "Metadata namespace does not exist", "severity": "warn", "given": "$.metadata", "resolved": false, "then": { "field": "namespace", "function": "truthy" } }, "metadata-namespace-whitelist-check": { "description": "Metadata namespace should not be having empty or special characters", "severity": "warn", "given": "$.metadata", "resolved": false, "then": { "field": "namespace", "function": "pattern", "functionOptions": { "match": "^(?![\\s\\W_]+$).+$" } } }, "spec-details-not-exist": { "description": "Spec details does not exist", "severity": "error", "given": "$", "resolved": false, "then": { "field": "spec", "function": "truthy" } }, "spec-details-whitelist-check": { "description": "Spec should not be having empty or special characters", "severity": "error", "given": "$", "resolved": false, "then": { "field": "spec", "function": "pattern", "functionOptions": { "match": "^(?![\\s\\W_]+$).+$" } } }, "tags-not-exist": { "description": "Tag does not exist", "severity": "warn", "given": "$.metadata", "resolved": false, "then": { "field": "tags", "function": "truthy" } }, "invalid-tag-type": { "description": "Invalid Tag Type", "severity": "error", "given": "$.metadata.tags", "resolved": false, "then": { "function": "schema", "functionOptions": { "schema": { "type": "array" } } } } } }; export type CommonKindType = `${CommonKind}`; export class RuntimeInventory implements IRuntimeInventory { public masterContent: MasterContent; public schemaDefinitions: Record<string, any>; public defaultVersionMap: Record<string, string>; private rulesetDefinitions: Record<string, any>; constructor() { try { // Initialize with the imported data this.schemaDefinitions = getCombinedSource(); this.rulesetDefinitions = getCombinedRuleset(); this.defaultVersionMap = getDefaultVersions(); this.masterContent = getMasterContent(); // Log successful initialization } catch (error) { this.schemaDefinitions = {}; this.rulesetDefinitions = {}; this.defaultVersionMap = {}; this.masterContent = {}; } } /** * Overrides the master content with new data * @param newMasterContent - New master content to replace the existing data */ public setMasterContent(newMasterContent: MasterContent): void { if (!newMasterContent) return; this.masterContent = newMasterContent; } /** * Extends or overrides the master content with custom data * @param customMasterContent - Custom master content to merge with existing data * @param overrideExisting - If true, will override existing entries; if false, will only add new entries */ public extendMasterContent(customMasterContent: Partial<MasterContent>, overrideExisting: boolean = false): void { if (!customMasterContent) return; // For complete replacement, use setMasterContent instead if (overrideExisting && Object.keys(customMasterContent).length > 0) { this.setMasterContent(customMasterContent as MasterContent); return; } // Handle policy sequences if (customMasterContent['policy-sequences']) { if (!this.masterContent['policy-sequences']) { this.masterContent['policy-sequences'] = {}; } // Handle staged policies if (customMasterContent['policy-sequences']?.staged) { if (!this.masterContent['policy-sequences']?.staged) { if (!this.masterContent['policy-sequences']) { this.masterContent['policy-sequences'] = {}; } this.masterContent['policy-sequences'].staged = []; } const stagedPolicies = this.masterContent['policy-sequences'].staged as StagedPolicyGroup[]; // For each custom staged policy group customMasterContent['policy-sequences'].staged.forEach(customGroup => { if (!customGroup.key) return; // Find if this group already exists const existingGroupIndex = stagedPolicies.findIndex( group => group.key === customGroup.key ); if (existingGroupIndex >= 0 && overrideExisting) { // Replace existing group stagedPolicies[existingGroupIndex] = customGroup; } else if (existingGroupIndex < 0) { // Add new group stagedPolicies.push(customGroup); } else if (existingGroupIndex >= 0 && !overrideExisting && customGroup.assets) { // Merge assets into existing group const existingGroup = stagedPolicies[existingGroupIndex]; if (!existingGroup.assets) existingGroup.assets = []; customGroup.assets.forEach(asset => { const existingAssetIndex = existingGroup.assets!.findIndex(a => a.kind === asset.kind); if (existingAssetIndex >= 0 && overrideExisting) { existingGroup.assets![existingAssetIndex] = asset; } else if (existingAssetIndex < 0) { existingGroup.assets!.push(asset); } }); } }); } // Handle free-flow policies if (customMasterContent['policy-sequences']?.['free-flow']) { if (!this.masterContent['policy-sequences']?.['free-flow']) { if (!this.masterContent['policy-sequences']) { this.masterContent['policy-sequences'] = {}; } this.masterContent['policy-sequences']['free-flow'] = []; } const freeFlowPolicies = this.masterContent['policy-sequences']['free-flow'] as any[]; customMasterContent['policy-sequences']['free-flow'].forEach(customGroup => { if (!customGroup.name) return; const existingGroupIndex = freeFlowPolicies.findIndex( group => group.name === customGroup.name ); if (existingGroupIndex >= 0 && overrideExisting) { freeFlowPolicies[existingGroupIndex] = customGroup; } else if (existingGroupIndex < 0) { freeFlowPolicies.push(customGroup); } }); } } // Handle other top-level properties Object.entries(customMasterContent).forEach(([key, value]) => { if (key !== 'policy-sequences') { if (overrideExisting || !this.masterContent[key]) { this.masterContent[key] = value; } } }); } /** * Extends or overrides the schema definitions with custom schemas * @param customSchemas - Custom schemas to merge with existing schemas * @param overrideExisting - If true, will override existing schemas; if false, will only add new schemas */ public extendSchemaDefinitions(customSchemas: Record<string, any>, overrideExisting: boolean = false): void { if (!customSchemas) return; Object.entries(customSchemas).forEach(([key, value]) => { if (overrideExisting || !this.schemaDefinitions[key]) { this.schemaDefinitions[key] = value; } }); } /** * Extends or overrides the default versions with custom default versions * @param customDefaultVersions - Custom default versions to merge with existing versions * @param overrideExisting - If true, will override existing versions; if false, will only add new versions */ public extendDefaultVersions(customDefaultVersions: Record<string, string>, overrideExisting: boolean = false): void { if (!customDefaultVersions) return; Object.entries(customDefaultVersions).forEach(([key, value]) => { if (overrideExisting || !this.defaultVersionMap[key]) { this.defaultVersionMap[key] = value; } }); } /** * Extends or overrides the ruleset definitions with custom rulesets * @param customRulesets - Custom rulesets to merge with existing rulesets * @param overrideExisting - If true, will override existing rulesets; if false, will only add new rulesets */ public extendRulesetDefinitions(customRulesets: Record<string, any>, overrideExisting: boolean = false): void { if (!customRulesets) return; Object.entries(customRulesets).forEach(([key, value]) => { if (overrideExisting || !this.rulesetDefinitions[key]) { this.rulesetDefinitions[key] = value; } }); } /** * Hook method for subclasses to provide overridden schema for a specific schema key * This is called automatically by getSchema() to check if there's an override * Extended classes can override this to provide custom schemas * @param schemaKey - The schema key (e.g., "api.ibm.com_v1_customkind.json") * @returns The overridden schema object or undefined if no override */ public getOverriddenSchema(schemaKey: string): any | undefined { // Base implementation returns undefined (no override) // Subclasses can override to provide custom schemas return undefined; } /** * Hook method for subclasses to provide overridden ruleset for a specific ruleset key * This is called automatically by getLintRuleset() to check if there's an override * Extended classes can override this to provide custom rulesets * @param rulesetKey - The ruleset key (e.g., "api.ibm.com_v1_customkind.ruleset.yaml") * @returns The overridden ruleset object or undefined if no override */ public getOverriddenRule(rulesetKey: string): any | undefined { // Base implementation returns undefined (no override) // Subclasses can override to provide custom rulesets return undefined; } public getSchema( name: string, version?: string ): any | undefined { try { const kindLower = name.toLowerCase(); if (!version) { // Get default version from the defaultVersion.json const matchedKey = Object.keys(this.defaultVersionMap).find( (key) => key.toLowerCase() === name.toLowerCase() ); if (matchedKey) { version = this.defaultVersionMap[matchedKey]; } else { } if (!version) { return undefined; } } // Format the key to match the combined-source.json format version = version.replace(/\//g, "_"); const schemaKey = `${version}_${kindLower}.json`; if(this.getOverriddenSchema(schemaKey)) { return this.getOverriddenSchema(schemaKey); } else if (this.schemaDefinitions[schemaKey]) { return this.schemaDefinitions[schemaKey]; } else { return undefined; } } catch (error) { console.error( `Error retrieving schema for ${name} version ${version}:`, error ); return undefined; } } public getSchemaFromDestination( name: string, version?: string ): any | undefined { try { const kindLower = name.toLowerCase(); if (!version) { // Get default version from the defaultVersion.json const matchedKey = Object.keys(this.defaultVersionMap).find( (key) => key.toLowerCase() === name.toLowerCase() ); if (matchedKey) { version = this.defaultVersionMap[matchedKey]; } if (!version) { return undefined; } } // Format the key to match the combined-source.json format version = version.replace(/\//g, "_"); const schemaKey = `${version}_${kindLower}.json`; // For destination schemas, we would need to implement a similar approach // Currently, destination schemas are not included in combined-source.json // This is a placeholder for future implementation return undefined; } catch (error) { console.error( `Error retrieving schema for ${name} version ${version} in destination:`, error ); return undefined; } } public getTypescript(name: string, version?: string): Record<string, any> | undefined { try { const kindLower = name.toLowerCase(); if (!version) { // Get default version from the defaultVersion.json const matchedKey = Object.keys(this.defaultVersionMap).find( (key) => key.toLowerCase() === name.toLowerCase() ); if (matchedKey) { version = this.defaultVersionMap[matchedKey]; } if (!version) { return undefined; } } // Format the key to match the combined-source.json format version = version.replace(/\//g, "_"); const schemaKey = `${version}_${kindLower}.json`; // TypeScript definitions are not included in combined-source.json // This is a placeholder for future implementation return undefined; } catch (error) { console.error( `Error retrieving TypeScript for ${name} version ${version}:`, error ); return undefined; } } public getLintRuleset(name: string, version?: string): Record<string, any> | undefined { try { const kindLower = name.toLowerCase(); if (!version) { // Get default version from the defaultVersion.json const matchedKey = Object.keys(this.defaultVersionMap).find( (key) => key.toLowerCase() === name.toLowerCase() ); if (matchedKey) { version = this.defaultVersionMap[matchedKey]; } if (!version) { return undefined; } } // Format the key to match the combined ruleset format version = version.replace(/\//g, "_"); const rulesetKey = `${version}_${kindLower}.ruleset.yaml`; if(this.getOverriddenRule(rulesetKey)) { return this.getOverriddenRule(rulesetKey) } // Look for the ruleset in the ruleset definitions else if (this.rulesetDefinitions[rulesetKey]) { // Return the ruleset object directly return this.rulesetDefinitions[rulesetKey]; } else { return generic_rules; } } catch (error) { console.error( `Error retrieving lint ruleset for ${name} version ${version}:`, error ); return undefined; } } public getPolicySequenceType(): { sequenceTypes: string[] } | undefined { try { const sequenceTypes: string[] = []; const stagedPolicies = this.getStagedPolicies(); if (stagedPolicies && Object.keys(stagedPolicies).length > 0) { sequenceTypes.push(STAGED); } const freeFlowPolicies = this.getFreeFlowPolicies(); if (freeFlowPolicies && Object.keys(freeFlowPolicies).length > 0) { sequenceTypes.push(FREE_FLOW); } return sequenceTypes.length > 0 ? { sequenceTypes } : undefined; } catch (error) { console.error("Error getting policy sequence types:", error); return undefined; } } public getStagedPolicies(): | Record<string, { stage: string; policies: PolicyInfo[] }> | undefined { try { const staged = this.masterContent?.[POLICY_SEQUENCES]?.[STAGED]; if (staged && Array.isArray(staged)) { const result: Record< string, { stage: string; policies: PolicyInfo[] } > = {}; for (const stageGroup of staged) { const stageName = stageGroup.key; const assets = stageGroup.assets || []; result[stageName] = { stage: stageName, policies: assets.map((asset: AssetInfo) => ({ name: asset.kind, defaultVersion: asset.defautlVersion || "api.ibm.com/v1", type: STAGED, })), }; } return result; } return undefined; } catch (error) { console.error("Error getting staged policies:", error); return undefined; } } public getFreeFlowPolicies(): | Record<string, { group: string; type: string; policies: PolicyInfo[] }> | undefined { try { const freeFlow = this.masterContent?.[POLICY_SEQUENCES]?.[FREE_FLOW]; if (!Array.isArray(freeFlow)) { return undefined; } const result: Record< string, { group: string; type: string; policies: PolicyInfo[] } > = {}; freeFlow.forEach((group, index) => { const groupName = group.name || `group_${index}`; const flattenedPolicies = this.flattenPolicies(group[POLICIES] || []); result[groupName] = { group: groupName, type: group.type, policies: flattenedPolicies.map((policy: any) => ({ name: policy.name, defaultVersion: policy[DEFAULT_VERSION_KEY] || "1.0.0", type: FREE_FLOW, })), }; }); return result; } catch (error) { console.error("Error getting free flow policies:", error); return undefined; } } public getPolicyDefaultVersion( sequenceType: typeof STAGED | typeof FREE_FLOW, groupName: string, policyName: string ): string | undefined { try { if (sequenceType === STAGED) { const stagedGroups = this.masterContent[POLICY_SEQUENCES]?.[STAGED]; if (stagedGroups && Array.isArray(stagedGroups)) { const group = stagedGroups.find(g => g.key === groupName); if (group && group.assets) { const asset = group.assets.find(a => a.kind === policyName); if (asset) { return asset.defautlVersion || "api.ibm.com/v1"; } } } } else if (sequenceType === FREE_FLOW) { const freeFlow = this.masterContent[POLICY_SEQUENCES]?.[FREE_FLOW]; if (Array.isArray(freeFlow)) { for (const group of freeFlow) { if (group.name === groupName && group[POLICIES]) { for (const policy of this.flattenPolicies(group[POLICIES])) { if (policy.name === policyName) { return policy[DEFAULT_VERSION_KEY] || "1.0.0"; } } } } } } return undefined; } catch (error) { console.error( `Error getting policy default version for ${sequenceType}/${groupName}/${policyName}:`, error ); return undefined; } } public getMasterContents(): MasterContent { // Create a copy of masterContent to ensure we don't modify the original const masterContentCopy = { ...this.masterContent }; // Ensure assetProperties is included in the returned object if (this.masterContent.assetProperties) { masterContentCopy.assetProperties = this.masterContent.assetProperties; } return masterContentCopy; } /** * Get the complete default versions mapping * @returns A record mapping kind names to their default API versions */ public getDefaultVersions(): Record<string, string> { // Return a copy of the default versions map to prevent direct modification return { ...this.defaultVersionMap }; } /** * Get the list of required kinds * @returns An array of required kind names */ public getRequiredKinds(): string[] { // Return the requiredKinds array from masterContent, or an empty array if not defined return this.masterContent.requiredKinds || []; } /** * Get the list of optional kinds * @returns An array of optional kind names */ public getOptionalKinds(): string[] { // Return the optionalKinds array from masterContent, or an empty array if not defined return this.masterContent.optionalKinds || []; } public getPolicyInfo( sequenceType: typeof STAGED | typeof FREE_FLOW, groupName: string, policyName: string ) { try { if (sequenceType === STAGED) { const stagedGroups = this.masterContent[POLICY_SEQUENCES]?.[STAGED]; if (stagedGroups && Array.isArray(stagedGroups)) { const group = stagedGroups.find(g => g.key === groupName); if (group && group.assets) { const asset = group.assets.find(a => a.kind === policyName); if (asset) { return { name: policyName, sequenceType: STAGED, group: groupName, defaultVersion: asset.defautlVersion || "api.ibm.com/v1", policy: asset, }; } } } } else if (sequenceType === FREE_FLOW) { const freeFlow = this.masterContent[POLICY_SEQUENCES]?.[FREE_FLOW]; if (Array.isArray(freeFlow)) { for (const group of freeFlow) { if (group.name === groupName && group[POLICIES]) { for (const policy of this.flattenPolicies(group[POLICIES])) { if (policy.name === policyName) { return { name: policyName, sequenceType: FREE_FLOW, group: groupName, defaultVersion: policy[DEFAULT_VERSION_KEY] || "1.0.0", policy, }; } } } } } } return undefined; } catch (error) { console.error( `Error getting policy info for ${sequenceType}/${groupName}/${policyName}:`, error ); return undefined; } } private flattenPolicies(policies: any[]): any[] { let result: any[] = []; for (const item of policies) { if (item.type === "policy") { result.push(item); } else if (item.type === "group" && item[POLICIES]) { result = result.concat(this.flattenPolicies(item[POLICIES])); } } return result; } } export const runtimeInventory = new RuntimeInventory();