Skip to content

Commit 50dd192

Browse files
authored
Add endpoint URL to ingress rule table; retrieve hosts on demand (#215)
* Use kubernetes context names data provider to populate chooser * Add endpoint URL to ingress rule table; retrieve hosts on demand * Combine and resize columns in the ingress rules table * Determine the generated host name based on the external address
1 parent 687d2a2 commit 50dd192

File tree

11 files changed

+217
-52
lines changed

11 files changed

+217
-52
lines changed

electron/app/js/ipcRendererPreload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ contextBridge.exposeInMainWorld(
236236
'undeploy-verrazzano-components',
237237
'get-verrazzano-component-names',
238238
'get-verrazzano-secret-names',
239+
'get-verrazzano-host-names',
239240
'get-verrazzano-cluster-names',
240241
'get-verrazzano-deployment-names-all-namespaces',
241242
'verify-verrazzano-components-exist',

electron/app/js/kubectlUtils.js

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,26 +1090,44 @@ async function getVerrazzanoApplicationHostnames(kubectlExe, applicationName, ap
10901090
};
10911091

10921092
return new Promise(resolve => {
1093-
executeFileCommand(kubectlExe, args, env).then(gatewayJson => {
1094-
const gateway = JSON.parse(gatewayJson);
1095-
const serversArray = gateway.spec?.servers;
1096-
if (Array.isArray(serversArray) && serversArray.length > 0) {
1097-
const hostsArray = serversArray[0].hosts;
1098-
if (Array.isArray(hostsArray) && hostsArray.length > 0) {
1099-
results.hostnames = hostsArray;
1100-
}
1101-
}
1102-
if (results.hostnames) {
1103-
results.reason = i18n.t('kubectl-get-vz-app-hostnames-not-found',
1104-
{ appName: applicationName, appNamespace: applicationNamespace });
1093+
// the external address will help to determine the generated application host name.
1094+
// this address is returned as the generated host name if no matching DNS name is found.
1095+
getVerrazzanoIngressExternalAddress(kubectlExe, options).then(externalAddressResults => {
1096+
if(!externalAddressResults.isSuccess) {
1097+
resolve(externalAddressResults);
1098+
return;
11051099
}
1106-
resolve(results);
1107-
}).catch(err => {
1108-
results.isSuccess = false;
1109-
results.reason =
1110-
i18n.t('kubectl-get-vz-app-hostnames-error-message',
1111-
{ appName: applicationName, appNamespace: applicationNamespace, error: getErrorMessage(err) });
1112-
resolve(results);
1100+
const externalAddress = externalAddressResults.externalAddress;
1101+
results.generatedHostname = externalAddress;
1102+
1103+
executeFileCommand(kubectlExe, args, env).then(gatewayJson => {
1104+
const gateway = JSON.parse(gatewayJson);
1105+
const serversArray = gateway.spec?.servers;
1106+
if (Array.isArray(serversArray) && serversArray.length > 0) {
1107+
const hostsArray = serversArray[0].hosts;
1108+
if (Array.isArray(hostsArray) && hostsArray.length > 0) {
1109+
results.hostnames = hostsArray;
1110+
1111+
// see if a DNS name contains the external address
1112+
for(const hostname of hostsArray) {
1113+
if(hostname.includes(externalAddress)) {
1114+
results.generatedHostname = hostname;
1115+
}
1116+
}
1117+
}
1118+
}
1119+
if (!results.hostnames) {
1120+
results.reason = i18n.t('kubectl-get-vz-app-hostnames-not-found',
1121+
{ appName: applicationName, appNamespace: applicationNamespace });
1122+
}
1123+
resolve(results);
1124+
}).catch(err => {
1125+
results.isSuccess = false;
1126+
results.reason =
1127+
i18n.t('kubectl-get-vz-app-hostnames-error-message',
1128+
{ appName: applicationName, appNamespace: applicationNamespace, error: getErrorMessage(err) });
1129+
resolve(results);
1130+
});
11131131
});
11141132
});
11151133
}

electron/app/js/vzUtils.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ async function getSecretNamesByNamespace(kubectlExe, namespace, kubectlOptions)
8282
});
8383
}
8484

85+
async function getHostNames(kubectlExe, applicationName, applicationNamespace, options) {
86+
return new Promise(resolve => {
87+
kubectlUtils.getVerrazzanoApplicationHostnames(kubectlExe, applicationName, applicationNamespace, options).then(result => {
88+
if (!result.isSuccess) {
89+
return resolve(result);
90+
}
91+
resolve(result);
92+
});
93+
});
94+
}
95+
8596
async function getVerrazzanoClusterNames(kubectlExe, kubectlOptions) {
8697
return new Promise(resolve => {
8798
kubectlUtils.getKubernetesObjectsByNamespace(kubectlExe, kubectlOptions, 'VerrazzanoManagedCluster', 'verrazzano-mc').then(result => {
@@ -116,6 +127,7 @@ module.exports = {
116127
deployProject,
117128
getComponentNamesByNamespace,
118129
getDeploymentNamesFromAllNamespaces,
130+
getHostNames,
119131
getSecretNamesByNamespace,
120132
getVerrazzanoClusterNames,
121133
undeployApplication,

electron/app/locales/en/electron.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,8 @@
328328
"kubectl-verify-vz-components-deployed-error-message": "Unable to find one or more components in namespace {{namespace}}: {{missingComponents}}",
329329
"kubectl-get-vz-ingress-external-address-not-found": "Unable to find the External IP Address in the Istio Gateway service {{gatewayService}} in Kubernetes namespace {{gatewayNamespace}}",
330330
"kubectl-get-vz-ingress-external-address-error-message": "Failed to find the External IP Address in the Istio Gateway service {{gatewayService}} in Kubernetes namespace {{gatewayNamespace}}: {{error}}",
331+
"kubectl-get-vz-app-hostnames-error-message": "Unable to get Verrazzano application host names: {{error}}",
332+
"kubectl-get-vz-app-hostnames-not-found": "Verrazzano application host names not found",
331333

332334
"helm-not-specified-error-message": "Helm executable path was not provided",
333335
"helm-not-exists-error-message": "Helm executable {{filePath}} does not exist",

electron/app/locales/en/webui.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,6 +1643,9 @@
16431643
"vz-application-design-choose-clusters-dialog-title": "Choose Verrazzano Clusters for Application Placement",
16441644
"vz-application-design-choose-clusters-name-label": "Verrazzano Cluster Names",
16451645
"vz-application-design-choose-clusters-name-help": "Select the Verrazzano Clusters to use for application placement.",
1646+
"vz-application-design-get-hosts-error-message": "Unable get hosts for the application: {{error}}.",
1647+
"vz-application-design-get-hosts-error-title": "Get Hosts for Application Aborted",
1648+
"vz-application-design-get-hosts-in-progress": "Getting Hosts for Application",
16461649

16471650
"vz-application-design-component-ingress-trait-enabled-label": "Enable Ingress Trait",
16481651
"vz-application-design-component-ingress-trait-enabled-help": "Enable the Ingress Trait for this component.",
@@ -1656,9 +1659,10 @@
16561659
"vz-application-design-ingress-trait-rules-hosts-label": "Hosts",
16571660
"vz-application-design-ingress-trait-rules-first-path-type-label": "First Path Type",
16581661
"vz-application-design-ingress-trait-rules-first-path-label": "First Path",
1662+
"vz-application-design-ingress-trait-rules-first-path-url-label": "URL",
1663+
"vz-application-design-ingress-trait-rules-update-urls-button-label": "Update URLs",
16591664
"vz-application-design-ingress-trait-rule-edit-destination-title": "Destination",
1660-
"vz-application-design-ingress-trait-rules-destination-host-label": "Destination Host",
1661-
"vz-application-design-ingress-trait-rules-destination-port-label": "Destination Port",
1665+
"vz-application-design-ingress-trait-rules-destination-label": "Destination",
16621666
"vz-application-design-add-rule-tooltip": "Add Ingress Rule",
16631667
"vz-application-design-edit-rule-tooltip": "Edit Ingress Rule",
16641668
"vz-application-design-delete-rule-tooltip": "Delete Ingress Rule",

electron/app/main.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ const { startWebLogicRemoteConsoleBackend, getDefaultDirectoryForOpenDialog, set
4040
const { getVerrazzanoReleaseVersions, isVerrazzanoInstalled, installVerrazzanoPlatformOperator,
4141
verifyVerrazzanoPlatformOperatorInstall, installVerrazzano, verifyVerrazzanoInstallStatus } = require('./js/vzInstaller');
4242
const { deployApplication, deployComponents, deployProject, getComponentNamesByNamespace, getSecretNamesByNamespace,
43-
getVerrazzanoClusterNames, getDeploymentNamesFromAllNamespaces, undeployApplication, undeployComponents } = require('./js/vzUtils');
43+
getVerrazzanoClusterNames, getDeploymentNamesFromAllNamespaces, undeployApplication, undeployComponents, getHostNames
44+
} = require('./js/vzUtils');
4445

4546
const { getHttpsProxyUrl, getBypassProxyHosts } = require('./js/userSettings');
4647
const { sendToWindow } = require('./js/windowUtils');
@@ -1081,6 +1082,11 @@ class Main {
10811082
return getSecretNamesByNamespace(kubectlExe, namespace, kubectlOptions);
10821083
});
10831084

1085+
// eslint-disable-next-line no-unused-vars
1086+
ipcMain.handle('get-verrazzano-host-names', async (event, kubectlExe, applicationName, applicationNamespace, options) => {
1087+
return getHostNames(kubectlExe, applicationName, applicationNamespace, options);
1088+
});
1089+
10841090
// eslint-disable-next-line no-unused-vars
10851091
ipcMain.handle('get-verrazzano-cluster-names', async (event, kubectlExe, kubectlOptions) => {
10861092
return getVerrazzanoClusterNames(kubectlExe, kubectlOptions);

webui/src/css/app.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,10 @@ h6.wkt-panel-heading {
583583
margin-bottom: -10px;
584584
}
585585

586+
.wkt-header-button-row.wkt-header-with-margin .oj-button {
587+
margin-bottom: 10px;
588+
}
589+
586590
.wkt-header-button-row h6 {
587591
flex: 1 1 auto;
588592
}

webui/src/js/models/vz-application-definition.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
22
* @license
3-
* Copyright (c) 2022, Oracle and/or its affiliates.
3+
* Copyright (c) 2022, 2023, Oracle and/or its affiliates.
44
* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
55
*/
66
'use strict';
77

8-
define(['utils/observable-properties', 'utils/validation-helper', 'utils/wkt-logger'],
9-
function(props, validationHelper) {
8+
define(['utils/observable-properties', 'utils/validation-helper', 'knockout', 'utils/wkt-logger'],
9+
function(props, validationHelper, ko) {
1010
return function (name, k8sDomain) {
1111
function VerrazzanoApplicationModel() {
1212
let componentChanged = false;
@@ -30,6 +30,10 @@ define(['utils/observable-properties', 'utils/validation-helper', 'utils/wkt-log
3030
'loggingTraitEnabled', 'loggingTraitImage', 'loggingTraitConfiguration' ];
3131
this.components = props.createListProperty(this.componentKeys).persistByKey('name');
3232

33+
// this is a transient ko observable that is not persisted
34+
this.hosts = ko.observableArray();
35+
this.generatedHost = ko.observable();
36+
3337
this.readFrom = (json) => {
3438
props.createGroup(name, this).readFrom(json);
3539
};

webui/src/js/viewModels/vz-application-design-view.js

Lines changed: 118 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright (c) 2022, Oracle and/or its affiliates.
3+
* Copyright (c) 2022, 2023, Oracle and/or its affiliates.
44
* Licensed under The Universal Permissive License (UPL), Version 1.0 as shown at https://oss.oracle.com/licenses/upl/
55
*/
66
define(['models/wkt-project', 'accUtils', 'utils/common-utilities', 'knockout', 'utils/i18n', 'ojs/ojbufferingdataprovider',
@@ -432,23 +432,23 @@ function (project, accUtils, utils, ko, i18n, BufferingDataProvider, ArrayDataPr
432432
this.ingressTraitRulesColumnData = [
433433
{
434434
'headerText': this.labelMapper('ingress-trait-rules-hosts-label'),
435-
'sortable': 'disable',
436-
},
437-
{
438-
'headerText': this.labelMapper('ingress-trait-rules-first-path-type-label'),
439-
'sortable': 'disable',
435+
'resizable': 'enabled',
436+
'sortable': 'disable'
440437
},
441438
{
442439
'headerText': this.labelMapper('ingress-trait-rules-first-path-label'),
443-
'sortable': 'disable',
440+
'resizable': 'enabled',
441+
'sortable': 'disable'
444442
},
445443
{
446-
'headerText': this.labelMapper('ingress-trait-rules-destination-host-label'),
444+
'headerText': this.labelMapper('ingress-trait-rules-first-path-url-label'),
445+
'resizable': 'enabled',
447446
'sortable': 'disable',
447+
'width': '35%'
448448
},
449449
{
450-
'headerText': this.labelMapper('ingress-trait-rules-destination-port-label'),
451-
'sortable': 'disable',
450+
'headerText': this.labelMapper('ingress-trait-rules-destination-label'),
451+
'sortable': 'disable'
452452
},
453453
{
454454
'className': 'wkt-table-delete-cell',
@@ -468,14 +468,119 @@ function (project, accUtils, utils, ko, i18n, BufferingDataProvider, ArrayDataPr
468468
},
469469
];
470470

471-
this.getFirstPathField = (paths, fieldName) => {
472-
let result;
471+
// display in the first path column, example "/path (regex)"
472+
this.getFirstPathText = (rowData) => {
473+
let result = null;
474+
const paths = rowData.paths;
473475
if (Array.isArray(paths) && paths.length > 0) {
474-
result = paths[0][fieldName];
476+
result = paths[0].path;
477+
if(result && result.length) {
478+
const pathType = paths[0].pathType;
479+
if(pathType && pathType !== 'exact') {
480+
result += ` (${pathType})`;
481+
}
482+
}
483+
}
484+
return result;
485+
};
486+
487+
// display in the destination column, example "host:port"
488+
this.getDestinationText = (rowData) => {
489+
let result = rowData.destinationHost;
490+
if(result) {
491+
const port = rowData.destinationPort;
492+
if(port != null) {
493+
result += `:${port}`;
494+
}
475495
}
476496
return result;
477497
};
478498

499+
function getRuleHost(rowData) {
500+
const ruleHostsText = rowData.hosts;
501+
if(ruleHostsText) {
502+
const ruleHosts = ruleHostsText.split(',').map(host => host.trim());
503+
if(ruleHosts.length) {
504+
return ruleHosts[0];
505+
}
506+
}
507+
return null;
508+
}
509+
510+
this.computedUrl = (rowData) => {
511+
return ko.computed(() => {
512+
let urlHost = '<host>';
513+
const generatedHost = project.vzApplication.generatedHost();
514+
if(generatedHost && generatedHost.length) {
515+
urlHost = generatedHost;
516+
}
517+
518+
const ruleHost = getRuleHost(rowData);
519+
if(ruleHost) {
520+
urlHost = ruleHost;
521+
}
522+
523+
let result = 'https://' + urlHost;
524+
525+
let urlPath = '<path>';
526+
const paths = rowData.paths;
527+
if(paths && paths.length) {
528+
urlPath = paths[0].path;
529+
if(urlPath && urlPath.length) {
530+
result += urlPath;
531+
}
532+
}
533+
534+
return result;
535+
});
536+
};
537+
538+
// resolves to true if the row data can make a clickable link
539+
this.computedCanLink = (rowData) => {
540+
return ko.computed(() => {
541+
const appHosts = project.vzApplication.hosts();
542+
if(!appHosts.length) {
543+
return false;
544+
}
545+
546+
const ruleHost = getRuleHost(rowData);
547+
if(ruleHost && !appHosts.includes(ruleHost)) {
548+
return false;
549+
}
550+
551+
const paths = rowData.paths;
552+
if(!paths || !paths.length) {
553+
return false;
554+
}
555+
556+
return paths[0].pathType !== 'regex';
557+
});
558+
};
559+
560+
this.updateUrls = async() => {
561+
const busyDialogMessage = this.labelMapper('get-hosts-in-progress');
562+
dialogHelper.openBusyDialog(busyDialogMessage, 'bar', 1 / 2.0);
563+
564+
const kubectlExe = this.project.kubectl.executableFilePath.value;
565+
const kubectlOptions = k8sHelper.getKubectlOptions();
566+
const applicationName = project.vzApplication.applicationName.value;
567+
const applicationNamespace = project.k8sDomain.kubernetesNamespace.value;
568+
const hostsResult = await window.api.ipc.invoke('get-verrazzano-host-names', kubectlExe, applicationName,
569+
applicationNamespace, kubectlOptions);
570+
571+
dialogHelper.closeBusyDialog();
572+
573+
if (!hostsResult.isSuccess) {
574+
const errTitle = 'vz-application-design-get-hosts-error-title';
575+
const errMessage = this.labelMapper('get-hosts-error-message', { error: hostsResult.reason });
576+
await window.api.ipc.invoke('show-error-message', errTitle, errMessage);
577+
return;
578+
}
579+
580+
project.vzApplication.hosts(hostsResult.hostnames);
581+
project.vzApplication.generatedHost(hostsResult.generatedHostname);
582+
};
583+
479584
this.componentsIngressTraitRulesDataProvider = (component) => {
480585
const key = component.name;
481586
let provider = this.componentIngressTraitRulesDataProviders[key];

webui/src/js/views/choose-kubectl-context-dialog.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
</div>
1111
<div slot="body">
1212
<div class="oj-panel">
13-
<oj-form-layout max-columns="2" direction="row">
13+
<oj-form-layout max-columns="1" direction="row">
1414
<oj-select-single label-hint="[[labelMapper('name-label')]]"
1515
value="{{selectedKubectlContextName}}"
16-
data="{{availableClusterNamesDP}}"
16+
data="{{availableKubectlContextNamesDP}}"
1717
help.instruction="[[labelMapper('name-help')]]">
1818
</oj-select-single>
1919
</oj-form-layout>

0 commit comments

Comments
 (0)