Skip to content

Commit 619f49c

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 868509c commit 619f49c

File tree

16 files changed

+632
-39
lines changed

16 files changed

+632
-39
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ src/webview/devfile-registry
1717
src/webview/welcome
1818
src/webview/git-import
1919
src/webview/helm-chart
20+
src/webview/add-service-binding/webpack.config.js
2021
test/sandbox-registration

build/esbuild.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const webviews = [
1616
'helm-chart',
1717
'log',
1818
'welcome',
19+
'add-service-binding',
1920
];
2021

2122
function kebabToCamel(text) {
2.45 MB
Loading

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,11 @@
455455
"title": "Start Dev",
456456
"category": "OpenShift"
457457
},
458+
{
459+
"command": "openshift.component.binding.add",
460+
"title": "Bind Service",
461+
"category": "OpenShift"
462+
},
458463
{
459464
"command": "openshift.component.exitDevMode",
460465
"title": "Stop Dev",
@@ -1074,6 +1079,10 @@
10741079
{
10751080
"command": "openshift.component.openInBrowser",
10761081
"when": "false"
1082+
},
1083+
{
1084+
"command": "openshift.component.binding.add",
1085+
"when": "false"
10771086
}
10781087
],
10791088
"view/title": [
@@ -1299,6 +1308,11 @@
12991308
"when": "view == openshiftComponentsView && viewItem =~ /openshift\\.component.*\\.dep-nrn.*/ || viewItem =~ /openshift\\.component.*\\.dep-run.*/",
13001309
"group": "c2@1"
13011310
},
1311+
{
1312+
"command": "openshift.component.binding.add",
1313+
"when": "view == openshiftComponentsView && viewItem =~ /openshift\\.component.*\\.dev-nrn.*/",
1314+
"group": "c2@2"
1315+
},
13021316
{
13031317
"command": "openshift.component.showDevTerminal",
13041318
"when": "view == openshiftComponentsView && viewItem =~ /openshift\\.component.*\\.dev-run.*/",

src/odo/command.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,21 @@ export class Command {
311311
new CommandOption('-f'),
312312
]);
313313
}
314+
315+
static addBinding(serviceNamespace: string, serviceName: string, bindingName: string): CommandText {
316+
return new CommandText('odo add binding',
317+
undefined,
318+
[
319+
new CommandOption('--service-namespace', serviceNamespace, false),
320+
new CommandOption('--service', serviceName, false),
321+
new CommandOption('--name', bindingName, false),
322+
]
323+
)
324+
}
325+
326+
static getBindableServices(): CommandText {
327+
return new CommandText('odo list service',
328+
undefined,
329+
[new CommandOption('-o json')]);
330+
}
314331
}

src/odo3.ts

Lines changed: 46 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,8 @@ export interface Odo3 {
2020
setNamespace(newNamespace: string): Promise<void>;
2121

2222
describeComponent(contextPath: string): Promise<ComponentDescription | undefined>;
23+
addBinding(contextPath: string, serviceName: string, serviceNamespace: string, bindingName: string): Promise<void>;
24+
getBindableServices(): Promise<KubernetesObject[]>;
2325
}
2426

2527
export class Odo3Impl implements Odo3 {
@@ -69,6 +71,49 @@ export class Odo3Impl implements Odo3 {
6971
// ignore and return undefined
7072
}
7173
}
74+
75+
async addBinding(contextPath: string, serviceNamespace: string, serviceName: string, bindingName: string) {
76+
const myCommand = Command.addBinding(serviceNamespace, serviceName, bindingName);
77+
await CliChannel.getInstance().executeTool(
78+
myCommand,
79+
{cwd: contextPath},
80+
true
81+
);
82+
}
83+
84+
async getBindableServices(): Promise<KubernetesObject[]> {
85+
const data: CliExitData = await CliChannel.getInstance().executeTool(
86+
Command.getBindableServices()
87+
);
88+
let responseObj;
89+
try {
90+
responseObj = JSON.parse(data.stdout);
91+
} catch {
92+
throw new Error(JSON.parse(data.stderr).message);
93+
}
94+
if (!responseObj.bindableServices) {
95+
return [];
96+
}
97+
return (responseObj.bindableServices as BindableService[]) //
98+
.map(obj => {
99+
return {
100+
kind: obj.kind,
101+
apiVersion: obj.apiVersion,
102+
metadata: {
103+
namespace: obj.namespace,
104+
name: obj.name,
105+
}
106+
} as KubernetesObject;
107+
});
108+
}
109+
}
110+
111+
interface BindableService {
112+
name: string;
113+
namespace: string;
114+
kind: string;
115+
apiVersion: string;
116+
service: string;
72117
}
73118

74119
export function newInstance(): Odo3 {

src/openshift/component.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88
import { ChildProcess, SpawnOptions } from 'child_process';
99
import * as fs from 'fs/promises';
1010
import * as path from 'path';
11+
import { basename } from 'path';
1112
import { DebugConfiguration, DebugSession, Disposable, EventEmitter, ProgressLocation, Terminal, Uri, commands, debug, extensions, window, workspace } from 'vscode';
1213
import * as YAML from 'yaml';
1314
import { CliChannel } from '../cli';
1415
import { Command } from '../odo/command';
1516
import { ComponentTypeAdapter, ComponentTypeDescription, DevfileComponentType, ascDevfileFirst, isDevfileComponent } from '../odo/componentType';
1617
import { StarterProject, isStarterProject } from '../odo/componentTypeDescription';
1718
import { ComponentWorkspaceFolder } from '../odo/workspace';
19+
import * as odo3 from '../odo3';
1820
import { NewComponentCommandProps } from '../telemetry';
1921
import { Progress } from '../util/progress';
2022
import { selectWorkspaceFolder } from '../util/workspace';
2123
import { VsCommandError, vsCommand } from '../vscommand';
24+
import AddServiceBindingViewLoader, { ServiceBindingFormResponse } from '../webview/add-service-binding/addServiceBindingViewLoader';
2225
import GitImportLoader from '../webview/git-import/gitImportLoader';
2326
import LogViewLoader from '../webview/log/LogViewLoader';
2427
import OpenShiftItem from './openshiftItem';
@@ -202,6 +205,56 @@ export class Component extends OpenShiftItem {
202205
return Component.dev(component, 'podman');
203206
}
204207

208+
@vsCommand('openshift.component.binding.add')
209+
static async addBinding(component: ComponentWorkspaceFolder) {
210+
const odo: odo3.Odo3 = odo3.newInstance();
211+
212+
const services = await odo.getBindableServices();
213+
if (!services || services.length === 0) {
214+
throw new Error('No bindable services are available');
215+
}
216+
217+
let formResponse: ServiceBindingFormResponse = undefined;
218+
try {
219+
formResponse = await new Promise<ServiceBindingFormResponse>(
220+
(resolve, reject) => {
221+
void AddServiceBindingViewLoader.loadView(
222+
basename(component.contextPath),
223+
services.map(
224+
(service) => `${service.metadata.namespace}/${service.metadata.name}`,
225+
),
226+
(panel) => {
227+
panel.onDidDispose((_e) => {
228+
reject(new Error('The \'Add Service Binding\' wizard was closed'));
229+
});
230+
return async (eventData) => {
231+
if (eventData.action === 'addServiceBinding') {
232+
resolve(eventData.params);
233+
await panel.dispose();
234+
}
235+
};
236+
},
237+
);
238+
},
239+
);
240+
} catch (e) {
241+
// The form was closed without submitting, stop the command
242+
return;
243+
}
244+
245+
const selectedServiceObject = services.filter(
246+
(service) =>
247+
`${service.metadata.namespace}/${service.metadata.name}` === formResponse.selectedService,
248+
)[0];
249+
250+
await odo.addBinding(
251+
component.contextPath,
252+
selectedServiceObject.metadata.namespace,
253+
selectedServiceObject.metadata.name,
254+
formResponse.bindingName,
255+
);
256+
}
257+
205258
@vsCommand('openshift.component.dev')
206259
//@clusterRequired() check for user is logged in should be implemented from scratch
207260
static async dev(component: ComponentWorkspaceFolder, runOn?: undefined | 'podman') {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*-----------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE file in the project root for license information.
4+
*-----------------------------------------------------------------------------------------------*/
5+
import * as path from 'path';
6+
import * as vscode from 'vscode';
7+
import { ExtensionID } from '../../util/constants';
8+
import { loadWebviewHtml } from '../common-ext/utils';
9+
10+
export interface ServiceBindingFormResponse {
11+
selectedService: string;
12+
bindingName: string;
13+
}
14+
15+
export default class AddServiceBindingViewLoader {
16+
private static get extensionPath(): string {
17+
return vscode.extensions.getExtension(ExtensionID).extensionPath;
18+
}
19+
20+
/**
21+
* Opens and returns a webview panel with the "Add Service Binding" UI.
22+
*
23+
* @param componentName the name of the component that's being binded to a service
24+
* @param availableServices the list of all bindable services on the cluster
25+
* @param listenerFactory the listener function to recieve and process messages from the webview
26+
* @return the webview as a promise
27+
*/
28+
static async loadView(
29+
componentName: string,
30+
availableServices: string[],
31+
listenerFactory: (panel: vscode.WebviewPanel) => (event) => Promise<void>,
32+
): Promise<vscode.WebviewPanel> {
33+
const localResourceRoot = vscode.Uri.file(
34+
path.join(AddServiceBindingViewLoader.extensionPath, 'out', 'addServiceBindingViewer'),
35+
);
36+
37+
let panel: vscode.WebviewPanel = vscode.window.createWebviewPanel(
38+
'addServiceBindingView',
39+
'Add service binding',
40+
vscode.ViewColumn.One,
41+
{
42+
enableScripts: true,
43+
localResourceRoots: [localResourceRoot],
44+
retainContextWhenHidden: true,
45+
},
46+
);
47+
48+
panel.iconPath = vscode.Uri.file(
49+
path.join(AddServiceBindingViewLoader.extensionPath, 'images/context/cluster-node.png'),
50+
);
51+
panel.webview.html = await loadWebviewHtml(
52+
'addServiceBindingViewer',
53+
panel,
54+
);
55+
panel.webview.onDidReceiveMessage(listenerFactory(panel));
56+
57+
// set theme
58+
void panel.webview.postMessage({
59+
action: 'setTheme',
60+
themeValue: vscode.window.activeColorTheme.kind,
61+
});
62+
const colorThemeDisposable = vscode.window.onDidChangeActiveColorTheme(async function (colorTheme: vscode.ColorTheme) {
63+
await panel.webview.postMessage({ action: 'setTheme', themeValue: colorTheme.kind });
64+
});
65+
66+
panel.onDidDispose(() => {
67+
colorThemeDisposable.dispose();
68+
panel = undefined;
69+
});
70+
71+
// send initial data to panel
72+
void panel.webview.postMessage({
73+
action: 'setAvailableServices',
74+
availableServices,
75+
});
76+
void panel.webview.postMessage({
77+
action: 'setComponentName',
78+
componentName,
79+
});
80+
81+
return Promise.resolve(panel);
82+
}
83+
}

0 commit comments

Comments
 (0)