Skip to content

Add endpoint URL to ingress rule table; retrieve hosts on demand #215

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions electron/app/js/ipcRendererPreload.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ contextBridge.exposeInMainWorld(
'undeploy-verrazzano-components',
'get-verrazzano-component-names',
'get-verrazzano-secret-names',
'get-verrazzano-host-names',
'get-verrazzano-cluster-names',
'get-verrazzano-deployment-names-all-namespaces',
'verify-verrazzano-components-exist',
Expand Down
56 changes: 37 additions & 19 deletions electron/app/js/kubectlUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1090,26 +1090,44 @@ async function getVerrazzanoApplicationHostnames(kubectlExe, applicationName, ap
};

return new Promise(resolve => {
executeFileCommand(kubectlExe, args, env).then(gatewayJson => {
const gateway = JSON.parse(gatewayJson);
const serversArray = gateway.spec?.servers;
if (Array.isArray(serversArray) && serversArray.length > 0) {
const hostsArray = serversArray[0].hosts;
if (Array.isArray(hostsArray) && hostsArray.length > 0) {
results.hostnames = hostsArray;
}
}
if (results.hostnames) {
results.reason = i18n.t('kubectl-get-vz-app-hostnames-not-found',
{ appName: applicationName, appNamespace: applicationNamespace });
// the external address will help to determine the generated application host name.
// this address is returned as the generated host name if no matching DNS name is found.
getVerrazzanoIngressExternalAddress(kubectlExe, options).then(externalAddressResults => {
if(!externalAddressResults.isSuccess) {
resolve(externalAddressResults);
return;
}
resolve(results);
}).catch(err => {
results.isSuccess = false;
results.reason =
i18n.t('kubectl-get-vz-app-hostnames-error-message',
{ appName: applicationName, appNamespace: applicationNamespace, error: getErrorMessage(err) });
resolve(results);
const externalAddress = externalAddressResults.externalAddress;
results.generatedHostname = externalAddress;

executeFileCommand(kubectlExe, args, env).then(gatewayJson => {
const gateway = JSON.parse(gatewayJson);
const serversArray = gateway.spec?.servers;
if (Array.isArray(serversArray) && serversArray.length > 0) {
const hostsArray = serversArray[0].hosts;
if (Array.isArray(hostsArray) && hostsArray.length > 0) {
results.hostnames = hostsArray;

// see if a DNS name contains the external address
for(const hostname of hostsArray) {
if(hostname.includes(externalAddress)) {
results.generatedHostname = hostname;
}
}
}
}
if (!results.hostnames) {
results.reason = i18n.t('kubectl-get-vz-app-hostnames-not-found',
{ appName: applicationName, appNamespace: applicationNamespace });
}
resolve(results);
}).catch(err => {
results.isSuccess = false;
results.reason =
i18n.t('kubectl-get-vz-app-hostnames-error-message',
{ appName: applicationName, appNamespace: applicationNamespace, error: getErrorMessage(err) });
resolve(results);
});
});
});
}
Expand Down
12 changes: 12 additions & 0 deletions electron/app/js/vzUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ async function getSecretNamesByNamespace(kubectlExe, namespace, kubectlOptions)
});
}

async function getHostNames(kubectlExe, applicationName, applicationNamespace, options) {
return new Promise(resolve => {
kubectlUtils.getVerrazzanoApplicationHostnames(kubectlExe, applicationName, applicationNamespace, options).then(result => {
if (!result.isSuccess) {
return resolve(result);
}
resolve(result);
});
});
}

async function getVerrazzanoClusterNames(kubectlExe, kubectlOptions) {
return new Promise(resolve => {
kubectlUtils.getKubernetesObjectsByNamespace(kubectlExe, kubectlOptions, 'VerrazzanoManagedCluster', 'verrazzano-mc').then(result => {
Expand Down Expand Up @@ -116,6 +127,7 @@ module.exports = {
deployProject,
getComponentNamesByNamespace,
getDeploymentNamesFromAllNamespaces,
getHostNames,
getSecretNamesByNamespace,
getVerrazzanoClusterNames,
undeployApplication,
Expand Down
2 changes: 2 additions & 0 deletions electron/app/locales/en/electron.json
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@
"kubectl-verify-vz-components-deployed-error-message": "Unable to find one or more components in namespace {{namespace}}: {{missingComponents}}",
"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}}",
"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}}",
"kubectl-get-vz-app-hostnames-error-message": "Unable to get Verrazzano application host names: {{error}}",
"kubectl-get-vz-app-hostnames-not-found": "Verrazzano application host names not found",

"helm-not-specified-error-message": "Helm executable path was not provided",
"helm-not-exists-error-message": "Helm executable {{filePath}} does not exist",
Expand Down
8 changes: 6 additions & 2 deletions electron/app/locales/en/webui.json
Original file line number Diff line number Diff line change
Expand Up @@ -1643,6 +1643,9 @@
"vz-application-design-choose-clusters-dialog-title": "Choose Verrazzano Clusters for Application Placement",
"vz-application-design-choose-clusters-name-label": "Verrazzano Cluster Names",
"vz-application-design-choose-clusters-name-help": "Select the Verrazzano Clusters to use for application placement.",
"vz-application-design-get-hosts-error-message": "Unable get hosts for the application: {{error}}.",
"vz-application-design-get-hosts-error-title": "Get Hosts for Application Aborted",
"vz-application-design-get-hosts-in-progress": "Getting Hosts for Application",

"vz-application-design-component-ingress-trait-enabled-label": "Enable Ingress Trait",
"vz-application-design-component-ingress-trait-enabled-help": "Enable the Ingress Trait for this component.",
Expand All @@ -1656,9 +1659,10 @@
"vz-application-design-ingress-trait-rules-hosts-label": "Hosts",
"vz-application-design-ingress-trait-rules-first-path-type-label": "First Path Type",
"vz-application-design-ingress-trait-rules-first-path-label": "First Path",
"vz-application-design-ingress-trait-rules-first-path-url-label": "URL",
"vz-application-design-ingress-trait-rules-update-urls-button-label": "Update URLs",
"vz-application-design-ingress-trait-rule-edit-destination-title": "Destination",
"vz-application-design-ingress-trait-rules-destination-host-label": "Destination Host",
"vz-application-design-ingress-trait-rules-destination-port-label": "Destination Port",
"vz-application-design-ingress-trait-rules-destination-label": "Destination",
"vz-application-design-add-rule-tooltip": "Add Ingress Rule",
"vz-application-design-edit-rule-tooltip": "Edit Ingress Rule",
"vz-application-design-delete-rule-tooltip": "Delete Ingress Rule",
Expand Down
8 changes: 7 additions & 1 deletion electron/app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const { startWebLogicRemoteConsoleBackend, getDefaultDirectoryForOpenDialog, set
const { getVerrazzanoReleaseVersions, isVerrazzanoInstalled, installVerrazzanoPlatformOperator,
verifyVerrazzanoPlatformOperatorInstall, installVerrazzano, verifyVerrazzanoInstallStatus } = require('./js/vzInstaller');
const { deployApplication, deployComponents, deployProject, getComponentNamesByNamespace, getSecretNamesByNamespace,
getVerrazzanoClusterNames, getDeploymentNamesFromAllNamespaces, undeployApplication, undeployComponents } = require('./js/vzUtils');
getVerrazzanoClusterNames, getDeploymentNamesFromAllNamespaces, undeployApplication, undeployComponents, getHostNames
} = require('./js/vzUtils');

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

// eslint-disable-next-line no-unused-vars
ipcMain.handle('get-verrazzano-host-names', async (event, kubectlExe, applicationName, applicationNamespace, options) => {
return getHostNames(kubectlExe, applicationName, applicationNamespace, options);
});

// eslint-disable-next-line no-unused-vars
ipcMain.handle('get-verrazzano-cluster-names', async (event, kubectlExe, kubectlOptions) => {
return getVerrazzanoClusterNames(kubectlExe, kubectlOptions);
Expand Down
4 changes: 4 additions & 0 deletions webui/src/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,10 @@ h6.wkt-panel-heading {
margin-bottom: -10px;
}

.wkt-header-button-row.wkt-header-with-margin .oj-button {
margin-bottom: 10px;
}

.wkt-header-button-row h6 {
flex: 1 1 auto;
}
Expand Down
10 changes: 7 additions & 3 deletions webui/src/js/models/vz-application-definition.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* @license
* Copyright (c) 2022, Oracle and/or its affiliates.
* Copyright (c) 2022, 2023, Oracle and/or its affiliates.
* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
*/
'use strict';

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

// this is a transient ko observable that is not persisted
this.hosts = ko.observableArray();
this.generatedHost = ko.observable();

this.readFrom = (json) => {
props.createGroup(name, this).readFrom(json);
};
Expand Down
131 changes: 118 additions & 13 deletions webui/src/js/viewModels/vz-application-design-view.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright (c) 2022, Oracle and/or its affiliates.
* Copyright (c) 2022, 2023, Oracle and/or its affiliates.
* Licensed under The Universal Permissive License (UPL), Version 1.0 as shown at https://oss.oracle.com/licenses/upl/
*/
define(['models/wkt-project', 'accUtils', 'utils/common-utilities', 'knockout', 'utils/i18n', 'ojs/ojbufferingdataprovider',
Expand Down Expand Up @@ -432,23 +432,23 @@ function (project, accUtils, utils, ko, i18n, BufferingDataProvider, ArrayDataPr
this.ingressTraitRulesColumnData = [
{
'headerText': this.labelMapper('ingress-trait-rules-hosts-label'),
'sortable': 'disable',
},
{
'headerText': this.labelMapper('ingress-trait-rules-first-path-type-label'),
'sortable': 'disable',
'resizable': 'enabled',
'sortable': 'disable'
},
{
'headerText': this.labelMapper('ingress-trait-rules-first-path-label'),
'sortable': 'disable',
'resizable': 'enabled',
'sortable': 'disable'
},
{
'headerText': this.labelMapper('ingress-trait-rules-destination-host-label'),
'headerText': this.labelMapper('ingress-trait-rules-first-path-url-label'),
'resizable': 'enabled',
'sortable': 'disable',
'width': '35%'
},
{
'headerText': this.labelMapper('ingress-trait-rules-destination-port-label'),
'sortable': 'disable',
'headerText': this.labelMapper('ingress-trait-rules-destination-label'),
'sortable': 'disable'
},
{
'className': 'wkt-table-delete-cell',
Expand All @@ -468,14 +468,119 @@ function (project, accUtils, utils, ko, i18n, BufferingDataProvider, ArrayDataPr
},
];

this.getFirstPathField = (paths, fieldName) => {
let result;
// display in the first path column, example "/path (regex)"
this.getFirstPathText = (rowData) => {
let result = null;
const paths = rowData.paths;
if (Array.isArray(paths) && paths.length > 0) {
result = paths[0][fieldName];
result = paths[0].path;
if(result && result.length) {
const pathType = paths[0].pathType;
if(pathType && pathType !== 'exact') {
result += ` (${pathType})`;
}
}
}
return result;
};

// display in the destination column, example "host:port"
this.getDestinationText = (rowData) => {
let result = rowData.destinationHost;
if(result) {
const port = rowData.destinationPort;
if(port != null) {
result += `:${port}`;
}
}
return result;
};

function getRuleHost(rowData) {
const ruleHostsText = rowData.hosts;
if(ruleHostsText) {
const ruleHosts = ruleHostsText.split(',').map(host => host.trim());
if(ruleHosts.length) {
return ruleHosts[0];
}
}
return null;
}

this.computedUrl = (rowData) => {
return ko.computed(() => {
let urlHost = '<host>';
const generatedHost = project.vzApplication.generatedHost();
if(generatedHost && generatedHost.length) {
urlHost = generatedHost;
}

const ruleHost = getRuleHost(rowData);
if(ruleHost) {
urlHost = ruleHost;
}

let result = 'https://' + urlHost;

let urlPath = '<path>';
const paths = rowData.paths;
if(paths && paths.length) {
urlPath = paths[0].path;
if(urlPath && urlPath.length) {
result += urlPath;
}
}

return result;
});
};

// resolves to true if the row data can make a clickable link
this.computedCanLink = (rowData) => {
return ko.computed(() => {
const appHosts = project.vzApplication.hosts();
if(!appHosts.length) {
return false;
}

const ruleHost = getRuleHost(rowData);
if(ruleHost && !appHosts.includes(ruleHost)) {
return false;
}

const paths = rowData.paths;
if(!paths || !paths.length) {
return false;
}

return paths[0].pathType !== 'regex';
});
};

this.updateUrls = async() => {
const busyDialogMessage = this.labelMapper('get-hosts-in-progress');
dialogHelper.openBusyDialog(busyDialogMessage, 'bar', 1 / 2.0);

const kubectlExe = this.project.kubectl.executableFilePath.value;
const kubectlOptions = k8sHelper.getKubectlOptions();
const applicationName = project.vzApplication.applicationName.value;
const applicationNamespace = project.k8sDomain.kubernetesNamespace.value;
const hostsResult = await window.api.ipc.invoke('get-verrazzano-host-names', kubectlExe, applicationName,
applicationNamespace, kubectlOptions);

dialogHelper.closeBusyDialog();

if (!hostsResult.isSuccess) {
const errTitle = 'vz-application-design-get-hosts-error-title';
const errMessage = this.labelMapper('get-hosts-error-message', { error: hostsResult.reason });
await window.api.ipc.invoke('show-error-message', errTitle, errMessage);
return;
}

project.vzApplication.hosts(hostsResult.hostnames);
project.vzApplication.generatedHost(hostsResult.generatedHostname);
};

this.componentsIngressTraitRulesDataProvider = (component) => {
const key = component.name;
let provider = this.componentIngressTraitRulesDataProviders[key];
Expand Down
4 changes: 2 additions & 2 deletions webui/src/js/views/choose-kubectl-context-dialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
</div>
<div slot="body">
<div class="oj-panel">
<oj-form-layout max-columns="2" direction="row">
<oj-form-layout max-columns="1" direction="row">
<oj-select-single label-hint="[[labelMapper('name-label')]]"
value="{{selectedKubectlContextName}}"
data="{{availableClusterNamesDP}}"
data="{{availableKubectlContextNamesDP}}"
help.instruction="[[labelMapper('name-help')]]">
</oj-select-single>
</oj-form-layout>
Expand Down
Loading