From 82ffcf4b8b131f2c98ba9ce9d4960349df317764 Mon Sep 17 00:00:00 2001 From: mbwhite Date: Thu, 4 Jan 2024 09:48:18 +0000 Subject: [PATCH] fix: handle missing specs Signed-off-by: mbwhite --- src/interfaces/common.ts | 17 +++++++++++++++++ src/lint.ts | 9 +++++++-- src/logger.ts | 11 ++++++++++- src/reporter.ts | 14 ++++++++------ src/rules/no-duplicate-env.ts | 2 ++ src/rules/no-invalid-name.ts | 2 ++ src/rules/no-invalid-param-type.ts | 7 ++++--- src/rules/no-latest-image.ts | 1 + src/rules/no-missing-hashbang.ts | 1 + src/rules/no-missing-workspace.ts | 4 ++-- src/rules/no-undefined-param.ts | 1 + src/rules/no-undefined-volume.ts | 1 + src/rules/no-unused-param.ts | 4 ++++ src/rules/prefer-beta.ts | 3 ++- src/runner.ts | 3 ++- 15 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/interfaces/common.ts b/src/interfaces/common.ts index 13f2127..0e48330 100644 --- a/src/interfaces/common.ts +++ b/src/interfaces/common.ts @@ -1,3 +1,5 @@ +import { Document } from 'yaml'; + interface Base { apiVersion: string; metadata: { @@ -42,4 +44,19 @@ interface Tekton { externaltasks?: ExternalResource[]; } +// ref: https://dev.to/ankittanna/how-to-create-a-type-for-complex-json-object-in-typescript-d81 +type JSONValue = string | number | boolean | { [x: string]: JSONValue } | Array; + +export interface Doc { + content: JSONValue; + doc: Document; + path: string; + raw: string; + no_report: boolean; +} + +export type RuleReportFn = (message: string, node, prop) => void; + +export type RuleFn = (docs, tekton: Tekton, report: RuleReportFn) => void; + export { Tekton, Base, Param, BaseName, ValueParam, ExternalResource }; diff --git a/src/lint.ts b/src/lint.ts index f977799..d75fecd 100644 --- a/src/lint.ts +++ b/src/lint.ts @@ -8,7 +8,7 @@ import { toolConfig } from './config.js'; import run from './runner.js'; import logProblems from './log-problems.js'; -import { logger } from './logger.js'; +import { logger, usingLogfile } from './logger.js'; const p = path.resolve(path.dirname(new url.URL(import.meta.url).pathname), '..', 'package.json'); const pkg = JSON.parse(fs.readFileSync(p, 'utf-8')); @@ -67,7 +67,12 @@ const parser = yargs(process.argv.slice(2)) process.exitCode = 0; } } catch (e) { - logger.error((e as Error).message); + if (usingLogfile()) { + logger.error(e as Error); + } else { + logger.error((e as Error).message); + } + process.exitCode = 1; } })(); diff --git a/src/logger.ts b/src/logger.ts index 3359a63..5539573 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -36,4 +36,13 @@ if (logfile) { }); } -export const logger = pino.pino(transports); +export const logger = pino.pino( + { + serializers: { + err: pino.stdSerializers.err, + }, + }, + transports, +); + +export const usingLogfile = () => logfile; diff --git a/src/reporter.ts b/src/reporter.ts index 70f1f00..5e24af4 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -1,4 +1,6 @@ -function getLine(chars, n) { +import { Doc } from './interfaces/common.js'; + +function getLine(chars: string[], n: number): number { let l = 1; for (let i = 0; i < n; i++) { if (chars[i] === '\n') l++; @@ -6,7 +8,7 @@ function getLine(chars, n) { return l; } -function getCol(chars, n) { +function getCol(chars: string[], n: number): number { let c = 1; for (let i = 0; i < n; i++) { if (chars[i] === '\n') c = 0; @@ -19,7 +21,7 @@ function getLocation(m, node, prop) { if (!m.has(node)) return {}; const k = m.get(node); - const chars = Array.from(k.doc.raw); + const chars: string[] = Array.from(k.doc.raw); let n = prop ? k.node.get(prop, true) : k.node; if (!n) n = k.node.items.find((pair) => pair.key.value === prop).key; return { @@ -51,11 +53,11 @@ function walk(node, path, visitor) { } } -function instrument(docs) { +function instrument(docs: Doc[]) { const m = new Map(); for (const doc of docs) { walk(doc.content, [], (node, path) => { - if (node != null && typeof node == 'object') { + if (node != null && typeof node == 'object') { m.set(node, { node: path.length ? doc.doc.getIn(path, true) : doc.doc, path, @@ -71,7 +73,7 @@ class Reporter { private m: any; problems: any[]; - constructor(docs = []) { + constructor(docs: Doc[] = []) { this.m = instrument(docs); this.problems = []; } diff --git a/src/rules/no-duplicate-env.ts b/src/rules/no-duplicate-env.ts index fd4f98b..20724c2 100644 --- a/src/rules/no-duplicate-env.ts +++ b/src/rules/no-duplicate-env.ts @@ -1,5 +1,7 @@ export default (docs, tekton, report) => { for (const task of Object.values(tekton.tasks)) { + if (!task.spec) continue; + if (task.spec.stepTemplate && task.spec.stepTemplate.env) { const templateEnvVars = new Set(); for (const env of task.spec.stepTemplate.env) { diff --git a/src/rules/no-invalid-name.ts b/src/rules/no-invalid-name.ts index f3d7e6c..9e9f524 100644 --- a/src/rules/no-invalid-name.ts +++ b/src/rules/no-invalid-name.ts @@ -12,6 +12,8 @@ function getTaskParams(spec) { function checkInvalidParameterName(resources, report) { for (const resource of Object.values(resources)) { + if (!resource.spec) continue; + let params; if (resource.kind === 'Task') { params = getTaskParams(resource.spec); diff --git a/src/rules/no-invalid-param-type.ts b/src/rules/no-invalid-param-type.ts index fd34f5a..ea9c2e0 100644 --- a/src/rules/no-invalid-param-type.ts +++ b/src/rules/no-invalid-param-type.ts @@ -30,23 +30,24 @@ export default (docs, tekton, report) => { } for (const task of Object.values(tekton.tasks)) { + if (!task.spec) continue; const params = getTaskParams(task.spec); if (!params) continue; checkParameterValues(task.metadata.name, task.kind, params, report); } for (const template of Object.values(tekton.triggerTemplates)) { - if (!template.spec.params) continue; + if (!template.spec || !template.spec.params) continue; checkParameterValues(template.metadata.name, template.kind, template.spec.params, report); } for (const pipeline of Object.values(tekton.pipelines)) { - if (!pipeline.spec.params) continue; + if (!pipeline.spec || !pipeline.spec.params) continue; checkParameterValues(pipeline.metadata.name, pipeline.kind, pipeline.spec.params, report); } for (const pipeline of Object.values(tekton.pipelines)) { - if (!pipeline.spec.params) continue; + if (!pipeline.spec || !pipeline.spec.params) continue; for (const task of Object.values(pipeline.spec.tasks)) { if (!task.params) continue; for (const param of Object.values(task.params)) { diff --git a/src/rules/no-latest-image.ts b/src/rules/no-latest-image.ts index b249ac0..ee884c8 100644 --- a/src/rules/no-latest-image.ts +++ b/src/rules/no-latest-image.ts @@ -9,6 +9,7 @@ export default (docs, tekton, report) => { } for (const task of Object.values(tekton.tasks)) { + if (!task.spec) continue; check(task.spec.stepTemplate); for (const step of Object.values(task.spec.steps)) { check(step); diff --git a/src/rules/no-missing-hashbang.ts b/src/rules/no-missing-hashbang.ts index b9984fe..9b6884e 100644 --- a/src/rules/no-missing-hashbang.ts +++ b/src/rules/no-missing-hashbang.ts @@ -10,6 +10,7 @@ const checkSteps = (steps, report) => { export default (docs, tekton, report) => { for (const task of Object.values(tekton.tasks)) { + if (!task.spec) continue; checkSteps(task.spec.steps, report); } diff --git a/src/rules/no-missing-workspace.ts b/src/rules/no-missing-workspace.ts index 0e407c8..bbd71d1 100644 --- a/src/rules/no-missing-workspace.ts +++ b/src/rules/no-missing-workspace.ts @@ -1,6 +1,6 @@ export default (docs, tekton, report) => { for (const task of Object.values(tekton.tasks)) { - if (!task.spec.workspaces) continue; + if (!task.spec || !task.spec.workspaces) continue; const taskName = task.metadata.name; const requiredWorkspaces = task.spec.workspaces.filter((ws) => !ws.optional).map((ws) => ws.name); @@ -42,7 +42,7 @@ export default (docs, tekton, report) => { } for (const pipeline of Object.values(tekton.pipelines)) { - if (!pipeline.spec.workspaces) continue; + if (!pipeline.spec || !pipeline.spec.workspaces) continue; const required = pipeline.spec.workspaces.map((ws) => ws.name); for (const template of Object.values(tekton.triggerTemplates)) { diff --git a/src/rules/no-undefined-param.ts b/src/rules/no-undefined-param.ts index bec2966..f2fd700 100644 --- a/src/rules/no-undefined-param.ts +++ b/src/rules/no-undefined-param.ts @@ -39,6 +39,7 @@ export default (docs, tekton, report) => { } for (const task of Object.values(tekton.tasks)) { + if (!task.spec) continue; const params = getTaskParams(task); for (const prefix of ['inputs.params', 'params']) { for (const prop of ['steps', 'volumes', 'stepTemplate', 'sidecars']) { diff --git a/src/rules/no-undefined-volume.ts b/src/rules/no-undefined-volume.ts index 4d0f572..9e5eba5 100644 --- a/src/rules/no-undefined-volume.ts +++ b/src/rules/no-undefined-volume.ts @@ -1,5 +1,6 @@ export default (docs, tekton, report) => { for (const task of Object.values(tekton.tasks)) { + if (!task.spec) continue; const volumes = (task.spec.volumes || []).map((volume) => volume.name); for (const step of task.spec.steps) { diff --git a/src/rules/no-unused-param.ts b/src/rules/no-unused-param.ts index 4f2e5cc..26fd547 100644 --- a/src/rules/no-unused-param.ts +++ b/src/rules/no-unused-param.ts @@ -24,6 +24,7 @@ function getParams(kind, spec) { export default (docs, tekton, report) => { for (const task of Object.values(tekton.tasks)) { + if (!task.spec) continue; const params = getParams(task.kind, task.spec); const occurences = Object.fromEntries(params.map((param) => [param.name, 0])); for (const prefix of ['inputs.params', 'params']) { @@ -41,6 +42,7 @@ export default (docs, tekton, report) => { } for (const condition of Object.values(tekton.conditions)) { + if (!condition.spec) continue; const params = getParams(condition.kind, condition.spec); const occurences = Object.fromEntries(params.map((param) => [param.name, 0])); walk(condition.spec.check, ['spec', 'check'], unused(occurences, 'params')); @@ -54,6 +56,7 @@ export default (docs, tekton, report) => { } for (const template of Object.values(tekton.triggerTemplates)) { + if (!template.spec) continue; const params = getParams(template.kind, template.spec); const occurences = Object.fromEntries(params.map((param) => [param.name, 0])); walk(template.spec, ['spec'], unused(occurences, 'params')); @@ -67,6 +70,7 @@ export default (docs, tekton, report) => { } for (const pipeline of Object.values(tekton.pipelines)) { + if (!pipeline.spec) continue; const params = getParams(pipeline.kind, pipeline.spec); const occurences = Object.fromEntries(params.map((param) => [param.name, 0])); walk(pipeline.spec.tasks, ['spec', 'tasks'], unused(occurences, 'params')); diff --git a/src/rules/prefer-beta.ts b/src/rules/prefer-beta.ts index 32ced1a..76c6bed 100644 --- a/src/rules/prefer-beta.ts +++ b/src/rules/prefer-beta.ts @@ -1,7 +1,7 @@ export default (docs, tekton, report) => { for (const pipeline of Object.values(tekton.pipelines)) { + if (!pipeline.spec) continue; for (const task of pipeline.spec.tasks) { - if (!task.taskSpec) continue; switch (pipeline.apiVersion) { case 'tekton.dev/v1alpha1': if (task.taskSpec.params) @@ -22,6 +22,7 @@ export default (docs, tekton, report) => { } for (const task of Object.values(tekton.tasks)) { + if (!task.spec) continue; switch (task.apiVersion) { case 'tekton.dev/v1alpha1': if (task.spec.params) diff --git a/src/runner.ts b/src/runner.ts index 1ee56be..84240b8 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -7,10 +7,11 @@ import yaml from 'yaml'; import fs from 'node:fs'; import { collectAllExternal } from './external.js'; import { logger } from './logger.js'; +import { Doc } from './interfaces/common.js'; /* Collect paths based on the glob pattern passed in */ const collector = async (paths: string[], cfg: ToolConfig) => { - const docs: any = []; + const docs: Doc[] = []; const files = await glob(paths); logger.info('Found these files %j', files); for (const file of files) {