diff --git a/package.json b/package.json index 27bc91e2..50ec31de 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,12 @@ "default": 5, "description": "Tree pagination limit for some tekton related nodes(pipeline runs/task runs)" } + }, + "vs-tekton.deploy": { + "Title": "Deploy command", + "type": "boolean", + "default": false, + "description": "Enable/disable to Deploy the yaml resources on cluster" } } }, @@ -821,6 +827,7 @@ "git-transport-protocol": "^0.1.0", "hasha": "5.0.0", "humanize-duration": "^3.21.0", + "js-yaml": "^3.13.1", "jstream": "^1.1.1", "lodash": "^4.17.15", "mkdirp": "^0.5.1", diff --git a/src/extension.ts b/src/extension.ts index 9b83f32f..b63583e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,6 +28,7 @@ import { k8sCommands } from './kubernetes'; import { initializeTknEditing } from './yaml-support/tkn-editing'; import { ToolsConfig } from './tools'; import { TKN_RESOURCE_SCHEME, TKN_RESOURCE_SCHEME_READONLY, tektonVfsProvider } from './util/tekton-vfs'; +import { updateTektonResource } from './tekton/deploy'; export let contextGlobalState: vscode.ExtensionContext; let k8sExplorer: k8s.ClusterExplorerV1 | undefined = undefined; @@ -116,7 +117,9 @@ export async function activate(context: vscode.ExtensionContext): Promise ).at(undefined); k8sExplorer.registerNodeContributor(nodeContributor); } - + vscode.workspace.onDidSaveTextDocument(async (document: vscode.TextDocument) => { + await updateTektonResource(document); + }); registerYamlSchemaSupport(context); registerPipelinePreviewContext(); initializeTknEditing(context); diff --git a/src/tekton/deploy.ts b/src/tekton/deploy.ts new file mode 100644 index 00000000..4e8f143f --- /dev/null +++ b/src/tekton/deploy.ts @@ -0,0 +1,72 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as path from 'path'; +import { cli } from '../cli'; +import * as fs from 'fs-extra'; +import * as yaml from 'js-yaml'; +import * as vscode from 'vscode'; +import { contextGlobalState } from '../extension'; +import { tektonYaml } from '../yaml-support/tkn-yaml'; +import { pipelineExplorer } from '../pipeline/pipelineExplorer'; +import { getStderrString, Command, newK8sCommand } from '../tkn'; + + +function checkDeploy(): boolean { + return vscode.workspace + .getConfiguration('vs-tekton') + .get('deploy'); +} + +export async function updateTektonResource(document: vscode.TextDocument): Promise { + let value: string; + if (!checkDeploy()) return; + if (document.languageId !== 'yaml') return; + if (!(document.uri.scheme.startsWith('tekton'))) { + const verifyTknYaml = tektonYaml.isTektonYaml(document); + if (!contextGlobalState.workspaceState.get(document.uri.fsPath) && verifyTknYaml) { + value = await vscode.window.showWarningMessage('Detected Tekton resources. Do you want to deploy to cluster?', 'Deploy', 'Deploy Once', 'Cancel'); + } + if (value === 'Deploy') { + contextGlobalState.workspaceState.update(document.uri.fsPath, true); + } + if (verifyTknYaml && (/Deploy/.test(value) || contextGlobalState.workspaceState.get(document.uri.fsPath))) { + const result = await cli.execute(Command.create(document.uri.fsPath)); + if (result.error) { + const tempPath = os.tmpdir(); + if (!tempPath) { + return; + } + const fsPath = path.join(tempPath, path.basename(document.uri.fsPath)); + try { + let yamlData = ''; + const resourceCheckRegex = /^(Task|PipelineResource|Pipeline|Condition|ClusterTask|EventListener|TriggerBinding)$/ as RegExp; + const fileContents = await fs.readFile(document.uri.fsPath, 'utf8'); + const data: object[] = yaml.safeLoadAll(fileContents).filter((obj: {kind: string}) => resourceCheckRegex.test(obj.kind)); + if (data.length === 0) return; + data.map(value => { + const yamlStr = yaml.safeDump(value); + yamlData += yamlStr + '---\n'; + }) + await fs.writeFile(fsPath, yamlData, 'utf8'); + } catch (err) { + // ignore + } + const apply = await cli.execute(newK8sCommand(`apply -f ${fsPath}`)); + await fs.unlink(fsPath); + if (apply.error) { + vscode.window.showErrorMessage(`Fail to deploy Resources: ${getStderrString(apply.error)}`); + } else { + pipelineExplorer.refresh(); + vscode.window.showInformationMessage('Resources were successfully Deploy.'); + } + } else { + pipelineExplorer.refresh(); + vscode.window.showInformationMessage('Resources were successfully Created.'); + } + } + } +} diff --git a/src/tkn.ts b/src/tkn.ts index d573353c..c25ad66e 100644 --- a/src/tkn.ts +++ b/src/tkn.ts @@ -352,6 +352,9 @@ export class Command { static getPipelineResource(): CliCommand { return newK8sCommand('get', 'pipelineresources', '-o', 'json'); } + static create(file: string): CliCommand { + return newK8sCommand('create', '--save-config','-f', file); + } } export class TektonNodeImpl implements TektonNode { diff --git a/src/yaml-support/tkn-yaml.ts b/src/yaml-support/tkn-yaml.ts index 86dea201..2f395149 100644 --- a/src/yaml-support/tkn-yaml.ts +++ b/src/yaml-support/tkn-yaml.ts @@ -9,12 +9,18 @@ import { TknElementType } from '../model/common'; import { PipelineTask, PipelineTaskCondition } from '../model/pipeline/pipeline-model'; const TEKTON_API = 'tekton.dev/'; +const TRIGGER_API = 'triggers.tekton.dev'; export enum TektonYamlType { Task = 'Task', TaskRun = 'TaskRun', Pipeline = 'Pipeline', + Condition = 'Condition', + ClusterTask = 'ClusterTask', PipelineRun = 'PipelineRun', + EventListener = 'EventListener', + TriggerBinding = 'TriggerBinding', + TriggerTemplate = 'TriggerTemplate', PipelineResource = 'PipelineResource' } @@ -93,7 +99,7 @@ export class TektonYaml { if (rootMap) { const apiVersion = getYamlMappingValue(rootMap, 'apiVersion'); const kind = getYamlMappingValue(rootMap, 'kind'); - if (apiVersion && apiVersion.startsWith(TEKTON_API)) { + if (apiVersion?.startsWith(TEKTON_API) || apiVersion?.startsWith(TRIGGER_API)) { return TektonYamlType[kind]; } } diff --git a/test/tekton/deploy.test.ts b/test/tekton/deploy.test.ts new file mode 100644 index 00000000..0e03767f --- /dev/null +++ b/test/tekton/deploy.test.ts @@ -0,0 +1,222 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as os from 'os'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as fs from 'fs-extra'; +import * as vscode from 'vscode'; +import { cli } from '../../src/cli'; +import * as sinonChai from 'sinon-chai'; +import { Command } from '../../src/tkn'; +import { updateTektonResource } from '../../src/tekton/deploy'; +import { contextGlobalState } from '../../src/extension'; +import { tektonYaml } from '../../src/yaml-support/tkn-yaml'; +import { pipelineExplorer } from '../../src/pipeline/pipelineExplorer'; + +const expect = chai.expect; +chai.use(sinonChai); + +suite('Deploy File', () => { + const sandbox = sinon.createSandbox(); + let execStub: sinon.SinonStub; + let osStub: sinon.SinonStub; + let unlinkStub: sinon.SinonStub; + let writeFileStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let showWarningMessageStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + let workspaceStateGetStub: sinon.SinonStub; + let workspaceStateUpdateStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + + const sampleYaml = ` + # pipeline.yaml + apiVersion: tekton.dev/v1beta1 + kind: Pipeline + metadata: + name: sample-pipeline-cluster-task-4 + spec: + tasks: + - name: cluster-task-pipeline-4 + taskRef: + name: cluster-task-pipeline-4 + kind: ClusterTask + `; + + const textDocument: vscode.TextDocument = { + uri: { + authority: '', + fragment: '', + fsPath: 'workspace.yaml', + scheme: '', + path: '', + query: '', + with: sandbox.stub(), + toJSON: sandbox.stub() + }, + fileName: 'workspace.yaml', + isClosed: false, + isDirty: false, + isUntitled: false, + languageId: 'yaml', + version: 1, + eol: vscode.EndOfLine.CRLF, + save: undefined, + lineCount: 33, + lineAt: undefined, + getText: sinon.stub().returns(sampleYaml), + getWordRangeAtPosition: undefined, + offsetAt: undefined, + positionAt: undefined, + validatePosition: undefined, + validateRange: undefined + }; + + + setup(() => { + sandbox.stub(vscode.workspace, 'getConfiguration').returns({ + get(): Promise { + return Promise.resolve(undefined); + }, + update(): Promise { + return Promise.resolve(); + }, + inspect(): { + key: string; + } { + return undefined; + }, + has(): boolean { + return true; + }, + deploy: true + }); + osStub = sandbox.stub(os, 'tmpdir').resolves(); + unlinkStub = sandbox.stub(fs, 'unlink').resolves(); + writeFileStub = sandbox.stub(fs, 'writeFile').resolves(); + readFileStub = sandbox.stub(fs, 'readFile').resolves(` + apiVersion: tekton.dev/v1beta1 + kind: Task + metadata: + name: print-data + spec: + workspaces: + - name: storage + readOnly: true + params: + - name: filename + steps: + - name: print-secrets + image: ubuntu + script: cat $(workspaces.storage.path)/$(params.filename) + `); + showWarningMessageStub = sandbox.stub(vscode.window, 'showWarningMessage').resolves(); + showErrorMessageStub = sandbox.stub(vscode.window, 'showErrorMessage').resolves(); + showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage').resolves(); + workspaceStateGetStub = sandbox.stub(contextGlobalState.workspaceState, 'get').resolves(); + workspaceStateUpdateStub = sandbox.stub(contextGlobalState.workspaceState, 'update').resolves(); + execStub = sandbox.stub(cli, 'execute').resolves(); + sandbox.stub(pipelineExplorer, 'refresh').resolves(); + sandbox.stub(tektonYaml, 'isTektonYaml').resolves('ClusterTask'); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('Deploy command', () => { + + test('calls the appropriate kubectl command to deploy on cluster', async () => { + execStub.resolves({ + error: undefined, + stderr: '', + stdout: 'successfully created' + }); + workspaceStateGetStub.onFirstCall().returns(undefined); + showWarningMessageStub.onFirstCall().resolves('Deploy Once'); + await updateTektonResource(textDocument); + expect(execStub).calledOnceWith(Command.create('workspace.yaml')); + unlinkStub.calledOnce; + osStub.calledOnce; + readFileStub.calledOnce; + writeFileStub.calledOnce; + showInformationMessageStub.calledOnce; + showWarningMessageStub.calledOnce; + workspaceStateGetStub.calledOnce; + }); + + test('get deploy data from workspaceState', async () => { + execStub.resolves({ + error: undefined, + stderr: '', + stdout: 'successfully created' + }); + workspaceStateGetStub.onFirstCall().returns('path'); + await updateTektonResource(textDocument); + expect(execStub).calledOnceWith(Command.create('workspace.yaml')); + showInformationMessageStub.calledOnce; + showWarningMessageStub.calledOnce; + workspaceStateGetStub.calledOnce; + }); + + test('Update the yaml if fail to create resources', async () => { + execStub.onFirstCall().resolves({ + error: 'error', + stderr: 'error', + stdout: '' + }); + execStub.onSecondCall().resolves({ + error: '', + stderr: '', + stdout: 'successfully Deploy' + }); + osStub.returns('path'); + workspaceStateGetStub.onFirstCall().returns('path'); + await updateTektonResource(textDocument); + showErrorMessageStub.calledOnce; + workspaceStateGetStub.calledOnce; + showInformationMessageStub.calledOnce; + }); + + test('Throw error when apply command fails', async () => { + execStub.onFirstCall().resolves({ + error: 'error', + stderr: 'error', + stdout: '' + }); + execStub.onSecondCall().resolves({ + error: 'error', + stderr: 'error', + stdout: '' + }); + osStub.returns('path'); + workspaceStateGetStub.onFirstCall().returns('path'); + await updateTektonResource(textDocument); + showErrorMessageStub.calledTwice; + workspaceStateGetStub.calledOnce; + }); + + test('update the path to workspaceState', async () => { + execStub.resolves({ + error: undefined, + stderr: '', + stdout: 'successfully created' + }); + workspaceStateGetStub.onFirstCall().returns(undefined); + showWarningMessageStub.onFirstCall().resolves('Deploy'); + workspaceStateUpdateStub.onFirstCall().resolves('path'); + await updateTektonResource(textDocument); + expect(execStub).calledOnceWith(Command.create('workspace.yaml')); + showInformationMessageStub.calledOnce; + showWarningMessageStub.calledOnce; + workspaceStateGetStub.calledOnce; + workspaceStateUpdateStub.calledOnce; + }); + + }); +});