2019-06-15 20:20:04 +00:00
|
|
|
import { createLogger } from 'bunyan';
|
2019-06-22 16:48:41 +00:00
|
|
|
import { diff } from 'deep-diff';
|
|
|
|
import { cloneDeep } from 'lodash';
|
2019-06-17 13:04:58 +00:00
|
|
|
import { Options, usage } from 'yargs';
|
2019-06-15 20:20:04 +00:00
|
|
|
|
2019-06-16 21:35:18 +00:00
|
|
|
import { loadConfig } from 'src/config';
|
2019-06-16 21:15:01 +00:00
|
|
|
import { YamlParser } from 'src/parser/YamlParser';
|
2019-06-16 18:30:04 +00:00
|
|
|
import { loadRules, resolveRules } from 'src/rule';
|
2019-06-16 01:21:11 +00:00
|
|
|
import { loadSource, writeSource } from 'src/source';
|
2019-06-15 20:20:04 +00:00
|
|
|
import { VERSION_INFO } from 'src/version';
|
2019-06-16 18:30:04 +00:00
|
|
|
import { VisitorContext } from 'src/visitor/context';
|
2019-06-15 20:20:04 +00:00
|
|
|
|
|
|
|
const CONFIG_ARGS_NAME = 'config-name';
|
|
|
|
const CONFIG_ARGS_PATH = 'config-path';
|
|
|
|
|
2019-06-22 16:48:41 +00:00
|
|
|
const MODES = ['check', 'fix'];
|
|
|
|
|
2019-06-17 13:04:58 +00:00
|
|
|
const RULE_OPTION: Options = {
|
|
|
|
default: [],
|
2019-06-17 13:19:35 +00:00
|
|
|
group: 'Rules:',
|
2019-06-17 13:04:58 +00:00
|
|
|
type: 'array',
|
2019-06-15 20:20:04 +00:00
|
|
|
};
|
|
|
|
|
2019-06-17 14:11:26 +00:00
|
|
|
const MAIN_ARGS = usage(`Usage: salty-dog <mode> [options]`)
|
2019-06-17 13:04:58 +00:00
|
|
|
.option(CONFIG_ARGS_NAME, {
|
|
|
|
default: `.${VERSION_INFO.app.name}.yml`,
|
2019-06-17 13:19:35 +00:00
|
|
|
group: 'Config:',
|
2019-06-17 13:04:58 +00:00
|
|
|
type: 'string',
|
|
|
|
})
|
|
|
|
.option(CONFIG_ARGS_PATH, {
|
|
|
|
default: [],
|
2019-06-17 13:19:35 +00:00
|
|
|
group: 'Config:',
|
2019-06-17 13:04:58 +00:00
|
|
|
type: 'array',
|
|
|
|
})
|
|
|
|
.option('coerce', {
|
|
|
|
default: false,
|
|
|
|
type: 'boolean',
|
|
|
|
})
|
|
|
|
.option('count', {
|
|
|
|
alias: ['c'],
|
|
|
|
default: false,
|
2019-06-17 14:11:26 +00:00
|
|
|
desc: 'Exit with error count',
|
2019-06-17 13:04:58 +00:00
|
|
|
type: 'boolean',
|
|
|
|
})
|
|
|
|
.option('dest', {
|
|
|
|
alias: ['d'],
|
|
|
|
default: '-',
|
|
|
|
type: 'string',
|
|
|
|
})
|
|
|
|
.option('format', {
|
|
|
|
alias: ['f'],
|
|
|
|
default: 'yaml',
|
|
|
|
type: 'string',
|
|
|
|
})
|
|
|
|
.option('mode', {
|
|
|
|
alias: ['m'],
|
2019-06-17 13:13:39 +00:00
|
|
|
choices: ['check', 'fix'],
|
2019-06-17 13:04:58 +00:00
|
|
|
default: 'check',
|
|
|
|
type: 'string',
|
|
|
|
})
|
|
|
|
.option('rules', {
|
|
|
|
alias: ['r'],
|
|
|
|
default: [],
|
2019-06-17 14:11:26 +00:00
|
|
|
desc: 'Rules file',
|
2019-06-17 13:04:58 +00:00
|
|
|
type: 'array',
|
|
|
|
})
|
|
|
|
.option('source', {
|
|
|
|
alias: ['s'],
|
|
|
|
default: '-',
|
|
|
|
type: 'string',
|
|
|
|
})
|
2019-06-17 13:16:13 +00:00
|
|
|
.option('exclude-level', RULE_OPTION)
|
|
|
|
.option('exclude-name', RULE_OPTION)
|
|
|
|
.option('exclude-tag', RULE_OPTION)
|
|
|
|
.option('include-level', RULE_OPTION)
|
|
|
|
.option('include-name', RULE_OPTION)
|
|
|
|
.option('include-tag', {
|
2019-06-17 13:04:58 +00:00
|
|
|
...RULE_OPTION,
|
|
|
|
alias: ['t', 'tag'],
|
|
|
|
})
|
|
|
|
.help();
|
|
|
|
|
2019-06-15 20:20:04 +00:00
|
|
|
const STATUS_SUCCESS = 0;
|
|
|
|
const STATUS_ERROR = 1;
|
|
|
|
|
|
|
|
export async function main(argv: Array<string>): Promise<number> {
|
2019-06-17 13:04:58 +00:00
|
|
|
const args = MAIN_ARGS.argv;
|
|
|
|
const config = await loadConfig(args[CONFIG_ARGS_NAME], ...args[CONFIG_ARGS_PATH]);
|
2019-06-15 20:20:04 +00:00
|
|
|
|
|
|
|
const logger = createLogger(config.data.logger);
|
|
|
|
logger.info(VERSION_INFO, 'version info');
|
2019-06-17 13:04:58 +00:00
|
|
|
logger.info({ args }, 'main arguments');
|
2019-06-15 20:20:04 +00:00
|
|
|
|
2019-06-22 16:48:41 +00:00
|
|
|
// check mode
|
|
|
|
if (!MODES.includes(args.mode)) {
|
|
|
|
logger.error({ mode: args.mode }, 'unsupported mode');
|
|
|
|
}
|
|
|
|
|
2019-06-15 20:20:04 +00:00
|
|
|
// const schema = new Schema();
|
|
|
|
const result = { errors: [], valid: true }; // schema.match(config);
|
|
|
|
if (!result.valid) {
|
|
|
|
logger.error({ errors: result.errors }, 'config failed to validate');
|
|
|
|
return STATUS_ERROR;
|
|
|
|
}
|
|
|
|
|
2019-06-17 12:11:36 +00:00
|
|
|
const ctx = new VisitorContext({
|
2019-06-17 13:04:58 +00:00
|
|
|
coerce: args.coerce,
|
|
|
|
defaults: args.mode === 'fix',
|
2019-06-17 12:11:36 +00:00
|
|
|
logger,
|
|
|
|
});
|
2019-06-16 21:15:01 +00:00
|
|
|
|
2019-06-22 16:48:41 +00:00
|
|
|
const parser = new YamlParser();
|
2019-06-22 17:26:11 +00:00
|
|
|
const source = await loadSource(args.source);
|
2019-06-22 16:48:41 +00:00
|
|
|
let data = parser.parse(source);
|
|
|
|
|
2019-06-22 17:26:11 +00:00
|
|
|
const rules = await loadRules(args.rules, ctx.ajv);
|
|
|
|
const activeRules = await resolveRules(rules, args as any);
|
|
|
|
|
2019-06-22 16:48:41 +00:00
|
|
|
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');
|
2019-06-15 23:07:46 +00:00
|
|
|
}
|
2019-06-22 16:48:41 +00:00
|
|
|
}
|
2019-06-15 20:20:04 +00:00
|
|
|
}
|
|
|
|
|
2019-06-16 18:30:04 +00:00
|
|
|
if (ctx.errors.length > 0) {
|
2019-06-22 16:48:41 +00:00
|
|
|
logger.error({ count: ctx.errors.length, errors: ctx.errors }, 'some rules failed');
|
2019-06-17 13:04:58 +00:00
|
|
|
if (args.count) {
|
2019-06-16 18:30:04 +00:00
|
|
|
return Math.min(ctx.errors.length, 255);
|
2019-06-15 23:27:36 +00:00
|
|
|
} else {
|
|
|
|
return STATUS_ERROR;
|
|
|
|
}
|
|
|
|
} else {
|
2019-06-15 23:56:42 +00:00
|
|
|
logger.info('all rules passed');
|
2019-06-16 21:35:18 +00:00
|
|
|
const output = parser.dump(data);
|
2019-06-17 13:04:58 +00:00
|
|
|
await writeSource(args.dest, output);
|
2019-06-15 23:27:36 +00:00
|
|
|
return STATUS_SUCCESS;
|
|
|
|
}
|
2019-06-15 20:20:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
main(process.argv).then((status) => process.exit(status)).catch((err) => {
|
|
|
|
/* tslint:disable-next-line:no-console */
|
|
|
|
console.error('uncaught error during main:', err);
|
|
|
|
process.exit(STATUS_ERROR);
|
|
|
|
});
|