From e82cde36988c36e755176161f0f9e630b57ba01d Mon Sep 17 00:00:00 2001 From: Yevhen Vydolob Date: Mon, 21 Sep 2020 14:58:05 +0300 Subject: [PATCH] #345 support finally tasks Signed-off-by: Yevhen Vydolob --- preview-src/index.ts | 6 ++ preview-src/model.ts | 1 + src/model/pipeline/pipeline-model.ts | 13 +++- src/pipeline/pipeline-graph.ts | 4 +- src/yaml-support/tkn-yaml.ts | 61 +++++++++++++------ test/model/pipeline/pipeline-model.test.ts | 12 +++- .../model/pipeline/pipeline-with-finally.yaml | 29 +++++++++ test/yaml-support/tkn-yaml.test.ts | 26 +++++++- 8 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 test/model/pipeline/pipeline-with-finally.yaml diff --git a/preview-src/index.ts b/preview-src/index.ts index 3f3c535d..43b680c7 100644 --- a/preview-src/index.ts +++ b/preview-src/index.ts @@ -192,6 +192,12 @@ function getStyle(style: CyTheme): cytoscape.Stylesheet[] { 'color': style.labelColor, }, }, + { + selector: 'node[?final]', + style: { + 'shape': 'diamond', + }, + }, { selector: 'node[state = "Started"]', style: { diff --git a/preview-src/model.ts b/preview-src/model.ts index 6f97812a..57ce658f 100644 --- a/preview-src/model.ts +++ b/preview-src/model.ts @@ -13,6 +13,7 @@ export interface NodeData extends BaseData { id: string; state?: 'Cancelled'| 'Finished' | 'Started' | 'Failed' | 'Unknown'; yamlPosition?: number; + final?: boolean; } export interface EdgeData extends BaseData { diff --git a/src/model/pipeline/pipeline-model.ts b/src/model/pipeline/pipeline-model.ts index 7ad32229..f78ffd37 100644 --- a/src/model/pipeline/pipeline-model.ts +++ b/src/model/pipeline/pipeline-model.ts @@ -49,6 +49,7 @@ export class PipelineSpec extends NodeTknElement { private _description: TknStringElement; private _params: TknArray; private _workspaces: TknArray; + private _finally: TknArray; constructor(parent: Pipeline, node: YamlMap) { super(parent, node); @@ -99,8 +100,18 @@ export class PipelineSpec extends NodeTknElement { return this._workspaces; } + get finally(): TknArray { + if (!this._finally) { + const finallyNode = findNodeByKey('finally', this.node as YamlMap); + if (finallyNode) { + this._finally = new TknArray(TknElementType.PIPELINE_TASKS, PipelineTask, this, finallyNode); + } + } + return this._finally; + } + collectChildren(): TknElement[] { - return [this.tasks, this.description, this.resources, this.params, this.workspaces]; + return [this.tasks, this.description, this.resources, this.params, this.workspaces, this.finally]; } } diff --git a/src/pipeline/pipeline-graph.ts b/src/pipeline/pipeline-graph.ts index 6351ed49..88b3fc1e 100644 --- a/src/pipeline/pipeline-graph.ts +++ b/src/pipeline/pipeline-graph.ts @@ -107,8 +107,8 @@ function convertTasksToNode(tasks: PipelineRunTask[], includePositions = true): tasks.forEach((task: DeclaredTask) => tasksMap.set(task.name, task)); for (const task of tasks) { - result.push({ data: { id: task.name, label: getLabel(task), type: task.kind, taskRef: task.taskRef, state: task.state, yamlPosition: includePositions ? task.position : undefined } as NodeData }); - for (const after of task.runAfter) { + result.push({ data: { id: task.name, label: getLabel(task), type: task.kind, taskRef: task.taskRef, state: task.state, yamlPosition: includePositions ? task.position : undefined, final: task.final } as NodeData }); + for (const after of task.runAfter ?? []) { if (tasksMap.has(after)) { result.push({ data: { source: after, target: task.name, id: `${after}-${task.name}`, state: tasksMap.get(after).state } as EdgeData }); } diff --git a/src/yaml-support/tkn-yaml.ts b/src/yaml-support/tkn-yaml.ts index 9f133239..f8059f5f 100644 --- a/src/yaml-support/tkn-yaml.ts +++ b/src/yaml-support/tkn-yaml.ts @@ -36,6 +36,7 @@ export interface DeclaredTask { runAfter: string[]; kind: 'Task' | 'ClusterTask' | 'Condition' | string; position?: number; + final?: boolean; } export type RunState = 'Cancelled' | 'Finished' | 'Started' | 'Failed' | 'Unknown'; @@ -332,41 +333,63 @@ export const pipelineRunYaml = new PipelineRunYaml(); function collectTasks(specMap: YamlMap): DeclaredTask[] { const result: DeclaredTask[] = []; + const lastTasks: string[] = []; if (specMap) { const tasksSeq = getTasksSeq(specMap); if (tasksSeq) { for (const taskNode of tasksSeq.items) { if (taskNode.kind === 'MAPPING') { - const decTask = {} as DeclaredTask; - const nameValue = findNodeByKey('name', taskNode as YamlMap); - if (nameValue) { - decTask.name = nameValue.raw; - decTask.position = nameValue.startPosition; - } - - const taskRef = pipelineYaml.getTaskRef(taskNode as YamlMap); - if (taskRef) { - const taskRefName = findNodeByKey('name', taskRef); - decTask.taskRef = taskRefName.raw; - const kindName = findNodeByKey('kind', taskRef); - if (kindName) { - decTask.kind = kindName.raw; - } else { - decTask.kind = 'Task'; - } - } - + const decTask = toDeclaredTask(taskNode as YamlMap); decTask.runAfter = getRunAfter(taskNode as YamlMap); + if (decTask.runAfter.length === 0){ + lastTasks.push(decTask.name); + } collectConditions(taskNode as YamlMap, result); result.push(decTask); } } } + + + // collect finally tasks + const finallyTasks = findNodeByKey('finally', specMap); + if (finallyTasks) { + for (const finalTask of finallyTasks.items) { + if (finalTask.kind === 'MAPPING') { + const fTask = toDeclaredTask(finalTask as YamlMap); + fTask.runAfter = lastTasks; + fTask.final = true; + result.push(fTask); + } + } + } } return result; } +function toDeclaredTask(taskNode: YamlMap): DeclaredTask { + const decTask = {} as DeclaredTask; + const nameValue = findNodeByKey('name', taskNode); + if (nameValue) { + decTask.name = nameValue.raw; + decTask.position = nameValue.startPosition; + } + + const taskRef = pipelineYaml.getTaskRef(taskNode); + if (taskRef) { + const taskRefName = findNodeByKey('name', taskRef); + decTask.taskRef = taskRefName.raw; + const kindName = findNodeByKey('kind', taskRef); + if (kindName) { + decTask.kind = kindName.raw; + } else { + decTask.kind = 'Task'; + } + } + return decTask; +} + function collectConditions(taskNode: YamlMap, tasks: DeclaredTask[]): void { const conditions = findNodeByKey('conditions', taskNode); diff --git a/test/model/pipeline/pipeline-model.test.ts b/test/model/pipeline/pipeline-model.test.ts index 0c490d0f..6db98e9b 100644 --- a/test/model/pipeline/pipeline-model.test.ts +++ b/test/model/pipeline/pipeline-model.test.ts @@ -10,7 +10,7 @@ import * as vscode from 'vscode'; import { yamlLocator } from '../../../src/yaml-support/yaml-locator'; import { TknElementType } from '../../../src/model/common'; import { TektonYamlType } from '../../../src/yaml-support/tkn-yaml'; -import { Pipeline, PipelineTaskRef } from '../../../src/model/pipeline/pipeline-model'; +import { Pipeline, PipelineTask, PipelineTaskRef } from '../../../src/model/pipeline/pipeline-model'; const expect = chai.expect; chai.use(sinonChai); @@ -97,6 +97,16 @@ suite('Pipeline Model', () => { expect(tknEl.type).eq(TknElementType.PIPELINE_TASK_REF); expect((tknEl as PipelineTaskRef).name.value).eq('build-docker-image-from-git-source'); }); + + test('findElementAt should return finally task', async () => { + const yaml = await fs.readFile(path.join(__dirname, '..', '..', '..', '..', 'test', 'model', 'pipeline', 'pipeline-with-finally.yaml')); + const docs = yamlLocator.getTknDocuments({ getText: () => yaml.toString(), version: 1, uri: vscode.Uri.parse('file:///model/pipeline/pipeline-with-finally.yaml') } as vscode.TextDocument); + const tknDoc = docs[0]; + const tknEl = tknDoc.findElementAt(new vscode.Position(26, 12)); + expect(tknEl).not.undefined; + expect(tknEl.type).eq(TknElementType.PIPELINE_TASK); + expect((tknEl as PipelineTask).name.value).eq('cleanup'); + }); }); diff --git a/test/model/pipeline/pipeline-with-finally.yaml b/test/model/pipeline/pipeline-with-finally.yaml new file mode 100644 index 00000000..6ff1bb70 --- /dev/null +++ b/test/model/pipeline/pipeline-with-finally.yaml @@ -0,0 +1,29 @@ +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: clone-cleanup-workspace +spec: + workspaces: + # common workspace where git repo is cloned and needs to be cleanup after done + - name: git-source + tasks: + # Clone app repo to workspace + - name: clone-app-repo + taskRef: + name: git-clone-from-catalog + params: + - name: url + value: https://github.com/tektoncd/community.git + - name: subdirectory + value: application + workspaces: + - name: output + workspace: git-source + finally: + # Cleanup workspace + - name: cleanup + taskRef: + name: cleanup-workspace + workspaces: + - name: source + workspace: git-source diff --git a/test/yaml-support/tkn-yaml.test.ts b/test/yaml-support/tkn-yaml.test.ts index 4a3dd586..8e0456f8 100644 --- a/test/yaml-support/tkn-yaml.test.ts +++ b/test/yaml-support/tkn-yaml.test.ts @@ -282,7 +282,6 @@ suite('Tekton yaml', () => { suite('PipelineRun', () => { - test('should return pipelinerun name', async () => { const yaml = await fs.readFile(path.join(__dirname, '..', '..', '..', 'test', '/yaml-support/pipelinerun.yaml')); const docs = tektonYaml.getTektonDocuments({ getText: () => yaml.toString(), version: 1, uri: vscode.Uri.parse('file:///pipelinerun/pipelinerun.yaml') } as vscode.TextDocument, TektonYamlType.PipelineRun); @@ -299,4 +298,29 @@ suite('Tekton yaml', () => { expect(result).eql('Failed'); }); }); + + suite('Finally tasks', () => { + test('Should return finally tasks', () => { + const yaml = ` + apiVersion: tekton.dev/v1alpha1 + kind: Pipeline + metadata: + name: pipeline-with-parameters + spec: + tasks: + - name: build-skaffold-web + taskRef: + name: build-push + finally: + - name: final-ask + taskRef: + name: build-push + ` + const docs = tektonYaml.getTektonDocuments({ getText: () => yaml, version: 1, uri: vscode.Uri.parse('file:///foo/pipeline/finally/tasks.yaml') } as vscode.TextDocument, TektonYamlType.Pipeline); + const pipelineTasks = pipelineYaml.getPipelineTasks(docs[0]); + expect(pipelineTasks).is.not.empty; + expect(pipelineTasks[1].name).equal('final-ask'); + expect(pipelineTasks[1].runAfter).eql(['build-skaffold-web']); + }) + }); });