diff --git a/electron/app/js/ipcRendererPreload.js b/electron/app/js/ipcRendererPreload.js index 8e757725e..c4666a6f4 100644 --- a/electron/app/js/ipcRendererPreload.js +++ b/electron/app/js/ipcRendererPreload.js @@ -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', diff --git a/electron/app/js/kubectlUtils.js b/electron/app/js/kubectlUtils.js index d93c221ba..c44aecc28 100644 --- a/electron/app/js/kubectlUtils.js +++ b/electron/app/js/kubectlUtils.js @@ -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); + }); }); }); } diff --git a/electron/app/js/vzUtils.js b/electron/app/js/vzUtils.js index 8455f792a..3a9f0f16c 100644 --- a/electron/app/js/vzUtils.js +++ b/electron/app/js/vzUtils.js @@ -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 => { @@ -116,6 +127,7 @@ module.exports = { deployProject, getComponentNamesByNamespace, getDeploymentNamesFromAllNamespaces, + getHostNames, getSecretNamesByNamespace, getVerrazzanoClusterNames, undeployApplication, diff --git a/electron/app/locales/en/electron.json b/electron/app/locales/en/electron.json index 6141c9e73..8a03819c7 100644 --- a/electron/app/locales/en/electron.json +++ b/electron/app/locales/en/electron.json @@ -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", diff --git a/electron/app/locales/en/webui.json b/electron/app/locales/en/webui.json index 5ad87a38e..acb54dca8 100644 --- a/electron/app/locales/en/webui.json +++ b/electron/app/locales/en/webui.json @@ -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.", @@ -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", diff --git a/electron/app/main.js b/electron/app/main.js index a1be39503..e0a544633 100644 --- a/electron/app/main.js +++ b/electron/app/main.js @@ -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'); @@ -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); diff --git a/webui/src/css/app.css b/webui/src/css/app.css index e9ad5a706..ce95ffb97 100644 --- a/webui/src/css/app.css +++ b/webui/src/css/app.css @@ -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; } diff --git a/webui/src/js/models/vz-application-definition.js b/webui/src/js/models/vz-application-definition.js index b2a2a6a6d..032486a34 100644 --- a/webui/src/js/models/vz-application-definition.js +++ b/webui/src/js/models/vz-application-definition.js @@ -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; @@ -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); }; diff --git a/webui/src/js/viewModels/vz-application-design-view.js b/webui/src/js/viewModels/vz-application-design-view.js index 60f6f9878..2112473c8 100644 --- a/webui/src/js/viewModels/vz-application-design-view.js +++ b/webui/src/js/viewModels/vz-application-design-view.js @@ -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', @@ -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', @@ -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 = ''; + 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 = ''; + 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]; diff --git a/webui/src/js/views/choose-kubectl-context-dialog.html b/webui/src/js/views/choose-kubectl-context-dialog.html index 7bd04d796..a306105e7 100644 --- a/webui/src/js/views/choose-kubectl-context-dialog.html +++ b/webui/src/js/views/choose-kubectl-context-dialog.html @@ -10,10 +10,10 @@
- + diff --git a/webui/src/js/views/vz-application-design-view.html b/webui/src/js/views/vz-application-design-view.html index 224b37780..1d4191625 100644 --- a/webui/src/js/views/vz-application-design-view.html +++ b/webui/src/js/views/vz-application-design-view.html @@ -1,5 +1,5 @@
@@ -132,11 +132,16 @@
-
- -
+
+
+ +
+ + + +
- - - - + - - + + + + + + + + + - +