Skip to content

Commit

Permalink
Deploy Tekton resources on save (#269)
Browse files Browse the repository at this point in the history
* Deploy Tekton resources on save
  • Loading branch information
sudhirverma committed May 21, 2020
1 parent 28d09fd commit 651ed2d
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 2 deletions.
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -116,7 +117,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
).at(undefined);
k8sExplorer.registerNodeContributor(nodeContributor);
}

vscode.workspace.onDidSaveTextDocument(async (document: vscode.TextDocument) => {
await updateTektonResource(document);
});
registerYamlSchemaSupport(context);
registerPipelinePreviewContext();
initializeTknEditing(context);
Expand Down
72 changes: 72 additions & 0 deletions src/tekton/deploy.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>('deploy');
}

export async function updateTektonResource(document: vscode.TextDocument): Promise<void> {
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.');
}
}
}
}
3 changes: 3 additions & 0 deletions src/tkn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion src/yaml-support/tkn-yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}

Expand Down Expand Up @@ -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];
}
}
Expand Down
222 changes: 222 additions & 0 deletions test/tekton/deploy.test.ts
Original file line number Diff line number Diff line change
@@ -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<T>(): Promise<T|undefined> {
return Promise.resolve(undefined);
},
update(): Promise<void> {
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;
});

});
});

0 comments on commit 651ed2d

Please sign in to comment.