diff --git a/src/commands/iascable-build.ts b/src/commands/iascable-build.ts index d8908c2..7af0683 100644 --- a/src/commands/iascable-build.ts +++ b/src/commands/iascable-build.ts @@ -4,7 +4,7 @@ import {promises} from 'fs'; import {default as jsYaml} from 'js-yaml'; import {dirname, join} from 'path'; -import {IascableInput} from './inputs/iascable.input'; +import {IascableBuild} from './inputs/iascable.input'; import {CommandLineInput} from './inputs/command-line.input'; import {BillOfMaterialModel, isTileConfig, OutputFile, TerraformComponent, Tile} from '../models'; import { @@ -79,7 +79,7 @@ export const builder = (yargs: Argv) => { }); }; -export const handler = async (argv: Arguments) => { +export const handler = async (argv: Arguments) => { process.env.LOG_LEVEL = argv.debug ? 'debug' : 'info'; const cmd: IascableApi = Container.get(IascableApi); @@ -107,7 +107,7 @@ export const handler = async (argv: Arguments) } }; -function buildCatalogBuilderOptions(input: IascableInput): IascableOptions { +function buildCatalogBuilderOptions(input: IascableBuild): IascableOptions { const tileConfig = { label: input.tileLabel, name: input.name, diff --git a/src/commands/iascable-validate.ts b/src/commands/iascable-validate.ts new file mode 100644 index 0000000..d6a580a --- /dev/null +++ b/src/commands/iascable-validate.ts @@ -0,0 +1,71 @@ +import {Container} from 'typescript-ioc'; +import {Arguments, Argv} from 'yargs'; +import {promises} from 'fs'; +import {default as jsYaml} from 'js-yaml'; +import {dirname, join} from 'path'; + +import {IascableBuild, IascableValidate} from './inputs/iascable.input'; +import {CommandLineInput} from './inputs/command-line.input'; +import {BillOfMaterialModel, isTileConfig, OutputFile, TerraformComponent, Tile} from '../models'; +import { + IascableApi, + IascableOptions, + IascableResult, + loadBillOfMaterialFromFile, + loadReferenceBom +} from '../services'; +import {LoggerApi} from '../util/logger'; +import {isUndefined} from '../util/object-util'; + +export const command = 'validate'; +export const desc = 'Validate the provided bill of material against the catalog'; +export const builder = (yargs: Argv) => { + return yargs + .option('catalogUrl', { + alias: 'u', + description: 'The url of the module catalog. Can be https:// or file:/ protocol.', + default: 'https://cloud-native-toolkit.github.io/garage-terraform-modules/index.yaml' + }) + .option('input', { + alias: 'i', + description: 'The path to the bill of materials to use as input', + conflicts: 'reference', + demandOption: false, + }) + .option('reference', { + alias: 'r', + description: 'The reference BOM to use for the build', + conflicts: 'input', + demandOption: false, + }) + .option('debug', { + type: 'boolean', + describe: 'Flag to turn on more detailed output message', + }); +}; + +export const handler = async (argv: Arguments) => { + process.env.LOG_LEVEL = argv.debug ? 'debug' : 'info'; + + const cmd: IascableApi = Container.get(IascableApi); + const logger: LoggerApi = Container.get(LoggerApi).child('build'); + + const bom: BillOfMaterialModel | undefined = argv.reference + ? await loadReferenceBom(argv.reference, '') + : await loadBillOfMaterialFromFile(argv.input, ''); + + if (isUndefined(bom)) { + console.log('No BOM found to validate!'); + return; + } + + const name = bom?.metadata?.name || 'component'; + console.log('Validating:', name); + + const results = await cmd.validate(argv.catalogUrl, bom); + + console.log('Results:'); + results.forEach(result => { + console.log(' Module: ', result); + }) +}; diff --git a/src/commands/inputs/iascable.input.ts b/src/commands/inputs/iascable.input.ts index 3ff6c35..91bcc7a 100644 --- a/src/commands/inputs/iascable.input.ts +++ b/src/commands/inputs/iascable.input.ts @@ -1,8 +1,11 @@ -export interface IascableInput { +export interface IascableValidate { catalogUrl: string; input?: string; reference?: string; +} + +export interface IascableBuild extends IascableValidate{ ci: boolean; prompt: boolean; platform?: string; diff --git a/src/services/iascable.api.ts b/src/services/iascable.api.ts index 0da9352..2b037b2 100644 --- a/src/services/iascable.api.ts +++ b/src/services/iascable.api.ts @@ -14,4 +14,5 @@ export interface IascableOptions { export abstract class IascableApi { abstract build(catalogUrl: string, input?: BillOfMaterialModel, options?: IascableOptions): Promise; + abstract validate(catalogUrl: string, input: BillOfMaterialModel): Promise>; } diff --git a/src/services/iascable.impl.ts b/src/services/iascable.impl.ts index d3aace8..99c87f3 100644 --- a/src/services/iascable.impl.ts +++ b/src/services/iascable.impl.ts @@ -48,4 +48,10 @@ export class CatalogBuilder implements IascableApi { tile, }; } + + async validate(catalogUrl: string, input: BillOfMaterialModel): Promise> { + const catalog: Catalog = await this.loader.loadCatalog(catalogUrl); + + return this.moduleSelector.validateBillOfMaterial(catalog, input); + } } diff --git a/src/services/module-selector/module-selector.api.ts b/src/services/module-selector/module-selector.api.ts index 1654546..86d3c80 100644 --- a/src/services/module-selector/module-selector.api.ts +++ b/src/services/module-selector/module-selector.api.ts @@ -1,9 +1,15 @@ import {CatalogModel} from '../catalog-loader'; -import {BillOfMaterialModel} from '../../models/bill-of-material.model'; +import {BillOfMaterialModel, BillOfMaterialModule} from '../../models/bill-of-material.model'; import {SingleModuleVersion} from '../../models/module.model'; export abstract class ModuleSelectorApi { - abstract buildBillOfMaterial(fullCatalog: CatalogModel, input?: BillOfMaterialModel, filter?: {platform?: string, provider?: string}): Promise; + abstract buildBillOfMaterial(fullCatalog: CatalogModel, input?: BillOfMaterialModel, filter?: { platform?: string, provider?: string }): Promise; + abstract resolveBillOfMaterial(fullCatalog: CatalogModel, input: BillOfMaterialModel): Promise; - abstract validateBillOfMaterialModuleConfigYaml(fullCatalog: CatalogModel, moduleRef: string, yaml: string): Promise; + + abstract validateBillOfMaterialModuleConfigYaml(fullCatalog: CatalogModel, moduleRef: string, yaml: string): Promise; + + abstract validateBillOfMaterial(catalogModel: CatalogModel, bom: BillOfMaterialModel): Promise>; + + abstract validateBillOfMaterialModuleConfig(catalogModel: CatalogModel, moduleConfig: BillOfMaterialModule): Promise; } diff --git a/src/services/module-selector/module-selector.impl.ts b/src/services/module-selector/module-selector.impl.ts index 141de30..9709b12 100644 --- a/src/services/module-selector/module-selector.impl.ts +++ b/src/services/module-selector/module-selector.impl.ts @@ -20,6 +20,7 @@ import {QuestionBuilderImpl} from '../../util/question-builder/question-builder. import {LoggerApi} from '../../util/logger'; import {BillOfMaterialModuleConfigError, ModuleNotFound} from '../../errors'; import {of as arrayOf} from '../../util/array-util'; +import {isDefinedAndNotNull, isUndefined} from '../../util/object-util'; export class ModuleSelector implements ModuleSelectorApi { logger: LoggerApi; @@ -133,9 +134,30 @@ export class ModuleSelector implements ModuleSelectorApi { return new SelectedModules(fullCatalog).resolveModules(modules); } - async validateBillOfMaterialModuleConfigYaml(catalogModel: CatalogModel, moduleRef: string, yaml: string) { + async validateBillOfMaterialModuleConfigYaml(catalogModel: CatalogModel, moduleRef: string, yaml: string): Promise { + const moduleConfig: BillOfMaterialModule = jsYaml.load(yaml) as any; + + return this.validateBillOfMaterialModuleConfig(catalogModel, moduleConfig); + } + + async validateBillOfMaterial(catalogModel: CatalogModel, bom: BillOfMaterialModel): Promise> { + const result = Promise + .all(BillOfMaterial.getModules(bom).map(m => { + return this.validateBillOfMaterialModuleConfig(catalogModel, m); + })) + .then((result: Array) => result.filter(isDefinedAndNotNull)); + + return result; + } + + async validateBillOfMaterialModuleConfig(catalogModel: CatalogModel, moduleConfig: BillOfMaterialModule): Promise { const fullCatalog: Catalog = Catalog.fromModel(catalogModel); + const moduleRef: string | undefined = moduleConfig.name || moduleConfig.id; + if (isUndefined(moduleRef)) { + throw new ModuleNotFound('unknown'); + } + const module: Module | undefined = fullCatalog.lookupModule({id: moduleRef, name: moduleRef}); if (!module) { throw new ModuleNotFound(moduleRef); @@ -148,7 +170,6 @@ export class ModuleSelector implements ModuleSelectorApi { .map(v => v.id) .asArray(); - const moduleConfig: BillOfMaterialModule = jsYaml.load(yaml) as any; const unmatchedVariableNames: string[] = arrayOf(moduleConfig.variables) .filter(v => !availableVariableNames.includes(v.name)) .map(v => v.name) @@ -161,6 +182,8 @@ export class ModuleSelector implements ModuleSelectorApi { if (unmatchedVariableNames.length > 0 || unmatchedDependencyNames.length > 0) { throw new BillOfMaterialModuleConfigError({unmatchedVariableNames, unmatchedDependencyNames, availableVariableNames, availableDependencyNames}); } + + return moduleRef; } }