From 8f1e62eb8415eaf99917c22980f0d2536c840e3c Mon Sep 17 00:00:00 2001 From: MBWhite Date: Mon, 15 Apr 2024 11:43:13 +0100 Subject: [PATCH] feat: add support for camelCase Signed-off-by: MBWhite --- .tektonlintrc.yaml | 1 + README.md | 13 +++- .../customconfig/.tektonlintrc.yaml | 8 +++ .../customconfig/ace-pipeline.yaml | 72 +++++++++++++++++++ .../ace-pipeline.yaml.expect.json | 53 ++++++++++++++ regression-tests/regression.test.ts | 26 ++++++- src/config.ts | 9 ++- src/default-rule-config.ts | 1 + src/rule-loader.ts | 3 + src/rules/prefer-camel-kebab-case.ts | 65 +++++++++++++++++ 10 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 regression-tests/customconfig/.tektonlintrc.yaml create mode 100644 regression-tests/customconfig/ace-pipeline.yaml create mode 100644 regression-tests/customconfig/ace-pipeline.yaml.expect.json create mode 100644 src/rules/prefer-camel-kebab-case.ts diff --git a/.tektonlintrc.yaml b/.tektonlintrc.yaml index 087a332..d2c67f3 100644 --- a/.tektonlintrc.yaml +++ b/.tektonlintrc.yaml @@ -17,6 +17,7 @@ rules: # error | warning | off no-latest-image: warning prefer-beta: warning prefer-kebab-case: warning + prefer-camel-kebab-case: off no-unused-param: warning no-missing-resource: error no-undefined-param: error diff --git a/README.md b/README.md index 2167cff..8e5b1d0 100644 --- a/README.md +++ b/README.md @@ -188,13 +188,24 @@ If you see an error like ` Pipeline 'pipeline-test-perf-tag' references task 'un - Unused `Pipeline` parameters - Unused `TriggerTemplate` parameters - Unpinned images in `Task` steps -- _kebab-case_ naming violations +- _kebab-case_ and OR _camelCase_ naming violations - `Task` & `Pipeline` definitions with `tekton.dev/v1alpha1` `apiVersion` - Missing `TriggerBinding` parameter values - Usage of deprecated `Condition` instead of `WhenExpression` - Usage of deprecated resources (resources marked with `tekton.dev/deprecated` label) - Missing `hashbang` line from a `Step`s `script` +The default rule is for preferring _kebab-case_; _camelCase_ is equally popular and many of the official examples on the Tekton website are in camel case. To use the rule that accepts camel case as well swap the rules in the `.tektonrc.yaml` file + +```yaml +--- +rules: # error | warning | off + prefer-kebab-case: off + prefer-camel-kebab-case: warning +``` + + + ## Configuring `tekton-lint` You can configure `tekton-lint` with a configuration file ([`.tektonlintrc.yaml`](./.tektonlintrc.yaml)). You can decide which rules are enabled and at what error level. In addition you can specify external tekton tasks defined in a git repository; for example [OpenToolChain](https://github.com/open-toolchain/tekton-catalog) provides a set of tasks that are helpful. But if you lint just your own tekton files there will be errors about not being able to find `git-clone-repo` for example. Not will any checks be made to see if your usage is correct. diff --git a/regression-tests/customconfig/.tektonlintrc.yaml b/regression-tests/customconfig/.tektonlintrc.yaml new file mode 100644 index 0000000..ce729da --- /dev/null +++ b/regression-tests/customconfig/.tektonlintrc.yaml @@ -0,0 +1,8 @@ +--- +rules: # error | warning | off + prefer-kebab-case: off + prefer-camel-kebab-case: warning + +# custom: +# my_rules: custom_rules + diff --git a/regression-tests/customconfig/ace-pipeline.yaml b/regression-tests/customconfig/ace-pipeline.yaml new file mode 100644 index 0000000..876b607 --- /dev/null +++ b/regression-tests/customconfig/ace-pipeline.yaml @@ -0,0 +1,72 @@ +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: ace-pipeline +spec: + params: + - name: outputRegistry + type: string + - name: url + type: string + default: "https://github.com/ot4i/ace-demo-pipeline" + - name: revision + type: string + default: "main" + - name: buildImage + type: string + - name: runtimeBaseImage + type: string + - name: knativeDeploy + type: string + default: "false" + tasks: + - name: build-from-source + taskRef: + name: aceBuild + params: + - name: outputRegistry + value: $(params.outputRegistry) + - name: url + value: $(params.url) + - name: revision + value: $(params.revision) + - name: buildImage + value: $(params.buildImage) + - name: runtimeBaseImage + value: $(params.runtimeBaseImage) + - name: deploy-to-cluster + taskRef: + name: deploy-to-cluster + params: + - name: dockerRegistry + value: $(params.outputRegistry) + - name: url + value: $(params.url) + - name: revision + value: $(params.revision) + - name: tag + value: "$(tasks.build-from-source.results.tag)" + runAfter: + - build-from-source + when: + - input: "$(params.knativeDeploy)" + operator: in + values: ["false"] + - name: deploy-knative-to-cluster + taskRef: + name: knative-deploy + params: + - name: dockerRegistry + value: $(params.outputRegistry) + - name: url + value: $(params.url) + - name: revision + value: $(params.revision) + - name: tag + value: "$(tasks.build-from-source.results.tag)" + runAfter: + - build-from-source + when: + - input: "$(params.knativeDeploy)" + operator: in + values: ["true"] \ No newline at end of file diff --git a/regression-tests/customconfig/ace-pipeline.yaml.expect.json b/regression-tests/customconfig/ace-pipeline.yaml.expect.json new file mode 100644 index 0000000..e69160d --- /dev/null +++ b/regression-tests/customconfig/ace-pipeline.yaml.expect.json @@ -0,0 +1,53 @@ +[ + { + "message": "Pipeline 'ace-pipeline' references task 'aceBuild' but the referenced task cannot be found. To fix this, include all the task definitions to the lint task for this pipeline.", + "rule": "no-missing-resource", + "level": "error", + "path": "./regression-tests/customconfig/ace-pipeline.yaml", + "loc": { + "range": [ + 521, + 529, + 530 + ], + "startLine": 25, + "startColumn": 15, + "endLine": 25, + "endColumn": 23 + } + }, + { + "message": "Pipeline 'ace-pipeline' references task 'deploy-to-cluster' but the referenced task cannot be found. To fix this, include all the task definitions to the lint task for this pipeline.", + "rule": "no-missing-resource", + "level": "error", + "path": "./regression-tests/customconfig/ace-pipeline.yaml", + "loc": { + "range": [ + 930, + 947, + 948 + ], + "startLine": 39, + "startColumn": 15, + "endLine": 39, + "endColumn": 32 + } + }, + { + "message": "Pipeline 'ace-pipeline' references task 'knative-deploy' but the referenced task cannot be found. To fix this, include all the task definitions to the lint task for this pipeline.", + "rule": "no-missing-resource", + "level": "error", + "path": "./regression-tests/customconfig/ace-pipeline.yaml", + "loc": { + "range": [ + 1442, + 1456, + 1457 + ], + "startLine": 57, + "startColumn": 15, + "endLine": 57, + "endColumn": 29 + } + } +] \ No newline at end of file diff --git a/regression-tests/regression.test.ts b/regression-tests/regression.test.ts index 925f828..31be7fe 100644 --- a/regression-tests/regression.test.ts +++ b/regression-tests/regression.test.ts @@ -1,12 +1,13 @@ import fs from 'node:fs' import fg from 'fast-glob'; +import path from 'node:path'; import {Problem, Config, Linter} from '../src/index' const pattern = "./regression-tests/general/*.yaml" const yamlfiles = fg.globSync(pattern) +const customconfig = "./regression-tests/customconfig/*.yaml" - -describe("Regression Tests",()=>{ +describe("Default Config Regression Tests",()=>{ test.each(yamlfiles)("%s",async (yamlSrcPath)=>{ const cfg: Config = Config.getDefaultConfig() @@ -23,4 +24,25 @@ describe("Regression Tests",()=>{ expect(problems).toMatchObject(expected) }) +}) + +describe("Custom Config Regression Tests",()=>{ + + test.each(fg.globSync(customconfig))("%s", async (yamlSrcPath)=>{ + const cfgPath = path.resolve(path.dirname(yamlSrcPath)) + const cfg: Config = Config.getConfig(cfgPath); + cfg.globs=[yamlSrcPath] + const problems: Problem[] = await Linter.run(cfg) + + const expectedPath =`${yamlSrcPath}.expect.json` + + if (!fs.existsSync(expectedPath)){ + fs.writeFileSync(expectedPath, JSON.stringify(problems)) + } + + const expected = JSON.parse(fs.readFileSync(expectedPath,'utf-8')) + expect(problems).toMatchObject(expected) + + }) + }) \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index c2d9bdf..013dec5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -103,18 +103,25 @@ export class Config { public static getDefaultConfig(): Config { // create default cli-proxy + return Config.getConfig(process.cwd()); + } + + public static getConfig(dir: string): Config { + // create default cli-proxy const argv = { watch: false, color: true, format: json, quite: false, - config: process.cwd(), + config: dir, 'refresh-cache': false, + _: [], }; const config = new Config(argv); return config; } + } diff --git a/src/default-rule-config.ts b/src/default-rule-config.ts index d51ec5c..9186f8e 100644 --- a/src/default-rule-config.ts +++ b/src/default-rule-config.ts @@ -19,6 +19,7 @@ const defaultRules: RulesConfig = { 'no-latest-image': 'warning', 'prefer-beta': 'warning', 'prefer-kebab-case': 'warning', + 'prefer-camel-kebab-case': 'off', 'no-unused-param': 'warning', 'no-missing-resource': 'error', 'no-undefined-param': 'error', diff --git a/src/rule-loader.ts b/src/rule-loader.ts index add736c..4688e32 100644 --- a/src/rule-loader.ts +++ b/src/rule-loader.ts @@ -55,6 +55,9 @@ const defaultRules = { // prefer-kebab-case 'prefer-kebab-case': (await import('./rules/prefer-kebab-case.js')).default, + // prefer-camel-kebab-case + 'prefer-camel-kebab-case': (await import('./rules/prefer-camel-kebab-case.js')).default, + // prefer-when-expression 'prefer-when-expression': (await import('./rules/prefer-when-expression.js')).default, diff --git a/src/rules/prefer-camel-kebab-case.ts b/src/rules/prefer-camel-kebab-case.ts new file mode 100644 index 0000000..474748e --- /dev/null +++ b/src/rules/prefer-camel-kebab-case.ts @@ -0,0 +1,65 @@ +import { walk, pathToString } from '../walk.js'; + +const isValidKebabName = (name) => { + const valid = new RegExp('^[a-z0-9-()$.]*$'); + return valid.test(name); +}; + +const isValidCamelName = (name) => { + const valid = new RegExp('^[a-z_][a-z0-9A-Z()$.]*$'); + return valid.test(name); +}; + +const isValidName = (name) => { + return isValidKebabName(name) || isValidCamelName(name); +}; + +const naming = (resource, prefix, report) => (node, path, parent) => { + let name = node; + const isNameDefinition = /.name$/.test(path); + + if (path.includes('env') && path.includes('name')) return; + + if (isNameDefinition && !isValidName(name)) { + report( + `Invalid name for '${name}' at ${pathToString( + path, + )} in '${resource}'. Names should be in lowercase, alphanumeric, kebab-case or camelCase format.`, + parent, + 'name', + ); + return; + } + + const parameterPlacementRx = new RegExp(`\\$\\(${prefix}.(.*?)\\)`); + const m = node && node.toString().match(parameterPlacementRx); + + if (m) { + name = m[1]; + if (!isValidName(name)) { + report( + `Invalid name for '${name}' at ${pathToString( + path, + )} in '${resource}'. Names should be in lowercase, alphanumeric, kebab-case or camelCase format.`, + parent, + path[path.length - 1], + ); + } + } +}; + +export default (docs, tekton, report) => { + for (const pipeline of Object.values(tekton.pipelines)) { + walk(pipeline.spec.tasks, ['spec', 'tasks'], naming(pipeline.metadata.name, 'params', report)); + walk(pipeline.spec.finally, ['spec', 'finally'], naming(pipeline.metadata.name, 'params', report)); + } + + for (const pipeline of Object.values(tekton.pipelineRuns)) { + walk(pipeline.spec.tasks, ['spec', 'pipelineSpec', 'tasks'], naming(pipeline.metadata.name, 'params', report)); + walk( + pipeline.spec.finally, + ['spec', 'pipelineSpec', 'finally'], + naming(pipeline.metadata.name, 'params', report), + ); + } +};