Skip to content

Commit 45914b9

Browse files
committed
Add UI to create service binding
Right click on a component in the components view and select "Bind Service" in order to bind the component to an Operator-backed service. In order for this to work: - the service binding operator must be installed on the cluster - an operator that manages instances of services must be installed (I tested with the RHOAS operator) - there must be an instance of the service that is managed by the operator in the current project Other things in this PR: - Add a walkthrough GIF of using the UI - Add a smoke test to check that the "Bind Service" context menu item exists - Fix the smoke tests. This includes fixing the existing test case for #2780, even though we are going to rewrite that as a WebView UI soon. I did this, since for the "Bind Service" smoke test, we need a component present in the components view. Signed-off-by: David Thompson <davthomp@redhat.com>
1 parent 3ed826e commit 45914b9

File tree

21 files changed

+714
-45
lines changed

21 files changed

+714
-45
lines changed

.eslintignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ test-resources
99
src/webview/log
1010
src/webview/describe
1111
src/webview/cluster
12-
src/webview/webpack.config.js
1312
out
1413
src/webview/common
1514
src/webview/create-service

USAGE_DATA.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ In addition to generic command's usage data (see above) `Login` command also rep
3737
* openshift_version - cluster's OpenShift version (if can be accessed by the current user)
3838
* kubernetes_version - cluster's Kubernetes version
3939

40+
#### Bind Service
41+
42+
In addition to the generic command's usage data (see above), the `Bind Service` context menu option reports events when:
43+
44+
* the wizard to select the service to bind to is opened
45+
* the wizard to select the service to bind to is submitted
46+
4047
### Add Cluster Editor
4148

4249
The editor reports selection made on first page:

build/esbuild.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const webviews = [
1616
'helm-chart',
1717
'log',
1818
'welcome',
19-
'feedback'
19+
'feedback',
20+
'add-service-binding',
2021
];
2122

2223
function kebabToCamel(text) {
2.45 MB
Loading

package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,11 @@
440440
"title": "Start Dev",
441441
"category": "OpenShift"
442442
},
443+
{
444+
"command": "openshift.component.binding.add",
445+
"title": "Bind Service",
446+
"category": "OpenShift"
447+
},
443448
{
444449
"command": "openshift.component.exitDevMode",
445450
"title": "Stop Dev",
@@ -510,6 +515,10 @@
510515
"title": "Open Console Dashboard for Current Cluster",
511516
"category": "OpenShift"
512517
},
518+
{
519+
"command": "openshift.open.operatorBackedServiceCatalog",
520+
"title": "Open Operator Backed Service Catalog"
521+
},
513522
{
514523
"command": "openshift.component.debug",
515524
"title": "Debug",
@@ -910,6 +919,10 @@
910919
"command": "openshift.open.developerConsole",
911920
"when": "view == openshiftProjectExplorer"
912921
},
922+
{
923+
"command": "openshift.open.operatorBackedServiceCatalog",
924+
"when": "false"
925+
},
913926
{
914927
"command": "openshift.component.debug",
915928
"when": "view == openshiftProjectExplorer"
@@ -1069,6 +1082,10 @@
10691082
{
10701083
"command": "openshift.component.openInBrowser",
10711084
"when": "false"
1085+
},
1086+
{
1087+
"command": "openshift.component.binding.add",
1088+
"when": "false"
10721089
}
10731090
],
10741091
"view/title": [
@@ -1309,6 +1326,11 @@
13091326
"when": "view == openshiftComponentsView && viewItem =~ /openshift\\.component.*\\.dep-nrn.*/ || viewItem =~ /openshift\\.component.*\\.dep-run.*/",
13101327
"group": "c2@1"
13111328
},
1329+
{
1330+
"command": "openshift.component.binding.add",
1331+
"when": "view == openshiftComponentsView && viewItem =~ /openshift\\.component.*\\.dev-nrn.*/",
1332+
"group": "c2@2"
1333+
},
13121334
{
13131335
"command": "openshift.component.showDevTerminal",
13141336
"when": "view == openshiftComponentsView && viewItem =~ /openshift\\.component.*\\.dev-run.*/",

src/odo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export interface Odo {
8989
*/
9090
getActiveProject(): Promise<string>;
9191

92-
/*
92+
/**
9393
* Deletes all the odo configuration files associated with the component (`.odo`, `devfile.yaml`) located at the given path.
9494
*
9595
* @param componentPath the path to the component

src/odo/command.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,20 @@ export class Command {
320320
return new CommandText('oc api-resources | grep openshift');
321321
}
322322

323+
static addBinding(serviceNamespace: string, serviceName: string, bindingName: string): CommandText {
324+
return new CommandText('odo add binding',
325+
undefined,
326+
[
327+
new CommandOption('--service-namespace', serviceNamespace, false),
328+
new CommandOption('--service', serviceName, false),
329+
new CommandOption('--name', bindingName, false),
330+
]
331+
)
332+
}
333+
334+
static getBindableServices(): CommandText {
335+
return new CommandText('odo list service',
336+
undefined,
337+
[new CommandOption('-o json')]);
338+
}
323339
}

src/odo3.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { KubernetesObject } from '@kubernetes/client-node';
77
import { CommandText } from './base/command';
8-
import { CliChannel } from './cli';
8+
import { CliChannel, CliExitData } from './cli';
99
import { Command as CommonCommand, loadItems } from './k8s/common';
1010
import { Command as DeploymentCommand } from './k8s/deployment';
1111
import { DeploymentConfig } from './k8s/deploymentConfig';
@@ -20,6 +20,30 @@ export interface Odo3 {
2020
setNamespace(newNamespace: string): Promise<void>;
2121

2222
describeComponent(contextPath: string): Promise<ComponentDescription | undefined>;
23+
24+
/**
25+
* Bind a component to a bindable service by modifying the devfile
26+
*
27+
* Resolves when the binding it created.
28+
*
29+
* @param contextPath the path to the component
30+
* @param serviceName the name of the service to bind to
31+
* @param serviceNamespace the namespace the the service is in
32+
* @param bindingName the name of the service binding
33+
*/
34+
addBinding(
35+
contextPath: string,
36+
serviceName: string,
37+
serviceNamespace: string,
38+
bindingName: string,
39+
): Promise<void>;
40+
41+
/**
42+
* Returns a list of all the bindable services on the cluster.
43+
*
44+
* @returns a list of all the bindable services on the cluster
45+
*/
46+
getBindableServices(): Promise<KubernetesObject[]>;
2347
}
2448

2549
export class Odo3Impl implements Odo3 {
@@ -69,6 +93,49 @@ export class Odo3Impl implements Odo3 {
6993
// ignore and return undefined
7094
}
7195
}
96+
97+
async addBinding(contextPath: string, serviceNamespace: string, serviceName: string, bindingName: string) {
98+
const myCommand = Command.addBinding(serviceNamespace, serviceName, bindingName);
99+
await CliChannel.getInstance().executeTool(
100+
myCommand,
101+
{cwd: contextPath},
102+
true
103+
);
104+
}
105+
106+
async getBindableServices(): Promise<KubernetesObject[]> {
107+
const data: CliExitData = await CliChannel.getInstance().executeTool(
108+
Command.getBindableServices()
109+
);
110+
let responseObj;
111+
try {
112+
responseObj = JSON.parse(data.stdout);
113+
} catch {
114+
throw new Error(JSON.parse(data.stderr).message);
115+
}
116+
if (!responseObj.bindableServices) {
117+
return [];
118+
}
119+
return (responseObj.bindableServices as BindableService[]) //
120+
.map(obj => {
121+
return {
122+
kind: obj.kind,
123+
apiVersion: obj.apiVersion,
124+
metadata: {
125+
namespace: obj.namespace,
126+
name: obj.name,
127+
}
128+
} as KubernetesObject;
129+
});
130+
}
131+
}
132+
133+
interface BindableService {
134+
name: string;
135+
namespace: string;
136+
kind: string;
137+
apiVersion: string;
138+
service: string;
72139
}
73140

74141
export function newInstance(): Odo3 {

src/openshift/cluster.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { KubernetesObject } from '@kubernetes/client-node';
77
import { ExtensionContext, QuickInputButton, QuickPickItem, QuickPickItemButtonEvent, Terminal, ThemeIcon, Uri, Progress as VProgress, WebviewPanel, commands, env, window, workspace } from 'vscode';
88
import { CliChannel, CliExitData } from '../cli';
9+
import { getInstance } from '../odo';
910
import { Command } from '../odo/command';
1011
import { TokenStore } from '../util/credentialManager';
1112
import { Filters } from '../util/filters';
@@ -96,6 +97,20 @@ export class Cluster extends OpenShiftItem {
9697
});
9798
}
9899

100+
@vsCommand('openshift.open.operatorBackedServiceCatalog')
101+
@clusterRequired()
102+
static async openOpenshiftConsoleTopography(): Promise<void> {
103+
return Progress.execFunctionWithProgress('Opening Operator Backed Service Catalog', async (progress) => {
104+
const [consoleUrl, namespace] = await Promise.all([
105+
Cluster.getConsoleUrl(progress),
106+
getInstance().getActiveProject()
107+
]);
108+
progress.report({increment: 100, message: 'Starting default browser'});
109+
// eg. https://console-openshift-console.apps-crc.testing/catalog/ns/default?catalogType=OperatorBackedService
110+
return commands.executeCommand('vscode.open', Uri.parse(`${consoleUrl}/catalog/ns/${namespace}?catalogType=OperatorBackedService`));
111+
});
112+
}
113+
99114
@vsCommand('openshift.resource.openInDeveloperConsole')
100115
@clusterRequired()
101116
static async openInDeveloperConsole(resource: KubernetesObject): Promise<void> {

src/openshift/component.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ import { Command } from '../odo/command';
1515
import { ComponentTypeAdapter, ComponentTypeDescription, DevfileComponentType, ascDevfileFirst, isDevfileComponent } from '../odo/componentType';
1616
import { StarterProject, isStarterProject } from '../odo/componentTypeDescription';
1717
import { ComponentWorkspaceFolder } from '../odo/workspace';
18-
import { NewComponentCommandProps } from '../telemetry';
18+
import * as odo3 from '../odo3';
19+
import sendTelemetry, { NewComponentCommandProps } from '../telemetry';
1920
import { Progress } from '../util/progress';
2021
import { selectWorkspaceFolder } from '../util/workspace';
2122
import { VsCommandError, vsCommand } from '../vscommand';
23+
import AddServiceBindingViewLoader, { ServiceBindingFormResponse } from '../webview/add-service-binding/addServiceBindingViewLoader';
24+
import DescribeViewLoader from '../webview/describe/describeViewLoader';
2225
import GitImportLoader from '../webview/git-import/gitImportLoader';
2326
import LogViewLoader from '../webview/log/LogViewLoader';
2427
import OpenShiftItem, { clusterRequired } from './openshiftItem';
25-
import DescribeViewLoader from '../webview/describe/describeViewLoader';
2628

2729
function createCancelledResult(stepName: string): any {
2830
const cancelledResult: any = new String('');
@@ -203,6 +205,76 @@ export class Component extends OpenShiftItem {
203205
return Component.dev(component, 'podman');
204206
}
205207

208+
@vsCommand('openshift.component.binding.add')
209+
static async addBinding(component: ComponentWorkspaceFolder) {
210+
const odo: odo3.Odo3 = odo3.newInstance();
211+
212+
const services = await Progress.execFunctionWithProgress('Looking for bindable services', (progress) => {
213+
return odo.getBindableServices();
214+
});
215+
216+
if (!services || services.length === 0) {
217+
void window.showErrorMessage('No bindable services are available', 'Open Service Catalog in OpenShift Console')
218+
.then((result) => {
219+
if (result === 'Open Service Catalog in OpenShift Console') {
220+
void commands.executeCommand('openshift.open.operatorBackedServiceCatalog')
221+
}
222+
});
223+
return;
224+
}
225+
226+
void sendTelemetry('startAddBindingWizard');
227+
228+
let formResponse: ServiceBindingFormResponse = undefined;
229+
try {
230+
formResponse = await new Promise<ServiceBindingFormResponse>(
231+
(resolve, reject) => {
232+
void AddServiceBindingViewLoader.loadView(
233+
component.contextPath,
234+
services.map(
235+
(service) => `${service.metadata.namespace}/${service.metadata.name}`,
236+
),
237+
(panel) => {
238+
panel.onDidDispose((_e) => {
239+
reject(new Error('The \'Add Service Binding\' wizard was closed'));
240+
});
241+
return async (eventData) => {
242+
if (eventData.action === 'addServiceBinding') {
243+
resolve(eventData.params);
244+
await panel.dispose();
245+
}
246+
};
247+
},
248+
).then(view => {
249+
if (!view) {
250+
// the view was already created
251+
reject();
252+
}
253+
});
254+
},
255+
);
256+
} catch (e) {
257+
// The form was closed without submitting,
258+
// or the form already exists for this component.
259+
// stop the command.
260+
return;
261+
}
262+
263+
const selectedServiceObject = services.filter(
264+
(service) =>
265+
`${service.metadata.namespace}/${service.metadata.name}` === formResponse.selectedService,
266+
)[0];
267+
268+
void sendTelemetry('finishAddBindingWizard');
269+
270+
await odo.addBinding(
271+
component.contextPath,
272+
selectedServiceObject.metadata.namespace,
273+
selectedServiceObject.metadata.name,
274+
formResponse.bindingName,
275+
);
276+
}
277+
206278
@vsCommand('openshift.component.dev')
207279
@clusterRequired()
208280
static async dev(component: ComponentWorkspaceFolder, runOn?: undefined | 'podman') {

0 commit comments

Comments
 (0)