From 6f15d1c6218b4821941e49246fb0596a7015772c Mon Sep 17 00:00:00 2001 From: ssube Date: Sat, 22 Jun 2019 11:48:41 -0500 Subject: [PATCH] feat: display diff when rule modifies data (fixes #3) --- config/rollup.js | 3 ++ examples/kubernetes-resources-low.yml | 1 - package.json | 2 ++ rules/kubernetes.yml | 12 +++++--- src/index.ts | 44 +++++++++++++++++---------- src/rule.ts | 13 +++++--- src/visitor/context.ts | 4 +++ src/visitor/index.ts | 2 +- yarn.lock | 10 ++++++ 9 files changed, 63 insertions(+), 28 deletions(-) diff --git a/config/rollup.js b/config/rollup.js index b5e721e..da6ed90 100644 --- a/config/rollup.js +++ b/config/rollup.js @@ -35,6 +35,9 @@ export default { }), commonjs({ namedExports: { + 'node_modules/deep-diff/index.js': [ + 'diff', + ], 'node_modules/lodash/lodash.js': [ 'cloneDeep', 'intersection', diff --git a/examples/kubernetes-resources-low.yml b/examples/kubernetes-resources-low.yml index 5d6c7c3..e0e0edf 100644 --- a/examples/kubernetes-resources-low.yml +++ b/examples/kubernetes-resources-low.yml @@ -7,7 +7,6 @@ spec: - name: test resources: limits: - cpu: 2m memory: 5Mi requests: cpu: 1m diff --git a/package.json b/package.json index 2479cf4..b03b8ad 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ }, "devDependencies": { "@types/bunyan": "^1.8.6", + "@types/deep-diff": "^1.0.0", "@types/js-yaml": "^3.12.1", "@types/lodash": "^4.14.134", "@types/yargs": "^13.0.0", + "deep-diff": "^1.0.2", "jsonpath-plus": "^0.20.1", "rollup": "^1.15.5", "rollup-plugin-commonjs": "^10.0.0", diff --git a/rules/kubernetes.yml b/rules/kubernetes.yml index 139bed2..94c3b19 100644 --- a/rules/kubernetes.yml +++ b/rules/kubernetes.yml @@ -22,11 +22,13 @@ rules: required: [cpu, memory] properties: cpu: &resources-cpu - oneOf: - - type: number - minimum: 1 - - type: string - pattern: "[1-9][0-9]*m" + default: 500m + type: string + # oneOf: + # - type: number + # minimum: 1 + # - type: string + # pattern: "[1-9][0-9]*m" memory: &resources-memory oneOf: - type: number diff --git a/src/index.ts b/src/index.ts index 273b4e2..57d2df0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import { createLogger } from 'bunyan'; +import { diff } from 'deep-diff'; +import { cloneDeep } from 'lodash'; import { Options, usage } from 'yargs'; import { loadConfig } from 'src/config'; @@ -11,6 +13,8 @@ import { VisitorContext } from 'src/visitor/context'; const CONFIG_ARGS_NAME = 'config-name'; const CONFIG_ARGS_PATH = 'config-path'; +const MODES = ['check', 'fix']; + const RULE_OPTION: Options = { default: [], group: 'Rules:', @@ -87,6 +91,11 @@ export async function main(argv: Array): Promise { logger.info(VERSION_INFO, 'version info'); logger.info({ args }, 'main arguments'); + // check mode + if (!MODES.includes(args.mode)) { + logger.error({ mode: args.mode }, 'unsupported mode'); + } + // const schema = new Schema(); const result = { errors: [], valid: true }; // schema.match(config); if (!result.valid) { @@ -97,9 +106,6 @@ export async function main(argv: Array): Promise { const rules = await loadRules(args.rules); const source = await loadSource(args.source); - const parser = new YamlParser(); - const data = parser.parse(source); - const activeRules = await resolveRules(rules, args as any); const ctx = new VisitorContext({ coerce: args.coerce, @@ -107,23 +113,29 @@ export async function main(argv: Array): Promise { logger, }); - switch (args.mode) { - case 'check': - case 'fix': - for (const rule of activeRules) { - if (rule.visit(ctx, data)) { - logger.info({ rule }, 'passed rule'); - } else { - logger.warn({ rule }, 'failed rule'); - } + const parser = new YamlParser(); + let data = parser.parse(source); + + for (const rule of activeRules) { + const workingCopy = cloneDeep(data); + const ruleErrors = await rule.visit(ctx, workingCopy); + + if (ruleErrors > 0) { + logger.warn({ rule }, 'rule failed'); + } else { + const ruleDiff = diff(data, workingCopy); + if (Array.isArray(ruleDiff) && ruleDiff.length > 0) { + logger.info({ diff: ruleDiff, rule }, 'rule passed with modifications'); + + data = workingCopy; + } else { + logger.info({ rule }, 'rule passed'); } - break; - default: - ctx.error({ mode: args.mode }, 'unsupported mode'); + } } if (ctx.errors.length > 0) { - logger.error({ errors: ctx.errors }, 'some rules failed'); + logger.error({ count: ctx.errors.length, errors: ctx.errors }, 'some rules failed'); if (args.count) { return Math.min(ctx.errors.length, 255); } else { diff --git a/src/rule.ts b/src/rule.ts index a2777cc..83a8dd8 100644 --- a/src/rule.ts +++ b/src/rule.ts @@ -101,7 +101,7 @@ export class Rule implements RuleData, Visitor { } } - public async visit(ctx: VisitorContext, node: any): Promise { + public async visit(ctx: VisitorContext, node: any): Promise { const check = ctx.ajv.compile(this.check); const filter = this.compileFilter(ctx); const scopes = JSONPath({ @@ -111,7 +111,7 @@ export class Rule implements RuleData, Visitor { if (isNil(scopes) || scopes.length === 0) { ctx.logger.debug('no data selected'); - return ctx; + return 0; } for (const item of scopes) { @@ -119,19 +119,22 @@ export class Rule implements RuleData, Visitor { if (filter(item)) { ctx.logger.debug({ item }, 'checking item') if (!check(item)) { + const errors = Array.from(check.errors); ctx.logger.warn({ + errors, name: this.name, item, + rule: this, }, 'rule failed on item'); - ctx.errors.push(...check.errors); - return ctx; + ctx.errors.push(...errors); + return errors.length; } } else { ctx.logger.debug({ errors: filter.errors, item }, 'skipping item'); } } - return ctx; + return 0; } protected compileFilter(ctx: VisitorContext): any { diff --git a/src/visitor/context.ts b/src/visitor/context.ts index 3a7ddcb..875e515 100644 --- a/src/visitor/context.ts +++ b/src/visitor/context.ts @@ -10,6 +10,8 @@ export interface VisitorContextOptions { export class VisitorContext { public readonly ajv: any; public readonly changes: Array; + public readonly coerce: boolean; + public readonly defaults: boolean; public readonly errors: Array; public readonly logger: Logger; @@ -19,6 +21,8 @@ export class VisitorContext { useDefaults: options.defaults, }); this.changes = []; + this.coerce = options.coerce; + this.defaults = options.defaults; this.errors = []; this.logger = options.logger; } diff --git a/src/visitor/index.ts b/src/visitor/index.ts index 0fb9343..27ca6f3 100644 --- a/src/visitor/index.ts +++ b/src/visitor/index.ts @@ -1,5 +1,5 @@ import { VisitorContext } from 'src/visitor/context'; export interface Visitor { - visit(ctx: VisitorContext, node: any): Promise; + visit(ctx: VisitorContext, node: any): Promise; } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 6d6371f..e88a0eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,6 +9,11 @@ dependencies: "@types/node" "*" +"@types/deep-diff@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/deep-diff/-/deep-diff-1.0.0.tgz#7eba3202a99b3a207f758f351f7f86387269fc40" + integrity sha512-ENsJcujGbCU/oXhDfQ12mSo/mCBWodT2tpARZKmatoSrf8+cGRCPi0KVj3I0FORhYZfLXkewXu7AoIWqiBLkNw== + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -577,6 +582,11 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +deep-diff@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26" + integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg== + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"