Skip to content

Commit

Permalink
feat: pipelineRun support (partial)
Browse files Browse the repository at this point in the history
Signed-off-by: mbwhite <whitemat@uk.ibm.com>
  • Loading branch information
mbwhite committed Jan 18, 2024
1 parent 15af2d5 commit 641813a
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 28 deletions.
4 changes: 4 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- TaskSpecs
- namespaces
- PipelineRun
- Reference pipelineref
20 changes: 20 additions & 0 deletions src/errorbase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type ErrorNames = 'RULE_PARSE_ERROR';

export class ErrorBase extends Error {
name: string;
message: string;
cause: any;

constructor(name: string, msg: string, cause?: any) {
super();
this.name = name;
this.message = msg;
this.cause = cause;
}
}

export class RuleError extends ErrorBase {
constructor(msg: string, rulename: string, data: string, cause?: any) {
super('RuleError', `${rulename} - ${msg} ${data}`, cause);
}
}
3 changes: 2 additions & 1 deletion src/interfaces/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface Tekton {
conditions?: ListResources;
externaltasks?: ExternalResource[];
finally?: ListResources;
pipelineRuns?: ListResources;
}

// ref: https://dev.to/ankittanna/how-to-create-a-type-for-complex-json-object-in-typescript-d81
Expand All @@ -63,7 +64,7 @@ export interface RulesConfig {
'external-tasks': ExternalResource[];
}

export type RuleReportFn = (message: string, node, prop) => void;
export type RuleReportFn = (message: string, node, prop?) => void;
export type RuleFn = (docs, tekton: Tekton, report: RuleReportFn) => void;

// problem report definitions
Expand Down
6 changes: 6 additions & 0 deletions src/lint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#! /usr/bin/env node

/*
* SPDX-License-Identifier: Apache-2.0
*/
import sourceMapSupport from 'source-map-support';
sourceMapSupport.install();

import yargs from 'yargs/yargs';
import * as fs from 'node:fs';
import * as path from 'node:path';
Expand Down
5 changes: 5 additions & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import rules from './rule-loader.js';
import { logger } from './logger.js';
import { Tekton } from './interfaces/common.js';


const createReporter = (rule, config, reporter) => {
const isError = config.rules[rule] && config.rules[rule] === 'error';

Expand Down Expand Up @@ -31,9 +32,13 @@ const parse = (docs): Tekton => {
conditions: Object.fromEntries(
docs.filter((item) => item.kind === 'Condition').map((item) => [item.metadata.name, item]),
),
pipelineRuns: Object.fromEntries(
docs.filter((item) => item.kind === 'PipelineRun').map((item) => [item.metadata.name, item]),
),
};

logger.info('Tekton: %o', tkn);
//fs.writeFileSync('tekton.json', JSON.stringify(tkn), 'utf-8');
return tkn;
};

Expand Down
22 changes: 21 additions & 1 deletion src/rules/no-duplicate-param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function checkParams(params, report) {
}

export default (docs, tekton, report) => {
for (const t of ['triggerBindings', 'pipelines', 'tasks', 'triggerTemplates']) {
for (const t of ['triggerBindings', 'pipelines', 'tasks', 'triggerTemplates', 'pipelineRuns']) {
for (const crd of Object.values<any>(tekton[t])) {
checkParams(getParams(crd.kind, crd.spec), report);
}
Expand Down Expand Up @@ -54,4 +54,24 @@ export default (docs, tekton, report) => {
}
}
}

for (const pipelineRun of Object.values<any>(tekton.pipelineRuns)) {
try {
if (!pipelineRun.spec.pipelineSpec) continue;

const tasks = [
...(pipelineRun.spec.pipelineSpec.tasks ? pipelineRun.spec.pipelineSpec.tasks : []),
...(pipelineRun.spec.pipelineSpec.finally ? pipelineRun.spec.pipelineSpec.finally : []),
];
for (const task of tasks) {
checkParams(getParams('Task', task), report);
if (task.taskSpec) {
checkParams(getParams('Task', task.taskSpec), report);
}
}
} catch (e) {
console.log(pipelineRun.metadata.name);
throw e;
}
}
};
6 changes: 6 additions & 0 deletions src/rules/no-extra-param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,10 @@ export default (docs, tekton, report) => {
}
}
}

// for (const pr of Object.values<any>(tekton.pipelineRuns)) {
// for (const task in pr.spec.pipelineSpec.tasks) {
// console.log(task);
// }
// }
};
25 changes: 25 additions & 0 deletions src/rules/no-missing-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export default (docs, tekton, report) => {
}

for (const pipeline of Object.values<any>(tekton.pipelines)) {
if (!pipeline.spec) continue;

// include any finally tasks if they are present
const tasks = [...pipeline.spec.tasks, ...(pipeline.spec.finally ? pipeline.spec.finally : [])];
for (const task of tasks) {
Expand All @@ -78,4 +80,27 @@ export default (docs, tekton, report) => {
}
}
}

// ---

for (const pipeline of Object.values<any>(tekton.pipelineRuns)) {
if (!pipeline.spec || !pipeline.spec.pipelineSpec) continue;
const maintasks = pipeline.spec.pipelineSpec.tasks ? pipeline.spec.pipelineSpec.tasks : [];
const finallytasks = pipeline.spec.pipelineSpec.finally ? pipeline.spec.pipelineSpec.finally : [];

const tasks = [...maintasks, ...finallytasks];
for (const task of tasks) {
if (!task.taskRef) continue;
const name = task.taskRef.name;

if (!tekton.tasks[name]) {
report(
`Pipeline '${pipeline.metadata.name}' references task '${name}' but the referenced task cannot be found. To fix this, include all the task definitions to the lint task for this pipeline.`,
task.taskRef,
'name',
);
continue;
}
}
}
};
68 changes: 51 additions & 17 deletions src/rules/no-missing-workspace.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
export default (docs, tekton, report) => {
import { RuleReportFn } from 'src/interfaces/common.js';

function checkTasks(tasks, parentWorkspaces, parentName, report) {
for (const task of tasks) {
if (!task.workspaces) continue;
for (const workspace of task.workspaces) {
let matchingWorkspace = false;
if (workspace.workspace) {
matchingWorkspace = parentWorkspaces.find(({ name }) => name === workspace.workspace);
} else {
// no workspace defined - which is strictly optional, so check the name field instead
matchingWorkspace = parentWorkspaces.find(({ name }) => name === workspace.name);
}
if (!matchingWorkspace) {
report(
`Pipeline '${parentName}' provides workspace '${workspace.workspace}' for '${workspace.name}' for Task '${task.name}', but '${workspace.workspace}' doesn't exists in '${parentName}'`,
workspace,
'workspace',
);
}
}
}
}

export default (docs, tekton, report: RuleReportFn) => {
for (const task of Object.values<any>(tekton.tasks)) {
if (!task.spec || !task.spec.workspaces) continue;
const taskName = task.metadata.name;
Expand Down Expand Up @@ -28,25 +52,35 @@ export default (docs, tekton, report) => {
const pipelineWorkspaces = pipeline.spec.workspaces || [];
// include any finally tasks if they are present
const tasks = [...pipeline.spec.tasks, ...(pipeline.spec.finally ? pipeline.spec.finally : [])];
for (const task of tasks) {
if (!task.workspaces) continue;
for (const workspace of task.workspaces) {
let matchingWorkspace = false;
if (workspace.workspace) {
matchingWorkspace = pipelineWorkspaces.find(({ name }) => name === workspace.workspace);
} else {
// no workspace defined - which is strictly optional, so check the name field instead
matchingWorkspace = pipelineWorkspaces.find(({ name }) => name === workspace.name);
}
if (!matchingWorkspace) {
checkTasks(tasks, pipelineWorkspaces, pipeline.metadata.name, report);
}

for (const pipeline of Object.values<any>(tekton.pipelineRuns)) {
if (!pipeline || !pipeline.spec || !pipeline.spec.pipelineSpec) continue;
const pipelineWorkspaces = pipeline.spec.pipelineSpec.workspaces || [];
// include any finally tasks if they are present
const tasks = [
...(pipeline.spec.pipelineSpec.tasks || []),
...(pipeline.spec.pipelineSpec.finally ? pipeline.spec.pipelineSpec.finally : []),
];
checkTasks(tasks, pipelineWorkspaces, pipeline.metadata.name, report);

// check the spec workspaces and the pieline spec
const runWorkspaces = pipeline.spec.workspaces;
const specWorkspaces = pipeline.spec.pipelineSpec.workspaces;
if (!specWorkspaces) continue;

specWorkspaces
.filter((s) => !s)
.forEach((sws) => {
if (!runWorkspaces.find((ws) => (sws as any).name == ws.name)) {
console.log(`${sws.name} not found`);
report(
`Pipeline '${pipeline.metadata.name}' provides workspace '${workspace.workspace}' for '${workspace.name}' for Task '${task.name}', but '${workspace.workspace}' doesn't exists in '${pipeline.metadata.name}'`,
workspace,
'workspace',
`Pipeline run '${pipeline.metadata.name}' has a pipelineSpec with workspace '${sws.name}' that is not defined in the pipeline`,
sws,
);
}
}
}
});
}

for (const pipeline of Object.values<any>(tekton.pipelines)) {
Expand Down
24 changes: 24 additions & 0 deletions src/rules/no-pipeline-missing-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,28 @@ export default (docs, tekton, report) => {
}
}
}

// Run on the PipelineSpecs as well
for (const pipelineSpec of Object.values<any>(tekton.pipelineRuns)) {
if (!pipelineSpec.spec || !pipelineSpec.spec.pipelineSpec) continue;
const taskRoot = pipelineSpec.spec.pipelineSpec.tasks;
if (taskRoot) {
for (const task of taskRoot) {
if (!task.runAfter) continue;

for (const dependency of task.runAfter) {
const exists = taskRoot.some((task) => task.name === dependency);
const details = task.taskSpec ? 'defined in-line' : `referenced as '${task.taskRef.name}'`;

if (!exists) {
report(
`Pipeline '${pipelineSpec.metadata.name}' uses task '${task.name}' (${details}), and it depends on '${dependency}', which doesn't exists (declared in runAfter)`,
task.runAfter,
task.runAfter.indexOf(dependency),
);
}
}
}
}
}
};
39 changes: 30 additions & 9 deletions src/rules/no-pipeline-task-cycle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Supports Pipeline and PipelineRun

import pkg from 'graphlib';
const { alg, Graph } = pkg;

import { RuleError } from '../errorbase.js';

const RESULT_PATTERN = '\\$\\(tasks\\.([^.]+)\\.results\\.[^.]*\\)';
const RESULT_REGEX_G = new RegExp(RESULT_PATTERN, 'g');
const RESULT_REGEX = new RegExp(RESULT_PATTERN);
Expand Down Expand Up @@ -52,13 +56,13 @@ function resourceInputReferences(task) {
}));
}

function buildTaskGraph(pipeline, referenceCreators) {
function buildTaskGraph(name, tasks, referenceCreators) {
const pipelineGraph = new Graph({ directed: true, multigraph: true });
pipelineGraph.setGraph(pipeline.metadata.name);
if (!pipeline.spec.tasks || pipeline.spec.tasks.length === 0) {
pipelineGraph.setGraph(name);
if (!tasks || tasks.length === 0) {
return pipelineGraph;
}
for (const task of pipeline.spec.tasks) {
for (const task of tasks) {
pipelineGraph.setNode(task.name, task);
for (const referenceCreator of referenceCreators) {
for (const { ref, type } of referenceCreator(task)) {
Expand All @@ -72,11 +76,11 @@ function buildTaskGraph(pipeline, referenceCreators) {
return pipelineGraph;
}

function errorCyclesInPipeline(pipeline, referenceCreators, report) {
const pipelineTaskGraph = buildTaskGraph(pipeline, referenceCreators);
function errorCyclesInPipeline(name, tasks, referenceCreators, report) {
const pipelineTaskGraph = buildTaskGraph(name, tasks, referenceCreators);
for (const cycle of alg.findCycles(pipelineTaskGraph)) {
for (const taskNameInCycle of cycle) {
const taskInCycle = Object.values<any>(pipeline.spec.tasks).find((task) => task.name === taskNameInCycle);
const taskInCycle = Object.values<any>(tasks).find((task) => task.name === taskNameInCycle);
report(
`Cycle found in tasks (dependency graph): ${[...cycle, cycle[0]].join(' -> ')}`,
taskInCycle,
Expand All @@ -87,7 +91,24 @@ function errorCyclesInPipeline(pipeline, referenceCreators, report) {
}

export default (docs, tekton, report) => {
for (const pipeline of Object.values(tekton.pipelines)) {
errorCyclesInPipeline(pipeline, [runAfterReferences, paramsReferences, resourceInputReferences], report);
for (const pipeline of Object.values<any>(tekton.pipelines)) {
try {
const name = pipeline.metadata.name;
const tasks = pipeline.spec.tasks;
errorCyclesInPipeline(name, tasks, [runAfterReferences, paramsReferences, resourceInputReferences], report);
} catch (e) {
throw new RuleError("Can't process", 'no-pipeline-task-cycle', pipeline.metadata.name);
}
}

for (const pipeline of Object.values<any>(tekton.pipelineRuns)) {
try {
if (!pipeline.spec.pipelineSpec) continue;
const name = pipeline.metadata.name;
const tasks = pipeline.spec.pipelineSpec.tasks;
errorCyclesInPipeline(name, tasks, [runAfterReferences, paramsReferences, resourceInputReferences], report);
} catch (e) {
throw new RuleError("Can't process", 'no-pipeline-task-cycle', pipeline.metadata.name);
}
}
};
9 changes: 9 additions & 0 deletions src/rules/prefer-kebab-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,13 @@ export default (docs, tekton, report) => {
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<any>(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),
);
}
};

0 comments on commit 641813a

Please sign in to comment.