From f020517b60648b566367f8628a8597d2ab5ef090 Mon Sep 17 00:00:00 2001 From: Robert Patrick Date: Mon, 27 Feb 2023 15:59:08 -0600 Subject: [PATCH] refactoring kubectl page for vz multi-cluster connectivity infro --- Jenkinsfile | 14 +- electron/app/js/ipcRendererPreload.js | 1 + electron/app/js/kubectlUtils.js | 101 ++++++- electron/app/js/vzUtils.js | 2 +- electron/app/locales/en/electron.json | 3 + electron/app/locales/en/webui.json | 24 ++ electron/app/main.js | 4 + electron/package-lock.json | 18 +- electron/package.json | 4 +- webui/src/css/app.css | 4 +- webui/src/js/models/kubectl-definition.js | 3 +- webui/src/js/utils/k8s-helper.js | 12 +- webui/src/js/utils/view-helper.js | 3 +- .../js/utils/vz-application-status-checker.js | 7 +- webui/src/js/utils/wkt-actions-base.js | 10 +- .../choose-kubectl-context-dialog.js | 52 ++++ webui/src/js/viewModels/kubectl-page.js | 261 +++++++++++++++--- .../views/choose-kubectl-context-dialog.html | 31 +++ webui/src/js/views/kubectl-page.html | 155 +++++++++-- 19 files changed, 610 insertions(+), 99 deletions(-) create mode 100644 webui/src/js/viewModels/choose-kubectl-context-dialog.js create mode 100644 webui/src/js/views/choose-kubectl-context-dialog.html diff --git a/Jenkinsfile b/Jenkinsfile index 251fc9589..d9bbaef88 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,9 +1,12 @@ /* - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. */ pipeline { agent { label 'ol8' } + options { + timeout(time: 480, unit: 'MINUTES') + } environment { WKTUI_PROXY = "${env.ORACLE_HTTP_PROXY}" ELECTRON_GET_USE_PROXY = "true" @@ -54,6 +57,9 @@ pipeline { parallel { stage('Linux Build') { agent { label 'ol8' } + options { + timeout(time: 300, unit: 'MINUTES') + } environment { linux_node_dir_name = "node-v${node_version}-linux-x64" linux_node_installer = "${linux_node_dir_name}.tar.gz" @@ -194,6 +200,9 @@ pipeline { } stage('MacOS Build') { agent { label('macosx') } + options { + timeout(time: 300, unit: 'MINUTES') + } environment { mac_node_dir_name = "node-v${node_version}-darwin-x64" mac_node_installer = "node-v${node_version}-darwin-x64.tar.gz" @@ -301,6 +310,9 @@ pipeline { } stage('Windows Build') { agent { label 'windows'} + options { + timeout(time: 300, unit: 'MINUTES') + } environment { windows_node_dir_name = "node-v${node_version}-win-x64" windows_node_installer = "node-v${node_version}-win-x64.zip" diff --git a/electron/app/js/ipcRendererPreload.js b/electron/app/js/ipcRendererPreload.js index 75b750c2a..f02f36895 100644 --- a/electron/app/js/ipcRendererPreload.js +++ b/electron/app/js/ipcRendererPreload.js @@ -180,6 +180,7 @@ contextBridge.exposeInMainWorld( 'do-push-image', 'kubectl-get-current-context', 'kubectl-set-current-context', + 'kubectl-get-contexts', 'validate-kubectl-exe', 'kubectl-verify-connection', 'validate-helm-exe', diff --git a/electron/app/js/kubectlUtils.js b/electron/app/js/kubectlUtils.js index 5346514d5..d93c221ba 100644 --- a/electron/app/js/kubectlUtils.js +++ b/electron/app/js/kubectlUtils.js @@ -1,6 +1,6 @@ /** * @license - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. + * Copyright (c) 2021, 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'; @@ -90,6 +90,30 @@ async function setCurrentContext(kubectlExe, context, options) { }); } +async function getContexts(kubectlExe, options) { + const args = [ 'config', 'get-contexts', '--output=name' ]; + const httpsProxyUrl = getHttpsProxyUrl(); + const bypassProxyHosts = getBypassProxyHosts(); + const env = getKubectlEnvironment(options, httpsProxyUrl, bypassProxyHosts); + const results = { + isSuccess: true + }; + + return new Promise(resolve => { + executeFileCommand(kubectlExe, args, env).then(currentContext => { + results['contexts'] = currentContext.trim().split('\n'); + getLogger().debug('getContexts result = %s', results['contexts']); + resolve(results); + }).catch(err => { + // kubectl config get-contexts returns an error if the config file doesn't exist... + results.isSuccess = false; + results.reason = + i18n.t('kubectl-get-contexts-error-message',{ error: getErrorMessage(err) }); + resolve(results); + }); + }); +} + async function getK8sConfigView(kubectlExe, options) { const args = [ 'config', 'view', '-o', 'json']; const httpsProxyUrl = getHttpsProxyUrl(); @@ -1018,6 +1042,78 @@ async function doCreateSecret(kubectlExe, createArgs, env, namespace, secret, re }); } +async function getVerrazzanoIngressExternalAddress(kubectlExe, options) { + const gatewayService = 'istio-ingressgateway'; + const gatewayNamespace = 'istio-system'; + + const args = [ 'get', 'service', gatewayService, '-n', gatewayNamespace, '-o', 'json']; + const httpsProxyUrl = getHttpsProxyUrl(); + const bypassProxyHosts = getBypassProxyHosts(); + const env = getKubectlEnvironment(options, httpsProxyUrl, bypassProxyHosts); + const results = { + isSuccess: true + }; + + return new Promise(resolve => { + executeFileCommand(kubectlExe, args, env).then(serviceJson => { + const service = JSON.parse(serviceJson); + const ingressArray = service.status?.loadBalancer?.ingress; + + if (Array.isArray(ingressArray) && ingressArray.length > 0) { + results.externalAddress = ingressArray[0].ip; + } + if (!results.externalAddress) { + results.reason = i18n.t('kubectl-get-vz-ingress-external-address-not-found', + { gatewayService, gatewayNamespace }); + } + resolve(results); + }).catch(err => { + results.isSuccess = false; + results.reason = + i18n.t('kubectl-get-vz-ingress-external-address-error-message', + { gatewayService, gatewayNamespace, error: getErrorMessage(err) }); + resolve(results); + }); + }); +} + +async function getVerrazzanoApplicationHostnames(kubectlExe, applicationName, applicationNamespace, options) { + const gatewayType = 'gateways.networking.istio.io'; + const appGatewayName = `${applicationNamespace}-${applicationName}-gw`; + + const args = [ 'get', gatewayType, appGatewayName, '-n', applicationNamespace, '-o', 'json']; + const httpsProxyUrl = getHttpsProxyUrl(); + const bypassProxyHosts = getBypassProxyHosts(); + const env = getKubectlEnvironment(options, httpsProxyUrl, bypassProxyHosts); + const results = { + isSuccess: true + }; + + 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 }); + } + 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); + }); + }); +} + function maskPasswordInCommand(err) { // How about other cases? return err.replace(/--docker-password=[^\s]+/, '--docker-password=*****'); @@ -1075,6 +1171,7 @@ module.exports = { createOrReplacePullSecret, createOrReplaceTLSSecret, createServiceAccountIfNotExists, + getContexts, getCurrentContext, getOperatorVersion, isOperatorAlreadyInstalled, @@ -1086,6 +1183,8 @@ module.exports = { getIngresses, getK8sConfigView, getK8sClusterInfo, + getVerrazzanoApplicationHostnames, + getVerrazzanoIngressExternalAddress, getWkoDomainStatus, getApplicationStatus, getOperatorStatus, diff --git a/electron/app/js/vzUtils.js b/electron/app/js/vzUtils.js index a32eb264a..8455f792a 100644 --- a/electron/app/js/vzUtils.js +++ b/electron/app/js/vzUtils.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 v 1.0 as shown at https://oss.oracle.com/licenses/upl. */ const kubectlUtils = require('./kubectlUtils'); diff --git a/electron/app/locales/en/electron.json b/electron/app/locales/en/electron.json index 6e4987157..67279e05e 100644 --- a/electron/app/locales/en/electron.json +++ b/electron/app/locales/en/electron.json @@ -281,6 +281,7 @@ "kubectl-current-context-error-message": "Failed to get the current kubectl cluster context: {{error}}", "kubectl-use-context-error-message": "Failed to change the current kubectl cluster context to {{context}}: {{error}}", + "kubectl-get-contexts-error-message": "Failed to get the available kubectl contexts: {{error}}", "kubectl-not-specified-error-message": "Kubernetes client path was not provided", "kubectl-not-exists-error-message": "Kubernetes client {{filePath}} does not exist", "kubectl-exists-failed-error-message": "Unable to verify Kubernetes client {{filePath}} exists: {{error}}", @@ -322,6 +323,8 @@ "kubectl-get-vz-application-status-error-message": "Unable to get Verrazzano application status: {{error}}", "kubectl-get-operator-version-not-found-error-message": "Failed to find the operator version from its log entries", "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}}", "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 24268f727..f6681d46c 100644 --- a/electron/app/locales/en/webui.json +++ b/electron/app/locales/en/webui.json @@ -39,6 +39,23 @@ "kubectl-form-name": "Kubernetes Client Configuration", "kubectl-title": "Configure the Kubernetes Client", + "kubectl-instructions-heading": "Instructions", + "kubectl-executables-heading": "Client Executables", + "kubectl-wko-connectivity-heading": "Kubernetes Cluster Connectivity Configuration", + "kubectl-vz-connectivity-heading": "Verrazzano Admin Cluster Connectivity Configuration", + "kubectl-vz-managed-clusters-heading": "Verrazzano Managed Clusters Connectivity Configuration", + "kubectl-vz-managed-clusters-table-aria-label": "Verrazzano Managed Clusters Connectivity Table", + "kubectl-vz-managed-clusters-add-row-tooltip": "Add a new Verrazzano Managed Cluster Connectivity row", + "kubectl-vz-managed-clusters-delete-row-tooltip": "Delete the Verrazzano Managed Cluster Connectivity row", + "kubectl-vz-managed-cluster-name-heading": "Verrazzano Managed Cluster Name", + "kubectl-vz-managed-cluster-name-help": "Name of the Verrazzano Managed Cluster.", + "kubectl-vz-managed-cluster-kubeconfig-heading": "Kubernetes Client Config File(s)", + "kubectl-vz-managed-cluster-kubeconfig-help": "The Kubernetes client configuration file(s) to use for connecting to the Verrazzano Managed Cluster.", + "kubectl-vz-managed-cluster-kubecontext-heading": "Kubectl Config Context to Use", + "kubectl-vz-managed-cluster-kubecontext-help": "The Kubernetes client configuration context name to use for connecting to the Verrazzano Managed Cluster.", + "kubectl-vz-managed-cluster-choose-kubecontext-tooltip": "Choose the Kubernetes cluster context for the Verrazzano Managed Cluster from the available contexts in the Kubernetes Client Config File", + "kubectl-vz-managed-cluster-get-context-tooltip": "Get the current Kubernetes cluster context for the Verrazzano Managed Cluster from the Kubernetes Client Config File", + "kubectl-k8s-flavor-label": "Kubernetes Cluster Type", "kubectl-k8s-flavor-help": "The Kubernetes cluster provider type where the Oracle Fusion Middleware domain will be deployed.", "kubectl-exe-file-path-label": "Kubectl Executable to Use", @@ -48,6 +65,8 @@ "kubectl-config-file-help": "The Kubernetes client configuration file(s) to use. Leave this empty to use the default location.", "kubectl-config-file-tooltip": "Choose the Kubernetes Client Config File(s)", "kubectl-config-context-label": "Kubectl Config Context to Use", + "kubectl-config-wko-choose-context-tooltip": "Choose the Kubernetes cluster context from the available contexts in the Kubernetes Client Config File", + "kubectl-config-vz-choose-admin-context-tooltip": "Choose the Kubernetes cluster context for the Verrazzano Admin Cluster from the available contexts in the Kubernetes Client Config File", "kubectl-config-context-tooltip": "Get the current Kubernetes cluster context from the Kubernetes Client Config File", "kubectl-config-context-help": "The Kubernetes config file context name for the cluster to which you want to connect. The default is to use whatever the current context is, if any.", "kubectl-helm-exe-file-path-label": "Helm Executable to Use", @@ -1126,6 +1145,11 @@ "kubectl-get-current-context-error-title": "Kubernetes Client Failed", "kubectl-get-current-context-error-message": "kubectl failed to get the current Kubernetes cluster context: {{error}}.", + "kubectl-get-contexts-error-title": "Kubernetes Client Failed", + "kubectl-get-contexts-error-message": "kubectl failed to get the available Kubernetes cluster contexts: {{error}}.", + "kubectl-choose-context-dialog-title": "Kubernetes Client Choose Cluster Context", + "kubectl-choose-context-name-label": "Cluster Context Name", + "kubectl-choose-context-name-help": "Select the Kubernetes cluster context to use.", "wko-get-install-version-aborted-error-title": "Getting Installed WebLogic Kubernetes Operator Version Aborted", "wko-get-install-version-kubectl-exe-invalid-error-message": "Unable to get the installed WebLogic Kubernetes Operator version because the Kubernetes client executable is invalid: {{error}}.", diff --git a/electron/app/main.js b/electron/app/main.js index a7f49188d..0da5eef22 100644 --- a/electron/app/main.js +++ b/electron/app/main.js @@ -755,6 +755,10 @@ class Main { return kubectlUtils.setCurrentContext(kubectlExe, context, kubectlOptions); }); + ipcMain.handle('kubectl-get-contexts', async (event, kubectlExe, kubectlOptions) => { + return kubectlUtils.getContexts(kubectlExe, kubectlOptions); + }); + ipcMain.handle('validate-kubectl-exe', async (event, kubectlExe) => { return kubectlUtils.validateKubectlExe(kubectlExe); }); diff --git a/electron/package-lock.json b/electron/package-lock.json index b72f97be4..6afb4e339 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "wktui", - "version": "1.4.2", + "version": "1.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wktui", - "version": "1.4.2", + "version": "1.5.0", "license": "UPL-1.0", "dependencies": { "electron-updater": "^5.3.0", @@ -29,7 +29,7 @@ "@electron/notarize": "^1.2.3", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", - "electron": "^22.1.0", + "electron": "^22.2.0", "electron-builder": "^23.6.0", "eslint": "^8.33.0", "mocha": "^10.2.0", @@ -2298,9 +2298,9 @@ } }, "node_modules/electron": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-22.1.0.tgz", - "integrity": "sha512-wz5s4N6V7zyKm4YQmXJImFoxO1Doai32ShYm0FzOLPBMwLMdQBV+REY+j1opRx0KJ9xJEIdjYgcA8OSw6vx3pA==", + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.2.0.tgz", + "integrity": "sha512-puRZSF2vWJ4pz3oetL5Td8LcuivTWz3MoAk/gjImHSN1B/2VJNEQlw1jGdkte+ppid2craOswE2lmCOZ7SwF1g==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -8132,9 +8132,9 @@ } }, "electron": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-22.1.0.tgz", - "integrity": "sha512-wz5s4N6V7zyKm4YQmXJImFoxO1Doai32ShYm0FzOLPBMwLMdQBV+REY+j1opRx0KJ9xJEIdjYgcA8OSw6vx3pA==", + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.2.0.tgz", + "integrity": "sha512-puRZSF2vWJ4pz3oetL5Td8LcuivTWz3MoAk/gjImHSN1B/2VJNEQlw1jGdkte+ppid2craOswE2lmCOZ7SwF1g==", "dev": true, "requires": { "@electron/get": "^2.0.0", diff --git a/electron/package.json b/electron/package.json index 7fef52e9f..089304f11 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,7 +1,7 @@ { "name": "wktui", "productName": "WebLogic Kubernetes Toolkit UI", - "version": "1.4.2", + "version": "1.5.0", "description": "WebLogic Kubernetes Toolkit UI", "copyright": "Copyright (c) 2021, 2023, Oracle and/or its affiliates.", "homepage": "https://github.com/oracle/weblogic-toolkit-ui", @@ -51,7 +51,7 @@ "@electron/notarize": "^1.2.3", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", - "electron": "^22.1.0", + "electron": "^22.2.0", "electron-builder": "^23.6.0", "eslint": "^8.33.0", "mocha": "^10.2.0", diff --git a/webui/src/css/app.css b/webui/src/css/app.css index 51938c18f..e9ad5a706 100644 --- a/webui/src/css/app.css +++ b/webui/src/css/app.css @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. */ .sendOffScreen { @@ -257,7 +257,7 @@ border: 1px solid var(--oj-collection-border-color); } -.wkt-paths-table, .wkt-domain-node-selectors-table { +.wkt-paths-table, .wkt-domain-node-selectors-table, .wkt-vz-managed-clusters-table { border: 1px solid var(--oj-collection-border-color); width: 100%; } diff --git a/webui/src/js/models/kubectl-definition.js b/webui/src/js/models/kubectl-definition.js index d9d4827a8..51a9546a2 100644 --- a/webui/src/js/models/kubectl-definition.js +++ b/webui/src/js/models/kubectl-definition.js @@ -1,6 +1,6 @@ /** * @license - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. + * Copyright (c) 2021, 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'; @@ -22,6 +22,7 @@ define(['utils/observable-properties'], executableFilePath: props.createProperty(window.api.k8s.getKubectlFilePath()), kubeConfigContextToUse: props.createProperty(), helmExecutableFilePath: props.createProperty(window.api.k8s.getHelmFilePath()), + vzManagedClusters: props.createListProperty(['uid', 'name', 'kubeConfig', 'KubeContext']).persistByKey('uid'), readFrom: function(json) { props.createGroup(name, this).readFrom(json); diff --git a/webui/src/js/utils/k8s-helper.js b/webui/src/js/utils/k8s-helper.js index 544ac62b8..77d5401fc 100644 --- a/webui/src/js/utils/k8s-helper.js +++ b/webui/src/js/utils/k8s-helper.js @@ -1,6 +1,6 @@ /** * @license - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. + * Copyright (c) 2021, 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'; @@ -13,11 +13,11 @@ function(WktActionsBase, project, wktConsole, i18n, projectIo, dialogHelper, val super(); } - async startVerifyClusterConnectivity() { - await this.executeAction(this.callVerifyClusterConnectivity); + async startVerifyClusterConnectivity(kubeConfig, kubeContext) { + await this.executeAction(this.callVerifyClusterConnectivity, kubeConfig, kubeContext); } - async callVerifyClusterConnectivity(options) { + async callVerifyClusterConnectivity(kubeConfig, kubeContext, options) { if (!options) { options = {}; } @@ -56,8 +56,8 @@ function(WktActionsBase, project, wktConsole, i18n, projectIo, dialogHelper, val busyDialogMessage = i18n.t('flow-kubectl-use-context-in-progress'); dialogHelper.updateBusyDialog(busyDialogMessage, 2/totalSteps); - const kubectlContext = this.project.kubectl.kubeConfigContextToUse.value; - const kubectlOptions = this.getKubectlOptions(); + const kubectlContext = kubeContext || this.project.kubectl.kubeConfigContextToUse.value; + const kubectlOptions = this.getKubectlOptions(kubeConfig); if (!options.skipKubectlSetContext) { if (! await this.useKubectlContext(kubectlExe, kubectlOptions, kubectlContext, errTitle, errPrefix)) { return Promise.resolve(false); diff --git a/webui/src/js/utils/view-helper.js b/webui/src/js/utils/view-helper.js index 9099a9ee3..6f728418a 100644 --- a/webui/src/js/utils/view-helper.js +++ b/webui/src/js/utils/view-helper.js @@ -1,6 +1,6 @@ /** * @license - * Copyright (c) 2021, Oracle and/or its affiliates. + * Copyright (c) 2021, 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'; @@ -12,6 +12,7 @@ define(['ojs/ojcontext'], // width of button column in WKT tables. // ideally this could be specified as 'auto', but oj-table will not set below 100px. this.BUTTON_COLUMN_WIDTH = '55px'; + this.TEXT_BUTTON_COLUMN_WIDTH = '175px'; const thisHelper = this; diff --git a/webui/src/js/utils/vz-application-status-checker.js b/webui/src/js/utils/vz-application-status-checker.js index 3e96b6de9..71df2edf1 100644 --- a/webui/src/js/utils/vz-application-status-checker.js +++ b/webui/src/js/utils/vz-application-status-checker.js @@ -1,15 +1,15 @@ /** * @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/vz-actions-base', 'models/wkt-project', 'models/wkt-console', 'utils/i18n', 'utils/project-io', 'utils/dialog-helper', 'utils/k8s-domain-resource-generator', 'utils/k8s-domain-configmap-generator', - 'utils/validation-helper', 'utils/helm-helper', 'utils/wkt-logger'], + 'utils/validation-helper', 'utils/wkt-logger', 'utils/helm-helper'], function (VzActionsBase, project, wktConsole, i18n, projectIo, dialogHelper, K8sDomainResourceGenerator, - K8sDomainConfigMapGenerator, validationHelper) { + K8sDomainConfigMapGenerator, validationHelper, wktLogger) { class VerrazzanoApplicationStatusChecker extends VzActionsBase { constructor() { super(); @@ -86,6 +86,7 @@ function (VzActionsBase, project, wktConsole, i18n, projectIo, dialogHelper, K8s const applicationStatusResult = await window.api.ipc.invoke('vz-get-application-status', kubectlExe, this.project.vzApplication.applicationName.value, this.project.k8sDomain.uid.value, this.project.k8sDomain.kubernetesNamespace.value, kubectlOptions); + wktLogger.debug('applicationStatusResult = %s', JSON.stringify(applicationStatusResult, null, 2)); if (!applicationStatusResult.isSuccess) { const errMessage = i18n.t('vz-application-status-checker-get-status-failed-error-message', {error: applicationStatusResult.reason}); diff --git a/webui/src/js/utils/wkt-actions-base.js b/webui/src/js/utils/wkt-actions-base.js index 3e0463d2d..7ff3b1574 100644 --- a/webui/src/js/utils/wkt-actions-base.js +++ b/webui/src/js/utils/wkt-actions-base.js @@ -1,6 +1,6 @@ /** * @license - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. + * Copyright (c) 2021, 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'; @@ -22,7 +22,7 @@ function(project, wktConsole, i18n, projectIo, dialogHelper, // Execute the specified async action method if no other action is in progress. // If another action is in progress, log a message and cancel the new action. - async executeAction(asyncMethod) { + async executeAction(asyncMethod, ...args) { const description = asyncMethod.name; if (actionInProgress) { wktLogger.info('Cancelling action ' + description + ' because ' + actionInProgress + ' is in progress'); @@ -30,7 +30,7 @@ function(project, wktConsole, i18n, projectIo, dialogHelper, wktLogger.debug('starting action ' + description); actionInProgress = description; try { - await asyncMethod.call(this); + await asyncMethod.call(this, ...args); } finally { wktLogger.debug('action ' + description + ' complete'); actionInProgress = null; @@ -54,9 +54,9 @@ function(project, wktConsole, i18n, projectIo, dialogHelper, }; } - getKubectlOptions() { + getKubectlOptions(kubeConfig = undefined) { return { - kubeConfig: this.project.kubectl.kubeConfig.value, + kubeConfig: kubeConfig || this.project.kubectl.kubeConfig.value, extraPathDirectories: this.getExtraPathDirectoriesArray(this.project.settings.extraPathDirectories.value), extraEnvironmentVariables: this.getExtraEnvironmentVariablesObject(this.project.settings.extraEnvironmentVariables.value) }; diff --git a/webui/src/js/viewModels/choose-kubectl-context-dialog.js b/webui/src/js/viewModels/choose-kubectl-context-dialog.js new file mode 100644 index 000000000..834934a16 --- /dev/null +++ b/webui/src/js/viewModels/choose-kubectl-context-dialog.js @@ -0,0 +1,52 @@ +/** + * @license + * Copyright (c) 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/ + */ +'use strict'; + +define(['accUtils', 'knockout', 'utils/i18n', 'utils/observable-properties', 'utils/validation-helper', + 'ojs/ojarraydataprovider', 'utils/wkt-logger', 'ojs/ojselectcombobox', 'ojs/ojinputtext', 'ojs/ojlabel', + 'ojs/ojbutton', 'ojs/ojdialog', 'ojs/ojformlayout', 'ojs/ojvalidationgroup'], +function(accUtils, ko, i18n, props, validationHelper, ArrayDataProvider) { + function ChooseKubectlContextDialogModel(args) { + const DIALOG_SELECTOR = '#chooseKubectlContextDialog'; + + this.i18n = i18n; + this.availableKubectlContextNames = args.availableKubectlContextNames; + this.selectedKubectlContextName = ko.observable(args.selectedKubectlContextName); + this.availableKubectlContextNamesDP = + new ArrayDataProvider(this.availableKubectlContextNames, { keyAttributes: 'name' }); + + this.connected = () => { + accUtils.announce('Choose Kubectl Context dialog loaded.', 'assertive'); + // open the dialog after the current thread, which is loading this view model. + // using oj-dialog initial-visibility="show" causes vertical centering issues. + setTimeout(function() { + $(DIALOG_SELECTOR)[0].open(); + }, 1); + }; + + this.labelMapper = (labelId) => { + return i18n.t(`kubectl-choose-context-${labelId}`); + }; + + this.okInput = () => { + $(DIALOG_SELECTOR)[0].close(); + + const result = {}; + result.kubectlContextName = this.selectedKubectlContextName(); + args.setValue(result); + }; + + this.cancelInput = () => { + $(DIALOG_SELECTOR)[0].close(); + args.setValue(); + }; + } + + /* + * Returns a constructor for the ViewModel. + */ + return ChooseKubectlContextDialogModel; +}); diff --git a/webui/src/js/viewModels/kubectl-page.js b/webui/src/js/viewModels/kubectl-page.js index aa13703b8..8bf8a1d04 100644 --- a/webui/src/js/viewModels/kubectl-page.js +++ b/webui/src/js/viewModels/kubectl-page.js @@ -1,14 +1,16 @@ /** * @license - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. + * Copyright (c) 2021, 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/ */ 'use strict'; define(['accUtils', 'knockout', 'models/wkt-project', 'utils/i18n', 'ojs/ojarraydataprovider', - 'ojs/ojbufferingdataprovider', 'utils/url-catalog', 'utils/k8s-helper', 'utils/common-utilities', 'utils/wkt-logger', - 'ojs/ojformlayout', 'ojs/ojinputtext', 'ojs/ojselectsingle', 'ojs/ojtable'], -function(accUtils, ko, project, i18n, ArrayDataProvider, BufferingDataProvider, urlCatalog, k8sHelper, utils, wktLogger) { + 'ojs/ojbufferingdataprovider', 'utils/url-catalog', 'utils/k8s-helper', 'utils/common-utilities', + 'utils/dialog-helper', 'utils/view-helper', 'utils/wkt-logger', 'ojs/ojformlayout', 'ojs/ojinputtext', + 'ojs/ojselectsingle', 'ojs/ojtable'], +function(accUtils, ko, project, i18n, ArrayDataProvider, BufferingDataProvider, urlCatalog, k8sHelper, utils, + dialogHelper, viewHelper, wktLogger) { function KubectlViewModel() { this.connected = () => { @@ -39,6 +41,218 @@ function(accUtils, ko, project, i18n, ArrayDataProvider, BufferingDataProvider, return this.project.settings.wdtTargetType.value === 'wko'; }; + this.vzManagedClustersColumnData = [ + { + headerText: this.labelMapper('vz-managed-cluster-name-heading'), + sortProperty: 'name' + }, + { + headerText: this.labelMapper('vz-managed-cluster-kubeconfig-heading'), + sortProperty: 'kubeConfig' + }, + { + headerText: this.labelMapper('vz-managed-cluster-kubecontext-heading'), + sortProperty: 'kubeContext' + }, + { + 'className': 'wkt-table-delete-cell', + // 'headerClassName': 'wkt-table-add-header', + // 'headerTemplate': 'headerTemplate', + 'template': 'actionTemplate', + 'sortable': 'disable', + width: viewHelper.TEXT_BUTTON_COLUMN_WIDTH + }, + { + 'className': 'wkt-table-delete-cell', + 'headerClassName': 'wkt-table-add-header', + 'headerTemplate': 'headerTemplate', + 'template': 'actionTemplate', + 'sortable': 'disable', + width: viewHelper.BUTTON_COLUMN_WIDTH + }, + ]; + + this.vzManagedClustersDP = new BufferingDataProvider(new ArrayDataProvider( + this.project.kubectl.vzManagedClusters.observable, {keyAttributes: 'uid'})); + + this.getConnectivityHeading = ko.computed(() => { + if (this.usingWko()) { + return this.labelMapper('wko-connectivity-heading'); + } else { + return this.labelMapper('vz-connectivity-heading'); + } + }, this); + + this.chooseKubectl = () => { + window.api.ipc.invoke('get-kubectl-exe').then(kubectlPath => { + if (kubectlPath) { + this.project.kubectl.executableFilePath.observable(kubectlPath); + } + }); + }; + + this.chooseHelm = () => { + window.api.ipc.invoke('get-helm-exe').then(helmPath => { + if (helmPath) { + this.project.kubectl.helmExecutableFilePath.observable(helmPath); + } + }); + }; + + async function getKubeConfig() { + const kubeConfigPath = await window.api.ipc.invoke('get-kube-config-files'); + wktLogger.debug('get-kube-config-files returned: %s', kubeConfigPath); + return kubeConfigPath; + } + + this.chooseKubeConfig = () => { + getKubeConfig().then(kubeConfigPath => { + if (kubeConfigPath) { + this.project.kubectl.kubeConfig.observable(kubeConfigPath); + } + }); + }; + + this.chooseManagedKubeConfig = (event, context) => { + const index = context.item.index; + const managedClusterData = this.project.kubectl.vzManagedClusters.observable()[index]; + getKubeConfig().then(kubeConfigPath => { + if (kubeConfigPath) { + const newManagedClusterData = { ...managedClusterData, kubeConfig:kubeConfigPath }; + this.project.kubectl.vzManagedClusters.observable.replace(managedClusterData, newManagedClusterData); + } + }); + }; + + async function getCurrentClusterContext(kubectlExe, options) { + return window.api.ipc.invoke('kubectl-get-current-context', kubectlExe, options); + } + + this.getCurrentContext = () => { + const kubectlExe = this.project.kubectl.executableFilePath.value; + const options = { kubeConfig: this.project.kubectl.kubeConfig.value }; + getCurrentClusterContext(kubectlExe, options).then(results => { + if (results.isSuccess) { + this.project.kubectl.kubeConfigContextToUse.observable(results.context); + } else { + const errTitle = i18n.t('kubectl-get-current-context-error-title'); + const errMessage = i18n.t('kubectl-get-current-context-error-message', { error: results.reason }); + window.api.ipc.invoke('show-error-message', errTitle, errMessage).then().catch(); + } + }); + }; + + this.getManagedCurrentContext = (event, context) => { + const index = context.item.index; + const managedClusterData = this.project.kubectl.vzManagedClusters.observable()[index]; + wktLogger.debug('getting current context for managed cluster" %s', JSON.stringify(managedClusterData)); + + const kubectlExe = this.project.kubectl.executableFilePath.value; + const options = { kubeConfig: managedClusterData.kubeConfig }; + getCurrentClusterContext(kubectlExe, options).then(results => { + if (results.isSuccess) { + const newManagedClusterData = { ...managedClusterData, kubeContext: results.context}; + this.project.kubectl.vzManagedClusters.observable.replace(managedClusterData, newManagedClusterData); + } else { + const errTitle = i18n.t('kubectl-get-current-context-error-title'); + const errMessage = i18n.t('kubectl-get-current-context-error-message', { error: results.reason }); + window.api.ipc.invoke('show-error-message', errTitle, errMessage).then().catch(); + } + }); + }; + + async function getContext(kubectlExe, options, kubeContext) { + const results = await window.api.ipc.invoke('kubectl-get-contexts', kubectlExe, options); + if (results.isSuccess) { + const availableKubectlContextNames = []; + results.contexts.forEach(entry => { + availableKubectlContextNames.push({ + name: entry, + label: entry + }); + }); + const args = { + availableKubectlContextNames, + selectedKubectlContextName: kubeContext + }; + return await dialogHelper.promptDialog('choose-kubectl-context-dialog', args); + } else { + const errTitle = i18n.t('kubectl-get-contexts-error-title'); + const errMessage = i18n.t('kubectl-get-contexts-error-message', { error: results.reason }); + window.api.ipc.invoke('show-error-message', errTitle, errMessage).then().catch(); + } + } + + this.chooseContext = () => { + const kubectlExe = this.project.kubectl.executableFilePath.value; + const options = { kubeConfig: this.project.kubectl.kubeConfig.value }; + const kubeContext = this.project.kubectl.kubeConfigContextToUse.value; + getContext(kubectlExe, options, kubeContext).then(result => { + if (result?.kubectlContextName) { + this.project.kubectl.kubeConfigContextToUse.observable(result.kubectlContextName); + } + }); + }; + + this.chooseManagedContext = (event, context) => { + const index = context.item.index; + const managedClusterData = this.project.kubectl.vzManagedClusters.observable()[index]; + + const kubectlExe = this.project.kubectl.executableFilePath.value; + const options = { kubeConfig: managedClusterData.kubeConfig }; + const kubeContext = managedClusterData.kubeContext; + getContext(kubectlExe, options, kubeContext).then(result => { + if (result?.kubectlContextName) { + const newManagedClusterData = { ...managedClusterData, kubeContext: result.kubectlContextName }; + this.project.kubectl.vzManagedClusters.observable.replace(managedClusterData, newManagedClusterData); + } + }); + }; + + this.chooseAdminContextTooltip = ko.computed(() => { + if (this.project.settings.wdtTargetType.observable() === 'vz') { + return this.labelMapper('config-vz-choose-admin-context-tooltip'); + } else { + return this.labelMapper('config-wko-choose-context-tooltip'); + } + }); + + const generatedManagedClusterNameRegex = /^managed(\d+)$/; + + this.generateNewManagedClusterName = () => { + let index = 1; + this.project.kubectl.vzManagedClusters.observable().forEach(managedCluster => { + const match = managedCluster.name.match(generatedManagedClusterNameRegex); + if (match) { + const indexFound = Number(match[1]); + if (indexFound >= index) { + index = indexFound + 1; + } + } + }); + return `managed${index}`; + }; + + this.handleAddManagedCluster = () => { + const managedClusterToAdd = { + uid: utils.getShortUuid(), + name: this.generateNewManagedClusterName(), + kubeConfig: this.project.kubectl.kubeConfig.value + }; + this.project.kubectl.vzManagedClusters.addNewItem(managedClusterToAdd); + }; + + this.handleDeleteManagedCluster = (event, context) => { + const index = context.item.index; + this.project.kubectl.vzManagedClusters.observable.splice(index, 1); + }; + + this.verifyManagedClusterConnectivity = async (event, context) => { + const index = context.item.index; + const managedClusterData = this.project.kubectl.vzManagedClusters.observable()[index]; + await k8sHelper.startVerifyClusterConnectivity(managedClusterData.kubeConfig, managedClusterData.kubeContext); + }; + this.createLink = function (url, label) { return '' + label + ''; }; @@ -159,45 +373,6 @@ function(accUtils, ko, project, i18n, ArrayDataProvider, BufferingDataProvider, } return flavor; }; - - this.chooseKubectl = () => { - window.api.ipc.invoke('get-kubectl-exe').then(kubectlPath => { - if (kubectlPath) { - this.project.kubectl.executableFilePath.observable(kubectlPath); - } - }); - }; - - this.chooseHelm = () => { - window.api.ipc.invoke('get-helm-exe').then(helmPath => { - if (helmPath) { - this.project.kubectl.helmExecutableFilePath.observable(helmPath); - } - }); - }; - - this.chooseKubeConfig = () => { - window.api.ipc.invoke('get-kube-config-files').then(kubeConfigPath => { - wktLogger.debug('get-kube-config-files returned: %s', kubeConfigPath); - if (kubeConfigPath) { - this.project.kubectl.kubeConfig.observable(kubeConfigPath); - } - }); - }; - - this.getCurrentContext = () => { - const kubectlExe = this.project.kubectl.executableFilePath.value; - const options = { kubeConfig: this.project.kubectl.kubeConfig.value }; - window.api.ipc.invoke('kubectl-get-current-context', kubectlExe, options).then(results => { - if (results.isSuccess) { - this.project.kubectl.kubeConfigContextToUse.observable(results.context); - } else { - const errTitle = i18n.t('kubectl-get-current-context-error-title'); - const errMessage = i18n.t('kubectl-get-current-context-error-message', { error: results.reason }); - window.api.ipc.invoke('show-error-message', errTitle, errMessage).then().catch(); - } - }); - }; } /* diff --git a/webui/src/js/views/choose-kubectl-context-dialog.html b/webui/src/js/views/choose-kubectl-context-dialog.html new file mode 100644 index 000000000..9e6d7abad --- /dev/null +++ b/webui/src/js/views/choose-kubectl-context-dialog.html @@ -0,0 +1,31 @@ + + +
+ + + +
+
+
+ + + + +
+
+ +
+ + + + + + +
+
diff --git a/webui/src/js/views/kubectl-page.html b/webui/src/js/views/kubectl-page.html index 41fbedc11..0202f2642 100644 --- a/webui/src/js/views/kubectl-page.html +++ b/webui/src/js/views/kubectl-page.html @@ -1,5 +1,5 @@
@@ -16,7 +16,8 @@
-
+
+
value="{{project.kubectl.k8sFlavor.observable}}" help.instruction="[[labelMapper('k8s-flavor-help')]]"> + +
+ +
+
+ +
+
+ @@ -32,6 +52,32 @@
+ + + + + + + + +
+
+
+
+
+ + + + + +
+ @@ -43,36 +89,97 @@
+ + + + - - - - - - - -
- -
-