diff --git a/package.json b/package.json index 656738e8..3788f327 100644 --- a/package.json +++ b/package.json @@ -240,6 +240,12 @@ "category": "Tekton", "enablement": "tekton:tkn" }, + { + "command": "tekton.triggerTemplate.url", + "title": "Copy Expose URL", + "category": "Tekton", + "enablement": "tekton:tkn" + }, { "command": "tekton.pipeline.list", "title": "List Pipelines", @@ -535,6 +541,10 @@ "command": "k8s.tekton.pipeline.start", "when": "view =~ /^tekton(CustomTree|PipelineExplorer)View/" }, + { + "command": "tekton.triggerTemplate.url", + "when": "view =~ /^tekton(CustomTree|PipelineExplorer)View/" + }, { "command": "tekton.openInEditor", "when": "view =~ /^tekton(CustomTree|PipelineExplorer)View/" @@ -664,6 +674,11 @@ "when": "view =~ /^tekton(CustomTree|PipelineExplorer)View/ && viewItem == pipeline", "group": "c1@2" }, + { + "command": "tekton.triggerTemplate.url", + "when": "view =~ /^tekton(CustomTree|PipelineExplorer)View/ && viewItem == triggertemplates", + "group": "c1@2" + }, { "command": "tekton.addTrigger", "when": "view =~ /^tekton(CustomTree|PipelineExplorer)View/ && viewItem == pipeline", diff --git a/src/extension.ts b/src/extension.ts index 733e2f29..994704a2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -30,6 +30,7 @@ import { deleteFromExplorer, deleteFromCustom } from './commands/delete'; import { addTrigger } from './tekton/trigger'; import { triggerDetection } from './util/detection'; import { showDiagnosticData } from './tekton/diagnostic'; +import { TriggerTemplate } from './tekton/triggertemplate'; export let contextGlobalState: vscode.ExtensionContext; let k8sExplorer: k8s.ClusterExplorerV1 | undefined = undefined; @@ -73,6 +74,7 @@ export async function activate(context: vscode.ExtensionContext): Promise vscode.commands.registerCommand('tekton.pipelinerun.followLogs.palette', (context) => execute(PipelineRun.followLogs, context)), vscode.commands.registerCommand('tekton.pipelinerun.cancel', (context) => execute(PipelineRun.cancel, context)), vscode.commands.registerCommand('tekton.pipelinerun.cancel.palette', (context) => execute(PipelineRun.cancel, context)), + vscode.commands.registerCommand('tekton.triggerTemplate.url', (context) => execute(TriggerTemplate.copyExposeUrl, context)), vscode.commands.registerCommand('tekton.task.start', (context) => execute(Task.start, context)), vscode.commands.registerCommand('tekton.task.start.palette', (context) => execute(Task.start, context)), vscode.commands.registerCommand('tekton.task.list', (context) => execute(Task.list, context)), diff --git a/src/tekton/addtrigger.ts b/src/tekton/addtrigger.ts index 94c0b788..867c1ebb 100644 --- a/src/tekton/addtrigger.ts +++ b/src/tekton/addtrigger.ts @@ -22,6 +22,7 @@ import { Progress } from '../util/progress'; import { cli } from '../cli'; import { TknVersion, version } from '../util/tknversion'; import { NewPvc } from './createpvc'; +import { getExposeURl } from '../util/exposeurl'; export const TriggerTemplateModel = { apiGroup: 'triggers.tekton.dev', @@ -100,7 +101,10 @@ export async function k8sCreate(trigger: TriggerTemplateKind | EventListenerKind vscode.window.showErrorMessage(`Fail to deploy Resources: ${getStderrString(result.error)}`); return false; } - if (trigger.kind === RouteModel.kind && !result.error) vscode.window.showInformationMessage('Trigger successfully created.'); + if (trigger.kind === RouteModel.kind && !result.error) { + const url = await getExposeURl(trigger.metadata.name); + vscode.window.showInformationMessage(`Trigger successfully created. Expose URL: ${url}`); + } await fs.unlink(fsPath); return true; } diff --git a/src/tekton/triggertemplate.ts b/src/tekton/triggertemplate.ts index bd1365ad..6eddaf4d 100644 --- a/src/tekton/triggertemplate.ts +++ b/src/tekton/triggertemplate.ts @@ -3,14 +3,45 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ - +import * as vscode from 'vscode'; import { TektonItem } from './tektonitem'; -import { TektonNode, Command } from '../tkn'; +import { TektonNode, Command, tkn } from '../tkn'; import { CliCommand } from '../cli'; +import { getExposeURl } from '../util/exposeurl'; export class TriggerTemplate extends TektonItem { static getDeleteCommand(item: TektonNode): CliCommand { return Command.deleteTriggerTemplate(item.getName()); } + + static async copyExposeUrl(trigger: TektonNode): Promise { + if (!trigger) return null; + const result = await tkn.execute(Command.listEventListener()); + const listEventListener = JSON.parse(result.stdout).items; + if (listEventListener.length === 0) { + vscode.window.showInformationMessage('Expose URl not available'); + return null; + } + for (const eventListener of listEventListener) { + for (const triggers of eventListener.spec.triggers) { + if (triggers?.template?.name === trigger.getName()) { + const url = await getExposeURl(eventListener.status.configuration.generatedName); + vscode.env.clipboard.writeText(url); + vscode.window.showInformationMessage('Expose URl successfully copied'); + return; + } else if (triggers?.triggerRef) { + const triggerData = await tkn.execute(Command.getTrigger(triggers.triggerRef)); + const triggerName = JSON.parse(triggerData.stdout).spec.template.name; + if (triggerName === trigger.getName()) { + const url = await getExposeURl(eventListener.status.configuration.generatedName); + vscode.env.clipboard.writeText(url); + vscode.window.showInformationMessage('Expose URl successfully copied'); + return; + } + } + } + } + vscode.window.showInformationMessage('Expose URl not available'); + } } diff --git a/src/tkn.ts b/src/tkn.ts index 289b9e8a..5bef6569 100644 --- a/src/tkn.ts +++ b/src/tkn.ts @@ -427,6 +427,14 @@ export class Command { return newK8sCommand('apply', '-f', file); } + static getRoute(name: string): CliCommand { + return newK8sCommand('get', 'route', name, '-o', 'json'); + } + + static getTrigger(name: string): CliCommand { + return newK8sCommand('get', 'trigger', name, '-o', 'json'); + } + } const IMAGES = '../../images'; diff --git a/src/util/exposeurl.ts b/src/util/exposeurl.ts new file mode 100644 index 00000000..bb2cb613 --- /dev/null +++ b/src/util/exposeurl.ts @@ -0,0 +1,20 @@ +/*----------------------------------------------------------------------------------------------- + * 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 _ from 'lodash'; +import { Command, tkn } from '../tkn'; + + + +export async function getExposeURl(name: string): Promise { + const result = await tkn.execute(Command.getRoute(name)); + const route = JSON.parse(result.stdout); + const scheme = _.get(route, 'spec.tls.termination') ? 'https' : 'http'; + let url = `${scheme}://${route.spec.host}`; + if (route.spec?.path) { + url += route.spec.path; + } + return url; +} diff --git a/test/tekton/triggertemplate.test.ts b/test/tekton/triggertemplate.test.ts new file mode 100644 index 00000000..6b92faa6 --- /dev/null +++ b/test/tekton/triggertemplate.test.ts @@ -0,0 +1,182 @@ +/*----------------------------------------------------------------------------------------------- + * 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 chai from 'chai'; +import * as vscode from 'vscode'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import { EventListenerKind } from '../../src/tekton'; +import { TriggerTemplate } from '../../src/tekton/triggertemplate'; +import { ContextType, TknImpl } from '../../src/tkn'; +import { TestItem } from './testTektonitem'; + +const expect = chai.expect; +chai.use(sinonChai); + +suite('Tekton/Pipeline', () => { + const sandbox = sinon.createSandbox(); + let exeStub: sinon.SinonStub; + const pipelineNode = new TestItem(TknImpl.ROOT, 'test-pipeline', ContextType.PIPELINENODE, null); + const triggerTemplateItem = new TestItem(pipelineNode, 'trigger-template-sample-pipeline-cluster-task-4-awhmgc', ContextType.TRIGGERTEMPLATES, null); + + setup(() => { + exeStub = sandbox.stub(TknImpl.prototype, 'execute').resolves({ + error: '', + stdout: '' + }); + }); + + teardown(() => { + sandbox.restore(); + }); + + const eventListener: EventListenerKind[] = [{ + apiVersion:'triggers.tekton.dev/v1alpha1', + kind:'EventListener', + metadata: { + name:'event-listener-jwwe6j', + namespace:'pipelines-tutorial' + }, + spec: { + serviceAccountName:'pipeline', + triggers: [{ + bindings: [{ + kind:'TriggerBinding', + name:'vote-app' + }], + template: { + name:'trigger-template-sample-pipeline-cluster-task-4-awhmgc' + } + }] + }, + status: { + configuration: { + generatedName:'el-event-listener-jwwe6j' + } + } + }]; + + const eventListenerTriggerRef: EventListenerKind[] = [{ + apiVersion:'triggers.tekton.dev/v1alpha1', + kind:'EventListener', + metadata: { + name:'event-listener-jwwe6j', + namespace:'pipelines-tutorial' + }, + spec: { + serviceAccountName:'pipeline', + triggers: [{ + triggerRef: 'test' + }] + }, + status: { + configuration: { + generatedName:'el-event-listener-jwwe6j' + } + } + }]; + + const triggerData = { + spec: { + template: { + name: 'trigger-template-sample-pipeline-cluster-task-4-awhmgc' + } + } + } + + const route = { + spec: { + host: 'test.openshift.com' + } + } + + suite('Add trigger', () => { + test('copy expose URL', async () => { + exeStub.onFirstCall().resolves({ + error: '', + stdout: JSON.stringify({items: eventListener}) + }); + exeStub.onSecondCall().resolves({ + error: '', + stdout: JSON.stringify(route) + }); + const writeTextStub = sandbox.stub(vscode.env.clipboard, 'writeText').resolves('http://test.openshift.com'); + const infoMsg = sandbox.stub(vscode.window, 'showInformationMessage').resolves('Expose URl successfully copied'); + await TriggerTemplate.copyExposeUrl(triggerTemplateItem); + expect(exeStub).called; + expect(infoMsg).is.calledOnce; + expect(writeTextStub).called; + }); + + test('copy expose URL for triggerRef', async () => { + exeStub.onFirstCall().resolves({ + error: '', + stdout: JSON.stringify({items: eventListenerTriggerRef}) + }); + exeStub.onSecondCall().resolves({ + error: '', + stdout: JSON.stringify(triggerData) + }); + exeStub.onThirdCall().resolves({ + error: '', + stdout: JSON.stringify(route) + }); + const writeTextStub = sandbox.stub(vscode.env.clipboard, 'writeText').resolves('http://test.openshift.com'); + const infoMsg = sandbox.stub(vscode.window, 'showInformationMessage').resolves('Expose URl successfully copied'); + await TriggerTemplate.copyExposeUrl(triggerTemplateItem); + expect(exeStub).called; + expect(infoMsg).is.calledOnce; + expect(writeTextStub).called; + }); + + test('return null if no EventListener found', async () => { + exeStub.onFirstCall().resolves({ + error: '', + stdout: JSON.stringify({items: []}) + }); + const infoMsg = sandbox.stub(vscode.window, 'showInformationMessage').resolves('Expose URl not available'); + const result = await TriggerTemplate.copyExposeUrl(triggerTemplateItem); + expect(result).equals(null); + expect(infoMsg).is.calledOnce; + }); + + test('expose URL not found', async () => { + exeStub.onFirstCall().resolves({ + error: '', + stdout: JSON.stringify({items: [{ + apiVersion:'triggers.tekton.dev/v1alpha1', + kind:'EventListener', + metadata: { + name:'event-listener-jwwe6j', + namespace:'pipelines-tutorial' + }, + spec: { + serviceAccountName:'pipeline', + triggers: [{ + bindings: [{ + kind:'TriggerBinding', + name:'vote-app' + }], + template: { + name:'test' + } + }] + }, + status: { + configuration: { + generatedName:'el-event-listener-jwwe6j' + } + } + }]}) + }); + const infoMsg = sandbox.stub(vscode.window, 'showInformationMessage').resolves('Expose URl not available'); + await TriggerTemplate.copyExposeUrl(triggerTemplateItem); + expect(infoMsg).is.calledOnce; + }); + }); +});