diff --git a/shell/assets/translations/en-us.yaml b/shell/assets/translations/en-us.yaml index 9ef3dd017c3..478c43599ed 100644 --- a/shell/assets/translations/en-us.yaml +++ b/shell/assets/translations/en-us.yaml @@ -355,9 +355,8 @@ addClusterMemberDialog: title: Add Cluster Member addonConfigConfirmation: - title: Add-On Config Reset - body: Changing the Kubernetes Version can reset the Add-On Config values. You should check that the values are as expected before continuing. - + title: Add-On Reset + body: Changing the Kubernetes Version can reset Add-On values. You should check that the values are as expected before continuing. addProjectMemberDialog: title: Add Project Member @@ -1108,38 +1107,38 @@ cis: cluster: addonChart: rancher-vsphere-cpi: - label: vSphere CPI - configuration: vSphere CPI Configuration + label: "Add-on: vSphere CPI" + configuration: vSphere CPI rancher-vsphere-csi: - label: vSphere CSI - configuration: vSphere CSI Configuration + label: "Add-on: vSphere CSI" + configuration: vSphere CSI rke2-calico: - label: Calico - configuration: Calico Configuration + label: "Add-on: Calico" + configuration: Calico rke2-calico-crd: - label: Calico - configuration: Calico Configuration + label: "Add-on: Calico" + configuration: Calico rke2-canal: - label: Canal - configuration: Canal Configuration + label: "Add-on: Canal" + configuration: Canal rke2-cilium: - label: Cilium - configuration: Cilium Configuration + label: "Add-on: Cilium" + configuration: Cilium rke2-coredns: - label: CoreDNS - configuration: CoreDNS Configuration + label: "Add-on: CoreDNS" + configuration: CoreDNS rke2-ingress-nginx: - label: NGINX - configuration: NGINX Ingress Configuration + label: "Add-on: NGINX" + configuration: NGINX Ingress rke2-kube-proxy: - label: Kube Proxy - configuration: Kube Proxy Configuration + label: "Add-on: Kube Proxy" + configuration: Kube Proxy rke2-metrics-server: - label: Metrics Server - configuration: Metrics Server Configuration + label: "Add-on: Metrics Server" + configuration: Metrics Server rke2-multus: - label: Multus - configuration: Multus Configuration + label: "Add-on: Multus" + configuration: Multus agentEnvVars: label: Agent Environment detail: Add additional environment variables to the agent container. This is most commonly useful for configuring a HTTP proxy. @@ -1155,7 +1154,7 @@ cluster: label: Google rancher-vsphere: label: vSphere - note: 'Important: Configure the vSphere Cloud Provider and Storage Provider options in the tabs on the left.' + note: 'Important: Configure the vSphere Cloud Provider and Storage Provider options in the Add-on tabs.' harvester: label: Harvester copyConfig: Copy KubeConfig to Clipboard @@ -1657,7 +1656,7 @@ cluster: serverOs: label: OS addOns: - dependencyBanner: Add-On Configurations can vary between Kubernetes versions. Changing the Kubernetes version may reset the values below. + dependencyBanner: Add-On Configuration can vary between Kubernetes versions. Changing the Kubernetes version may reset the values below. additionalManifest: title: Additional Manifest tooltip: 'Additional Kubernetes Manifest YAML to be applied to the cluster on startup.' diff --git a/shell/edit/provisioning.cattle.io.cluster/rke2.vue b/shell/edit/provisioning.cattle.io.cluster/rke2.vue index 682e098d420..2f51dbb24f8 100644 --- a/shell/edit/provisioning.cattle.io.cluster/rke2.vue +++ b/shell/edit/provisioning.cattle.io.cluster/rke2.vue @@ -75,6 +75,7 @@ import MemberRoles from '@shell/edit/provisioning.cattle.io.cluster/MemberRoles' import Basics from '@shell/edit/provisioning.cattle.io.cluster/Basics'; import AddOnConfig from '@shell/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig'; import AddOnAdditionalManifest from '@shell/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest'; +import VsphereUtils from '@shell/utils/v-sphere'; const HARVESTER = 'harvester'; const HARVESTER_CLOUD_PROVIDER = 'harvester-cloud-provider'; @@ -839,6 +840,8 @@ export default { created() { this.registerBeforeHook(this.saveMachinePools, 'save-machine-pools', 1); this.registerBeforeHook(this.setRegistryConfig, 'set-registry-config'); + this.registerBeforeHook(this.handleVsphereCpiSecret, 'sync-vsphere-cpi'); + this.registerBeforeHook(this.handleVsphereCsiSecret, 'sync-vsphere-csi'); this.registerAfterHook(this.cleanupMachinePools, 'cleanup-machine-pools'); this.registerAfterHook(this.saveRoleBindings, 'save-role-bindings'); @@ -851,6 +854,13 @@ export default { methods: { set, + async handleVsphereCpiSecret() { + return VsphereUtils.handleVsphereCpiSecret(this); + }, + async handleVsphereCsiSecret() { + return VsphereUtils.handleVsphereCsiSecret(this); + }, + /** * Initialize all the cluster specs */ diff --git a/shell/utils/cluster.js b/shell/utils/cluster.js index 67f78f5cddf..591b1f98c34 100644 --- a/shell/utils/cluster.js +++ b/shell/utils/cluster.js @@ -94,7 +94,7 @@ export function abbreviateClusterName(input) { export function labelForAddon(name, configuration = true) { const addon = camelToTitle(name.replace(/^(rke|rke2|rancher)-/, '')); - const fallback = `${ addon } ${ configuration ? 'Configuration' : '' }`; + const fallback = `${ configuration ? '' : 'Add-on: ' }${ addon }`; const key = `cluster.addonChart."${ name }"${ configuration ? '.configuration' : '.label' }`; return this.$store.getters['i18n/withFallback'](key, null, fallback); diff --git a/shell/utils/v-sphere.ts b/shell/utils/v-sphere.ts new file mode 100644 index 00000000000..e37ca189d75 --- /dev/null +++ b/shell/utils/v-sphere.ts @@ -0,0 +1,237 @@ +import merge from 'lodash/merge'; +import { SECRET } from '@shell/config/types'; + +type Rke2Component = { + versionInfo: any; + userChartValues: any; + chartVersionKey: (chartName: string) => string; + value: any; + isEdit: boolean; + $store: any, +} + +type SecretDetails = { + generateName: string, + upstreamClusterName: string, + upstreamNamespace: string, + downstreamName: string, + downstreamNamespace: string, + json?: object, +} +type Values = any; + +type ChartValues = { + defaultValues: Values, + userValues: Values, + combined: Values, +}; + +const rootGenerateName = 'vsphere-secret-'; + +type SecretJson = any; + +class VSphereUtils { + private async findSecret( + { $store }: Rke2Component, { + generateName, upstreamClusterName, upstreamNamespace, downstreamName, downstreamNamespace + }: SecretDetails): Promise { + const secrets = await $store.dispatch('management/request', { url: `/v1/${ SECRET }/${ upstreamNamespace }?filter=metadata.name=${ generateName }` }); + + const applicableSecret = secrets.data?.filter((s: any) => { + return s.metadata.annotations['provisioning.cattle.io/sync-target-namespace'] === downstreamNamespace && + s.metadata.annotations['provisioning.cattle.io/sync-target-name'] === downstreamName && + s.metadata.annotations['rke.cattle.io/object-authorized-for-clusters'].includes(upstreamClusterName); + }); + + if (applicableSecret.length > 1) { + return Promise.reject(new Error(`Found multiple matching secrets (${ upstreamNamespace }/${ upstreamNamespace } for ${ upstreamClusterName }), this will cause synchronizing mishaps. Consider removing stale secrets from old clusters`)); + } + + return applicableSecret[0]; + } + + private async findOrCreateSecret( + rke2Component: Rke2Component, + { + generateName, upstreamClusterName, upstreamNamespace, downstreamName, downstreamNamespace, json + }: SecretDetails + ) { + const { $store } = rke2Component; + + const secretJson = await this.findSecret(rke2Component, { + generateName, + upstreamClusterName, + upstreamNamespace, + downstreamName, + downstreamNamespace + }) || json; + + return await $store.dispatch('management/create', secretJson); + } + + private findChartValues({ + versionInfo, + userChartValues, + chartVersionKey, + }: Rke2Component, chartName: string): ChartValues | undefined { + const chartValues = versionInfo[chartName]?.values; + + if (!chartValues) { + return; + } + const userValues = userChartValues[chartVersionKey(chartName)]; + + return { + defaultValues: chartValues, + userValues, + combined: merge({}, chartValues || {}, userValues || {}) + }; + } + + /** + * Create upstream vsphere cpi secret to sync downstream + */ + async handleVsphereCpiSecret(rke2Component: Rke2Component) { + const generateName = `${ rootGenerateName }cpi-`; + const downstreamName = 'vsphere-cpi-creds'; + const downstreamNamespace = 'kube-system'; + const { value } = rke2Component; + + // check values for cpi chart has 'use our method' checkbox + const { userValues, combined } = this.findChartValues(rke2Component, 'rancher-vsphere-cpi') || {}; + + if (!combined?.vCenter?.credentialsSecret?.generate) { + return; + } + + // find values needed in cpi chart value - https://github.com/rancher/vsphere-charts/blob/main/charts/rancher-vsphere-cpi/questions.yaml#L16-L42 + const { username, password, host } = combined.vCenter; + + if (!username || !password || !host) { + throw new Error('vSphere CPI username, password and host are all required when generating a new secret'); + } + + // create secret as per https://github.com/rancher/vsphere-charts/blob/main/charts/rancher-vsphere-cpi/templates/secret.yaml + const upstreamClusterName = value.metadata.name; + const upstreamNamespace = value.metadata.namespace; + const secret = await this.findOrCreateSecret(rke2Component, { + generateName, + upstreamClusterName, + upstreamNamespace, + downstreamName, + downstreamNamespace, + json: { + type: SECRET, + metadata: { + namespace: upstreamNamespace, + generateName, + labels: { + 'vsphere-cpi-infra': 'secret', + component: 'rancher-vsphere-cpi-cloud-controller-manager' + }, + annotations: { + 'provisioning.cattle.io/sync-target-namespace': downstreamNamespace, + 'provisioning.cattle.io/sync-target-name': downstreamName, + 'rke.cattle.io/object-authorized-for-clusters': upstreamClusterName, + 'provisioning.cattle.io/sync-bootstrap': 'true' + } + }, + } + }); + + secret.setData(`${ host }.username`, username); + secret.setData(`${ host }.password`, password); + + await secret.save(); + + // reset cpi chart values + if (!userValues.vCenter.credentialsSecret) { + userValues.vCenter.credentialsSecret = {}; + } + userValues.vCenter.credentialsSecret.generate = false; + userValues.vCenter.credentialsSecret.name = downstreamName; + userValues.vCenter.username = ''; + userValues.vCenter.password = ''; + } + + /** + * Create upstream vsphere csi secret to sync downstream + */ + async handleVsphereCsiSecret(rke2Component: Rke2Component) { + const generateName = `${ rootGenerateName }csi-`; + const downstreamName = 'vsphere-csi-creds'; + const downstreamNamespace = 'kube-system'; + const { value } = rke2Component; + + // check values for cpi chart has 'use our method' checkbox + const { userValues, combined } = this.findChartValues(rke2Component, 'rancher-vsphere-csi') || {}; + + if (!combined?.vCenter?.configSecret?.generate) { + return; + } + + // find values needed in cpi chart value - https://github.com/rancher/vsphere-charts/blob/main/charts/rancher-vsphere-csi/questions.yaml#L1-L36 + const { + username, password, host, datacenters, port, insecureFlag + } = combined.vCenter; + + if (!username || !password || !host || !datacenters) { + throw new Error('vSphere CSI username, password, host and datacenters are all required when generating a new secret'); + } + + // This is a copy of https://github.com/rancher/vsphere-charts/blob/a5c99d716df960dc50cf417d9ecffad6b55ca0ad/charts/rancher-vsphere-csi/values.yaml#L12-L21 + // Which makes it's way into the secret via https://github.com/rancher/vsphere-charts/blob/main/charts/rancher-vsphere-csi/templates/secret.yaml#L8 + let configTemplateString = ' |\n [Global]\n cluster-id = {{ required \".Values.vCenter.clusterId must be provided\" (default .Values.vCenter.clusterId .Values.global.cattle.clusterId) | quote }}\n user = {{ .Values.vCenter.username | quote }}\n password = {{ .Values.vCenter.password | quote }}\n port = {{ .Values.vCenter.port | quote }}\n insecure-flag = {{ .Values.vCenter.insecureFlag | quote }}\n\n [VirtualCenter {{ .Values.vCenter.host | quote }}]\n datacenters = {{ .Values.vCenter.datacenters | quote }}'; + + configTemplateString = configTemplateString.replace('{{ required \".Values.vCenter.clusterId must be provided\" (default .Values.vCenter.clusterId .Values.global.cattle.clusterId) | quote }}', `"{{clusterId}}"`); + configTemplateString = configTemplateString.replace('{{ .Values.vCenter.username | quote }}', `"${ username }"`); + configTemplateString = configTemplateString.replace('{{ .Values.vCenter.password | quote }}', `"${ password }"`); + configTemplateString = configTemplateString.replace('{{ .Values.vCenter.port | quote }}', `"${ port }"`); + configTemplateString = configTemplateString.replace('{{ .Values.vCenter.insecureFlag | quote }}', `"${ insecureFlag }"`); + configTemplateString = configTemplateString.replace('{{ .Values.vCenter.host | quote }}', `"${ host }"`); + configTemplateString = configTemplateString.replace('{{ .Values.vCenter.datacenters | quote }}', `"${ datacenters }"`); + // create secret as per https://github.com/rancher/vsphere-charts/blob/main/charts/rancher-vsphere-csi/templates/secret.yaml + const upstreamClusterName = value.metadata.name; + const upstreamNamespace = value.metadata.namespace; + + const secret = await this.findOrCreateSecret(rke2Component, { + generateName, + upstreamClusterName, + upstreamNamespace, + downstreamName, + downstreamNamespace, + json: { + type: SECRET, + metadata: { + namespace: upstreamNamespace, + generateName, + annotations: { + 'provisioning.cattle.io/sync-target-namespace': downstreamNamespace, + 'provisioning.cattle.io/sync-target-name': downstreamName, + 'rke.cattle.io/object-authorized-for-clusters': upstreamClusterName, + 'provisioning.cattle.io/sync-bootstrap': 'true' + } + }, + } + }); + + secret.setData(`csi-vsphere.conf`, configTemplateString); + + await secret.save(); + + // reset csi chart values + if (!userValues.vCenter.configSecret) { + userValues.vCenter.configSecret = {}; + } + userValues.vCenter.configSecret.generate = false; + userValues.vCenter.configSecret.name = downstreamName; + userValues.vCenter.username = ''; + userValues.vCenter.password = ''; + userValues.vCenter.host = ''; + userValues.vCenter.datacenters = ''; + } +} + +const utils = new VSphereUtils(); + +export default utils;