From ab79ed310c6434a1058d97542a8ccf06f04053b5 Mon Sep 17 00:00:00 2001 From: Cody Jackson Date: Thu, 12 Sep 2024 14:58:03 -0700 Subject: [PATCH] Adding in fixes to get harvester cluster pages rendering --- pkg/harvester/config/harvester-cluster.js | 811 ++++++++++++++++++ pkg/harvester/index.ts | 9 + .../harvesterhci.io.management.cluster.js | 227 +++++ pkg/harvester/routing/harvester-routing.js | 85 +- 4 files changed, 1122 insertions(+), 10 deletions(-) create mode 100644 pkg/harvester/config/harvester-cluster.js create mode 100644 pkg/harvester/models/harvesterhci.io.management.cluster.js diff --git a/pkg/harvester/config/harvester-cluster.js b/pkg/harvester/config/harvester-cluster.js new file mode 100644 index 00000000..9accb466 --- /dev/null +++ b/pkg/harvester/config/harvester-cluster.js @@ -0,0 +1,811 @@ +import { + NODE, + CONFIG_MAP, + NAMESPACE, + VIRTUAL_TYPES, + MANAGEMENT, + PVC, + NETWORK_ATTACHMENT, + MONITORING, + LOGGING, + STORAGE_CLASS, + SECRET, +} from '@shell/config/types'; +import { HCI, VOLUME_SNAPSHOT } from '../types'; +import { + STATE, + NAME as NAME_COL, + AGE, + NAMESPACE as NAMESPACE_COL, + LOGGING_OUTPUT_PROVIDERS, + OUTPUT, + CLUSTER_OUTPUT, + CONFIGURED_PROVIDERS, + SUB_TYPE, + ADDRESS, +} from '@shell/config/table-headers'; +import { IF_HAVE } from '@shell/store/type-map'; +import { + IMAGE_DOWNLOAD_SIZE, + FINGERPRINT, + IMAGE_PROGRESS, + SNAPSHOT_TARGET_VOLUME, +} from './table-headers'; + +const TEMPLATE = HCI.VM_VERSION; +const MONITORING_GROUP = 'Monitoring & Logging::Monitoring'; +const LOGGING_GROUP = 'Monitoring & Logging::Logging'; + +export const PRODUCT_NAME = 'harvester'; + +export const IP_POOL_HEADERS = [ + STATE, + NAME_COL, + { + name: 'subnet', + labelKey: 'harvester.ipPool.subnet.label', + value: 'subnetDisplay', + }, + { + name: 'availableIP', + labelKey: 'harvester.ipPool.availableIP.label', + value: 'status.available', + }, + AGE +]; + +export function init($plugin, store) { + const { + product, + basicType, + headers, + configureType, + virtualType, + weightGroup, + weightType, + } = $plugin.DSL(store, PRODUCT_NAME); + + product({ + inStore: 'harvester', + removable: false, + showNamespaceFilter: true, + hideKubeShell: true, + hideKubeConfig: true, + showClusterSwitcher: true, + hideCopyConfig: true, + hideSystemResources: true, + customNamespaceFilter: true, + typeStoreMap: { + [MANAGEMENT.PROJECT]: 'management', + [MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING]: 'management', + [MANAGEMENT.PROJECT_ROLE_TEMPLATE_BINDING]: 'management' + }, + supportRoute: { name: `${PRODUCT_NAME}-c-cluster-support` }, + to: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { + product: PRODUCT_NAME, + resource: HCI.DASHBOARD + } + }, + hideNamespaceLocation: true, + }); + + basicType([HCI.DASHBOARD]); + virtualType({ + labelKey: 'harvester.dashboard.label', + group: 'Root', + name: HCI.DASHBOARD, + weight: 500, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { + product: PRODUCT_NAME, + resource: HCI.DASHBOARD + } + } + }); + configureType(HCI.DASHBOARD, { showListMasthead: false }); + + configureType(HCI.HOST, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.HOST } + }, + resource: NODE, + resourceDetail: HCI.HOST, + resourceEdit: HCI.HOST + }); + + configureType(HCI.HOST, { isCreatable: false, isEditable: true }); + basicType([HCI.HOST]); + + virtualType({ + ifHaveType: NODE, + labelKey: 'harvester.host.label', + group: 'Root', + name: HCI.HOST, + namespaced: true, + weight: 399, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.HOST } + }, + exact: false + }); + + // multiVirtualCluster + basicType(['cluster-members'], 'rbac'); + virtualType({ + ifHave: IF_HAVE.MULTI_CLUSTER, + labelKey: 'members.clusterMembers', + group: 'root', + namespaced: false, + name: VIRTUAL_TYPES.CLUSTER_MEMBERS, + weight: 100, + route: { name: `${PRODUCT_NAME}-c-cluster-members` }, + exact: true, + ifHaveType: { + type: MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING, + store: 'management' + } + }); + + basicType([HCI.VM]); + virtualType({ + labelKey: 'harvester.virtualMachine.label', + group: 'root', + name: HCI.VM, + namespaced: true, + weight: 299, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.VM } + }, + exact: false + }); + + basicType([HCI.VOLUME]); + configureType(HCI.VOLUME, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.VOLUME } + }, + resource: PVC, + resourceDetail: HCI.VOLUME, + resourceEdit: HCI.VOLUME + }); + virtualType({ + labelKey: 'harvester.volume.label', + group: 'root', + ifHaveType: PVC, + name: HCI.VOLUME, + namespaced: true, + weight: 199, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.VOLUME } + }, + exact: false + }); + + basicType([HCI.IMAGE]); + headers(HCI.IMAGE, [ + STATE, + NAME_COL, + NAMESPACE_COL, + IMAGE_PROGRESS, + IMAGE_DOWNLOAD_SIZE, + AGE + ]); + virtualType({ + labelKey: 'harvester.image.label', + group: 'root', + name: HCI.IMAGE, + namespaced: true, + weight: 198, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.IMAGE } + }, + exact: false + }); + + basicType(['projects-namespaces']); + virtualType({ + ifHave: IF_HAVE.MULTI_CLUSTER, + labelKey: 'harvester.projectNamespace.label', + group: 'root', + namespaced: true, + name: 'projects-namespaces', + weight: 98, + route: { name: `${PRODUCT_NAME}-c-cluster-projectsnamespaces` }, + exact: true, + }); + + basicType([ + HCI.ALERTMANAGERCONFIG + ], MONITORING_GROUP); + + basicType([ + HCI.CLUSTER_FLOW, + HCI.CLUSTER_OUTPUT, + HCI.FLOW, + HCI.OUTPUT, + ], LOGGING_GROUP); + + weightGroup('Monitoring', 2, true); + weightGroup('Logging', 1, true); + + headers(HCI.ALERTMANAGERCONFIG, [ + STATE, + NAME_COL, + NAMESPACE_COL, + { + name: 'receivers', + labelKey: 'tableHeaders.receivers', + formatter: 'ReceiverIcons', + value: 'name' + }, + ]); + + configureType(HCI.ALERTMANAGERCONFIG, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.ALERTMANAGERCONFIG }, + }, + resource: MONITORING.ALERTMANAGERCONFIG, + resourceDetail: HCI.ALERTMANAGERCONFIG, + resourceEdit: HCI.ALERTMANAGERCONFIG + }); + + virtualType({ + ifHaveType: MONITORING.ALERTMANAGERCONFIG, + labelKey: 'harvester.monitoring.alertmanagerConfig.label', + name: HCI.ALERTMANAGERCONFIG, + namespaced: true, + weight: 87, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.ALERTMANAGERCONFIG } + }, + exact: false, + }); + + configureType(HCI.CLUSTER_FLOW, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.CLUSTER_FLOW }, + }, + resource: LOGGING.CLUSTER_FLOW, + resourceDetail: HCI.CLUSTER_FLOW, + resourceEdit: HCI.CLUSTER_FLOW + }); + + virtualType({ + labelKey: 'harvester.logging.clusterFlow.label', + name: HCI.CLUSTER_FLOW, + namespaced: true, + weight: 79, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.CLUSTER_FLOW } + }, + exact: false, + }); + + configureType(HCI.CLUSTER_OUTPUT, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.CLUSTER_OUTPUT }, + }, + resource: LOGGING.CLUSTER_OUTPUT, + resourceDetail: HCI.CLUSTER_OUTPUT, + resourceEdit: HCI.CLUSTER_OUTPUT + }); + + virtualType({ + labelKey: 'harvester.logging.clusterOutput.label', + name: HCI.CLUSTER_OUTPUT, + namespaced: true, + weight: 78, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.CLUSTER_OUTPUT } + }, + exact: false, + }); + + configureType(HCI.FLOW, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.FLOW }, + }, + resource: LOGGING.FLOW, + resourceDetail: HCI.FLOW, + resourceEdit: HCI.FLOW + }); + + virtualType({ + labelKey: 'harvester.logging.flow.label', + name: HCI.FLOW, + namespaced: true, + weight: 77, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.FLOW } + }, + exact: false, + }); + + configureType(HCI.OUTPUT, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.OUTPUT }, + }, + resource: LOGGING.OUTPUT, + resourceDetail: HCI.OUTPUT, + resourceEdit: HCI.OUTPUT + }); + + virtualType({ + labelKey: 'harvester.logging.output.label', + name: HCI.OUTPUT, + namespaced: true, + weight: 76, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.OUTPUT } + }, + exact: false, + }); + + headers(HCI.FLOW, [STATE, NAME_COL, NAMESPACE_COL, OUTPUT, CLUSTER_OUTPUT, CONFIGURED_PROVIDERS, AGE]); + headers(HCI.OUTPUT, [STATE, NAME_COL, NAMESPACE_COL, LOGGING_OUTPUT_PROVIDERS, AGE]); + headers(HCI.CLUSTER_FLOW, [STATE, NAME_COL, NAMESPACE_COL, CLUSTER_OUTPUT, CONFIGURED_PROVIDERS, AGE]); + headers(HCI.CLUSTER_OUTPUT, [STATE, NAME_COL, NAMESPACE_COL, LOGGING_OUTPUT_PROVIDERS, AGE]); + + basicType( + [ + HCI.CLUSTER_NETWORK, + HCI.NETWORK_ATTACHMENT, + HCI.LB, + HCI.IP_POOL, + ], + 'networks' + ); + + basicType( + [ + HCI.BACKUP, + HCI.SNAPSHOT, + HCI.VM_SNAPSHOT, + ], + 'backupAndSnapshot' + ); + + weightGroup('networks', 300, true); + weightType(NAMESPACE, 299, true); + weightGroup('backupAndSnapshot', 289, true); + + basicType( + [ + TEMPLATE, + HCI.SSH, + HCI.CLOUD_TEMPLATE, + HCI.STORAGE, + HCI.SR_IOV, + HCI.PCI_DEVICE, + HCI.SR_IOVGPU_DEVICE, + HCI.VGPU_DEVICE, + HCI.ADD_ONS, + HCI.SECRET, + HCI.SETTING + ], + 'advanced' + ); + + configureType(HCI.CLUSTER_NETWORK, { + realResource: HCI.SETTING, + showState: false + }); + + virtualType({ + labelKey: 'harvester.vmTemplate.label', + group: 'root', + name: TEMPLATE, + namespaced: true, + weight: 289, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: TEMPLATE } + }, + exact: false + }); + + configureType(HCI.BACKUP, { showListMasthead: false, showConfigView: false }); + virtualType({ + labelKey: 'harvester.backup.label', + name: HCI.BACKUP, + namespaced: true, + weight: 200, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.BACKUP } + }, + exact: false + }); + + configureType(HCI.VLAN_CONFIG, { hiddenNamespaceGroupButton: true }); + + configureType(HCI.CLUSTER_NETWORK, { showListMasthead: false }); + virtualType({ + labelKey: 'harvester.clusterNetwork.title', + name: HCI.CLUSTER_NETWORK, + ifHaveType: HCI.CLUSTER_NETWORK, + namespaced: false, + weight: 189, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.CLUSTER_NETWORK } + }, + exact: false, + }); + + configureType(HCI.NETWORK_ATTACHMENT, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.NETWORK_ATTACHMENT } + }, + resource: NETWORK_ATTACHMENT, + resourceDetail: HCI.NETWORK_ATTACHMENT, + resourceEdit: HCI.NETWORK_ATTACHMENT + }); + + virtualType({ + labelKey: 'harvester.network.label', + name: HCI.NETWORK_ATTACHMENT, + namespaced: true, + weight: 188, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.NETWORK_ATTACHMENT } + }, + exact: false + }); + + configureType(HCI.SNAPSHOT, { + isCreatable: false, + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.SNAPSHOT }, + }, + resource: VOLUME_SNAPSHOT, + resourceDetail: HCI.SNAPSHOT, + resourceEdit: HCI.SNAPSHOT, + }); + headers(HCI.SNAPSHOT, [STATE, NAME_COL, NAMESPACE_COL, SNAPSHOT_TARGET_VOLUME, AGE]); + virtualType({ + labelKey: 'harvester.snapshot.label', + name: HCI.SNAPSHOT, + namespaced: true, + weight: 190, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.SNAPSHOT } + }, + exact: false, + }); + + configureType(HCI.VM_SNAPSHOT, { + showListMasthead: false, + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.VM_SNAPSHOT } + }, + resource: HCI.BACKUP, + resourceDetail: HCI.VM_SNAPSHOT, + resourceEdit: HCI.VM_SNAPSHOT + }); + + virtualType({ + labelKey: 'harvester.vmSnapshot.label', + name: HCI.VM_SNAPSHOT, + namespaced: true, + weight: 191, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.VM_SNAPSHOT } + }, + exact: false + }); + + headers(HCI.SSH, [STATE, NAME_COL, NAMESPACE_COL, FINGERPRINT, AGE]); + virtualType({ + labelKey: 'harvester.sshKey.label', + name: HCI.SSH, + namespaced: true, + weight: 170, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.SSH } + }, + exact: false + }); + + configureType(HCI.CLOUD_TEMPLATE, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.CLOUD_TEMPLATE } + }, + resource: CONFIG_MAP, + resourceDetail: HCI.CLOUD_TEMPLATE, + resourceEdit: HCI.CLOUD_TEMPLATE + }); + + virtualType({ + labelKey: 'harvester.cloudTemplate.label', + name: HCI.CLOUD_TEMPLATE, + namespaced: true, + weight: 87, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.CLOUD_TEMPLATE } + }, + exact: false + }); + + headers(HCI.SECRET, [ + STATE, + NAME_COL, + NAMESPACE_COL, + SUB_TYPE, + { + name: 'data', + labelKey: 'tableHeaders.data', + value: 'dataPreview', + formatter: 'SecretData' + }, + AGE + ]); + + configureType(HCI.SECRET, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.SECRET } + }, + resource: SECRET, + resourceDetail: HCI.SECRET, + resourceEdit: HCI.SECRET, + notFilterNamespace: ['cattle-monitoring-system', 'cattle-logging-system'] + }); + + virtualType({ + labelKey: 'harvester.secret.label', + name: HCI.SECRET, + namespaced: true, + weight: -999, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.SECRET } + }, + exact: false + }); + + // settings + configureType(HCI.SETTING, { isCreatable: false }); + virtualType({ + ifHaveType: HCI.SETTING, + ifHaveVerb: 'POST', + labelKey: 'harvester.setting.label', + name: HCI.SETTING, + namespaced: true, + weight: -1000, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.SETTING } + }, + exact: false + }); + + configureType(HCI.STORAGE, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.STORAGE } + }, + resource: STORAGE_CLASS, + resourceDetail: HCI.STORAGE, + resourceEdit: HCI.STORAGE, + isCreatable: true, + }); + virtualType({ + labelKey: 'harvester.storage.title', + group: 'root', + ifHaveType: STORAGE_CLASS, + name: HCI.STORAGE, + namespaced: false, + weight: 79, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.STORAGE } + }, + exact: false, + }); + + virtualType({ + label: 'PCI Devices', + group: 'advanced', + weight: 14, + name: HCI.PCI_DEVICE, + namespaced: false, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.PCI_DEVICE } + }, + exact: false, + }); + + configureType(HCI.PCI_DEVICE, { + isCreatable: false, + hiddenNamespaceGroupButton: true, + listGroups: [ + { + icon: 'icon-list-grouped', + value: 'description', + field: 'groupByDevice', + hideColumn: 'description', + tooltipKey: 'resourceTable.groupBy.device' + }, + { + icon: 'icon-cluster', + value: 'node', + field: 'groupByNode', + hideColumn: 'node', + tooltipKey: 'resourceTable.groupBy.node' + } + ] + }); + + virtualType({ + ifHaveType: HCI.SR_IOV, + labelKey: 'harvester.sriov.label', + group: 'advanced', + weight: 15, + name: HCI.SR_IOV, + namespaced: false, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.SR_IOV } + }, + exact: false + }); + + configureType(HCI.SR_IOV, { + isCreatable: false, + hiddenNamespaceGroupButton: true, + }); + + virtualType({ + ifHaveType: HCI.SR_IOVGPU_DEVICE, + labelKey: 'harvester.sriovgpu.label', + group: 'advanced', + weight: 13, + name: HCI.SR_IOVGPU_DEVICE, + namespaced: false, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.SR_IOVGPU_DEVICE } + }, + exact: false, + }); + + configureType(HCI.SR_IOVGPU_DEVICE, { + isCreatable: false, + hiddenNamespaceGroupButton: true, + }); + + virtualType({ + labelKey: 'harvester.vgpu.label', + group: 'advanced', + weight: 12, + name: HCI.VGPU_DEVICE, + namespaced: false, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.VGPU_DEVICE } + }, + exact: false, + }); + + configureType(HCI.VGPU_DEVICE, { + isCreatable: false, + hiddenNamespaceGroupButton: true, + listGroups: [ + { + icon: 'icon-cluster', + value: 'node', + field: 'groupByNode', + hideColumn: 'node', + tooltipKey: 'resourceTable.groupBy.node' + } + ] + }); + + configureType(HCI.ADD_ONS, { + isCreatable: false, + isRemovable: false, + showState: false, + }); + + virtualType({ + label: 'Addons', + group: 'advanced', + name: HCI.ADD_ONS, + ifHaveType: HCI.ADD_ONS, + weight: -900, + namespaced: false, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.ADD_ONS } + }, + exact: false, + }); + + configureType(HCI.LB, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.LB } + }, + }); + virtualType({ + labelKey: 'harvester.loadBalancer.label', + name: HCI.LB, + namespaced: true, + weight: 185, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.LB } + }, + exact: false, + ifHaveType: HCI.LB, + }); + headers(HCI.LB, [ + STATE, + NAME_COL, + { + ...ADDRESS, + formatter: 'HarvesterListener', + }, + { + name: 'workloadType', + labelKey: 'harvester.loadBalancer.workloadType.label', + value: 'workloadTypeDisplay', + }, + { + name: 'ipam', + labelKey: 'harvester.loadBalancer.ipam.label', + value: 'ipamDisplay', + }, + AGE + ]); + + configureType(HCI.IP_POOL, { + location: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.IP_POOL } + }, + }); + virtualType({ + labelKey: 'harvester.ipPool.label', + name: HCI.IP_POOL, + namespaced: false, + weight: 184, + route: { + name: `${PRODUCT_NAME}-c-cluster-resource`, + params: { resource: HCI.IP_POOL } + }, + exact: false, + ifHaveType: HCI.IP_POOL, + }); + headers(HCI.IP_POOL, IP_POOL_HEADERS); +} diff --git a/pkg/harvester/index.ts b/pkg/harvester/index.ts index 3f1f68cb..d51dd116 100644 --- a/pkg/harvester/index.ts +++ b/pkg/harvester/index.ts @@ -1,6 +1,9 @@ import { importTypes } from '@rancher/auto-import'; import { IPlugin } from '@shell/core/types'; import extensionRoutes from './routing/harvester-routing'; +import harvesterCommonStore from './store/harvester-common'; +import harvesterStore from './store/harvester-store'; +import customValidators from './validators'; // Init the package export default function (plugin: IPlugin) { @@ -14,5 +17,11 @@ export default function (plugin: IPlugin) { plugin.metadata.icon = require('./icon.svg'); plugin.addProduct(require('./config/harvester-manager')); + plugin.addProduct(require('./config/harvester-cluster')); + + plugin.addDashboardStore(harvesterCommonStore.config.namespace, harvesterCommonStore.specifics, harvesterCommonStore.config); + plugin.addDashboardStore(harvesterStore.config.namespace, harvesterStore.specifics, harvesterStore.config, harvesterStore.init); + plugin.validators = customValidators; + plugin.addRoutes(extensionRoutes); } diff --git a/pkg/harvester/models/harvesterhci.io.management.cluster.js b/pkg/harvester/models/harvesterhci.io.management.cluster.js new file mode 100644 index 00000000..7ebe46e3 --- /dev/null +++ b/pkg/harvester/models/harvesterhci.io.management.cluster.js @@ -0,0 +1,227 @@ +import ProvCluster from '@shell/models/provisioning.cattle.io.cluster'; +import { DEFAULT_WORKSPACE, HCI, MANAGEMENT } from '@shell/config/types'; +import { HARVESTER_NAME, HARVESTER_NAME as VIRTUAL } from '@shell/config/features'; +import { SETTING } from '@shell/config/settings'; + +export default class HciCluster extends ProvCluster { + get stateObj() { + return this._stateObj; + } + + applyDefaults() { + if ( !this.spec ) { + this['spec'] = { agentEnvVars: [] }; + this['metadata'] = { namespace: DEFAULT_WORKSPACE }; + } + } + + get isReady() { + // If the Connected condition exists, use that (2.6+) + if ( this.hasCondition('Connected') ) { + return this.isCondition('Connected'); + } + + // Otherwise use Ready (older) + return this.isCondition('Ready'); + } + + get canEdit() { + return false; + } + + cachedHarvesterClusterVersion = ''; + + _uiInfo = undefined; + + /** + * Fetch and cache the response for /ui-info + * + * Storing this in a cache means any changes to `ui-info` require a dashboard refresh... but it cuts out a http request every time we + * go to a cluster + * + * @param {string} clusterId + */ + async _getUiInfo(clusterId) { + if (!this._uiInfo) { + try { + const infoUrl = `/k8s/clusters/${ clusterId }/v1/harvester/ui-info`; + + this._uiInfo = await this.$dispatch('request', { url: infoUrl }); + } catch (e) { + console.info(`Failed to fetch harvester ui-info from ${ this.nameDisplay }, this may be an older cluster that cannot provide one`); // eslint-disable-line no-console + } + } + + return this._uiInfo; + } + + /** + * Determine the harvester plugin's package name and url for legacy clusters that don't provide the package (i.e. it's coming from + * outside the cluster) + */ + _legacyClusterPkgDetails() { + let uiOfflinePreferred = this.$rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.UI_OFFLINE_PREFERRED)?.value; + // options: ['dynamic', 'true', 'false'] + + if (uiOfflinePreferred === 'dynamic') { + // We shouldn't need to worry about the version of the dashboard when embedded in harvester (aka in isSingleProduct) + const version = this.$rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.VERSION_RANCHER)?.value; + + if (version.endsWith('-head')) { + uiOfflinePreferred = 'false'; + } else { + uiOfflinePreferred = 'true'; + } + } + + // This is the version that's embedded in the dashboard + const pkgName = `${ HARVESTER_NAME }-1.0.3`; + + if (uiOfflinePreferred === 'true') { + // Embedded (aka give me the embedded plugin that was in the last rancher release) + const embeddedPath = `${ pkgName }/${ pkgName }.umd.min.js`; + + return { + pkgUrl: process.env.dev ? `${ process.env.api }/dashboard/${ embeddedPath }` : embeddedPath, + pkgName + }; + } + + if (uiOfflinePreferred === 'false') { + // Remote (aka give me the latest version of the embedded plugin that might not have been released yet) + const uiDashboardHarvesterRemotePlugin = this.$rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.UI_DASHBOARD_HARVESTER_LEGACY_PLUGIN)?.value; + const parts = uiDashboardHarvesterRemotePlugin?.replace('.umd.min.js', '').split('/'); + const pkgNameFromUrl = parts?.length > 1 ? parts[parts.length - 1] : null; + + if (!pkgNameFromUrl) { + throw new Error(`Unable to determine harvester plugin name from '${ uiDashboardHarvesterRemotePlugin }'`); + } + + return { + pkgUrl: uiDashboardHarvesterRemotePlugin, + pkgName: pkgNameFromUrl + }; + } + + throw new Error(`Unsupported value for ${ SETTING.UI_OFFLINE_PREFERRED }: 'uiOfflinePreferred'`); + } + + /** + * Determine the harvester plugin's package name and url for clusters that provide the plugin + */ + _supportedClusterPkgDetails(uiInfo, clusterId) { + let pkgName = `${ HARVESTER_NAME }-${ uiInfo['ui-plugin-bundled-version'] }`; + const fileName = `${ pkgName }.umd.min.js`; + let pkgUrl; + + if (uiInfo['ui-source'] === 'bundled' ) { // offline bundled + pkgUrl = `/k8s/clusters/${ clusterId }/v1/harvester/plugin-assets/${ fileName }`; + } else if (uiInfo['ui-source'] === 'external') { + if (uiInfo['ui-plugin-index']) { + pkgUrl = uiInfo['ui-plugin-index']; + + // When using an external address, the pkgName should also be get from the url + const names = pkgUrl.split('/'); + const jsName = names[names.length - 1]; + + pkgName = jsName?.split('.umd.min.js')[0]; + } else { + throw new Error('Harvester cluster requested the plugin at `ui-plugin-index` is used, however did not provide a value for it'); + } + } + + return { + pkgUrl, + pkgName + }; + } + + _overridePkgDetails() { + // Support loading the pkg from a locally, or other, address + // This helps testing of the harvester plugin when packaged up, instead of directly imported + const harvesterPkgUrl = process.env.harvesterPkgUrl; + + if (!harvesterPkgUrl) { + return; + } + const parts = harvesterPkgUrl.replace('.umd.min.js', '').split('/'); + const pkgNameFromUrl = parts.length > 1 ? parts[parts.length - 1] : null; + + if (pkgNameFromUrl) { + return { + pkgUrl: harvesterPkgUrl, + pkgName: pkgNameFromUrl + }; + } + } + + async _pkgDetails() { + const overridePkgDetails = this._overridePkgDetails(); + + if (overridePkgDetails) { + return overridePkgDetails; + } + + const clusterId = this.mgmt.id; + const uiInfo = await this._getUiInfo(clusterId); + + return uiInfo ? this._supportedClusterPkgDetails(uiInfo, clusterId) : this._legacyClusterPkgDetails(); + } + + async loadClusterPlugin() { + // Skip loading if it's built in + const plugins = this.$rootState.$plugin.getPlugins(); + const loadedPkgs = Object.keys(plugins); + + if (loadedPkgs.find((pkg) => pkg === HARVESTER_NAME)) { + console.info('Harvester plugin built is built in, skipping load from external sources'); // eslint-disable-line no-console + + return; + } + + // Determine the plugin name and the url it can be fetched from + const { pkgUrl, pkgName } = await this._pkgDetails(); + + console.info('Harvester plugin details: ', pkgName, pkgUrl); // eslint-disable-line no-console + + // Skip loading if we've previously loaded the correct one + if (!!plugins[pkgName]) { + console.info('Harvester plugin already loaded, no need to load', pkgName); // eslint-disable-line no-console + + return; + } + + console.info('Attempting to load Harvester plugin', pkgName); // eslint-disable-line no-console + + const res = await this.$rootState.$plugin.loadAsync(pkgName, pkgUrl); + + console.info('Loaded Harvester plugin', pkgName); // eslint-disable-line no-console + + return res; + } + + async goToCluster() { + await this.loadClusterPlugin() + .then(() => { + this.currentRouter().push({ + name: `${ VIRTUAL }-c-cluster-resource`, + params: { + cluster: this.status.clusterName, + product: VIRTUAL, + resource: HCI.DASHBOARD // Go directly to dashboard to avoid blip of components on screen + } + }); + }) + .catch((err) => { + const message = typeof error === 'object' ? JSON.stringify(err) : err; + + console.error('Failed to load harvester package: ', message); // eslint-disable-line no-console + + this.$dispatch('growl/error', { + title: this.t('harvesterManager.plugins.loadError'), + message, + timeout: 5000 + }, { root: true }); + }); + } +} diff --git a/pkg/harvester/routing/harvester-routing.js b/pkg/harvester/routing/harvester-routing.js index 19cf2688..68beef4e 100644 --- a/pkg/harvester/routing/harvester-routing.js +++ b/pkg/harvester/routing/harvester-routing.js @@ -1,18 +1,83 @@ -import { RouteRecordRaw } from 'vue-router'; -import { PRODUCT_NAME } from '../config/harvester-manager'; +// eslint-disable-next-line import/named +// import { RouteRecordRaw } from 'vue-router'; +import { PRODUCT_NAME } from '../config/harvester'; import Root from '../pages/c/_cluster/index.vue'; -import ListHarvesterMgrResource from '../pages/c/_cluster/_resource/index.vue'; -import CreateHarvesterMgrResource from '../pages/c/_cluster/_resource/create.vue'; -import ViewHarvesterMgrResource from '../pages/c/_cluster/_resource/_id.vue'; -import ClusterListPage from '../list/harvesterhci.io.management.cluster.vue'; +import HarvesterSupport from '../pages/c/_cluster/support/index.vue'; +import HarvesterConsoleSerial from '../pages/c/_cluster/console/_uid/serial.vue'; +import HarvesterConsoleVnc from '../pages/c/_cluster/console/_uid/vnc.vue'; +import ListHarvesterResource from '../pages/c/_cluster/_resource/index.vue'; +import HarvesterBrand from '../pages/c/_cluster/brand/index.vue'; +import CreateHarvesterResource from '../pages/c/_cluster/_resource/create.vue'; +import ViewHarvesterResource from '../pages/c/_cluster/_resource/_id.vue'; +import ViewHarvesterNsResource from '../pages/c/_cluster/_resource/_namespace/_id.vue'; +import HarvesterAirgapUpdgrade from '../pages/c/_cluster/airgapupgrade/index.vue'; +import HarvesterMembers from '../pages/c/_cluster/members/index.vue'; +import ProjectNamespaces from '../pages/c/_cluster/projectsnamespaces.vue'; +import HarvesterAlertmanagerReceiver from '../pages/c/_cluster/alertmanagerconfig/_alertmanagerconfigid/receiver.vue'; const routes = [ { - path: '/harvesterManager/clusters', - name: `${ PRODUCT_NAME }-c-cluster-list-page`, - component: ClusterListPage, + name: `${ PRODUCT_NAME }-c-cluster-support`, + path: `/:product/c/:cluster/support`, + component: HarvesterSupport, }, + { + name: `${ PRODUCT_NAME }-c-cluster-console-uid-serial`, + path: `/:product/c/:cluster/console/:uid/serial`, + component: HarvesterConsoleSerial, + }, + { + name: `${ PRODUCT_NAME }-c-cluster-console-uid-vnc`, + path: `/:product/c/:cluster/console/:uid/vnc`, + component: HarvesterConsoleVnc, + }, + { + name: `${ PRODUCT_NAME }-c-cluster-airgapupgrade`, + path: `/:product/c/:cluster/airgapupgrade`, + component: HarvesterAirgapUpdgrade, + }, + { + name: `${ PRODUCT_NAME }-c-cluster-brand`, + path: `/:product/c/:cluster/brand`, + component: HarvesterBrand, + }, + { + name: `${ PRODUCT_NAME }-c-cluster-alertmanagerconfig-alertmanagerconfigid-receiver`, + path: `/:product/c/:cluster/alertmanagerconfig/:alertmanagerconfigid/receiver`, + component: HarvesterAlertmanagerReceiver, + }, + { + name: `${ PRODUCT_NAME }-c-cluster-members`, + path: `/:product/c/:cluster/members`, + component: HarvesterMembers, + }, + { + name: `${ PRODUCT_NAME }-c-cluster`, + path: `/:product/c/:cluster`, + component: Root, + }, { + name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces`, + path: `/:product/c/:cluster/projectsnamespaces`, + component: ProjectNamespaces, + }, { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + path: `/:product/c/:cluster/:resource`, + component: ListHarvesterResource, + }, { + name: `${ PRODUCT_NAME }-c-cluster-resource-create`, + path: `/:product/c/:cluster/:resource/create`, + component: CreateHarvesterResource, + }, { + name: `${ PRODUCT_NAME }-c-cluster-resource-id`, + path: `/:product/c/:cluster/:resource/:id`, + component: ViewHarvesterResource, + }, { + name: `${ PRODUCT_NAME }-c-cluster-resource-namespace-id`, + path: `/:product/c/:cluster/:resource/:namespace/:id`, + component: ViewHarvesterNsResource, + }, + ]; -export default routes; +export default routes; \ No newline at end of file