Skip to content

Commit

Permalink
Merge pull request #12770 from torchiaf/backport-12768-harvester-port…
Browse files Browse the repository at this point in the history
…ings

[backport 2.10] Harvester Manager plugin, portings from Harvester
  • Loading branch information
torchiaf authored Dec 5, 2024
2 parents e966f71 + 87f2b7b commit be1fdec
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 188 deletions.
8 changes: 8 additions & 0 deletions pkg/harvester-manager/config/harvester-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,16 @@ export function init($plugin, store) {
headers(HCI.CLUSTER, [
STATE,
NAME_COL,
{
name: 'harvesterVersion',
sort: 'harvesterVersion',
labelKey: 'harvesterManager.tableHeaders.harvesterVersion',
value: 'harvesterVersion',
getValue: (row) => row.harvesterVersion
},
{
...VERSION,
labelKey: 'harvesterManager.tableHeaders.kubernetesVersion',
value: 'kubernetesVersion',
getValue: (row) => row.kubernetesVersion
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export default {
@finish="saveOverride"
@error="e=>errors = e"
>
<Banner
v-if="isCreate"
color="warning"
>
{{ t('harvesterManager.cluster.supportMessage') }}
</Banner>
<div class="mt-20">
<NameNsDescription
v-if="!isView"
Expand Down
6 changes: 5 additions & 1 deletion pkg/harvester-manager/l10n/en-us.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
harvesterManager:
manage: Manage
tableHeaders:
kubernetesVersion: Kubernetes Version
harvesterVersion: Harvester Version
cluster:
label: Harvester Clusters
none: There are no Harvester Clusters
learnMore: Learn more about Harvester from the <a target="_blank" href="https://harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Web Site</a> or read the the <a target="_blank" href="https://docs.harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Docs</a>
learnMore: Learn more about Harvester from the <a target="_blank" href="https://harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Web Site</a> or read the <a target="_blank" href="https://docs.harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Docs</a>
description: Harvester is a modern Hyperconverged infrastructure (HCI) solution built for bare metal servers using enterprise-grade open source technologies including Kubernetes, Kubevirt and Longhorn.
supportMessage: Harvester ui extension only supports Harvester cluster versions greater or equal to 1.3.0
plugins:
loadError: Error loading harvester plugin
extension:
Expand Down
22 changes: 15 additions & 7 deletions pkg/harvester-manager/list/harvesterhci.io.management.cluster.vue
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,23 @@ export default {
},
rows() {
return this.hciClusters.filter((c) => {
const cluster = this.mgmtClusters.find((cluster) => cluster?.metadata?.name === c?.status?.clusterName);
return isHarvesterCluster(cluster);
});
return this.hciClusters
.filter((c) => {
const cluster = this.mgmtClusters.find((cluster) => cluster?.metadata?.name === c?.status?.clusterName);
return isHarvesterCluster(cluster);
})
.map((row) => {
if (row.isReady) {
row.setSupportedHarvesterVersion();
}
return row;
});
},
typeDisplay() {
return this.t(`typeLabel."${ HCI.CLUSTER }"`, { count: this.row?.length || 0 });
return this.t(`typeLabel."${ HCI.CLUSTER }"`, { count: this.rows?.length || 0 });
},
},
Expand Down Expand Up @@ -183,7 +191,7 @@ export default {
<td>
<span class="cluster-link">
<a
v-if="row.isReady"
v-if="row.isReady && row.isSupportedHarvester"
class="link"
:disabled="navigating ? true : null"
@click="goToCluster(row)"
Expand Down
220 changes: 40 additions & 180 deletions pkg/harvester-manager/models/harvesterhci.io.management.cluster.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
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';
import { DEFAULT_WORKSPACE, HCI } from '@shell/config/types';
import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
import { colorForState, stateDisplay, STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';

export default class HciCluster extends ProvCluster {
get isSupportedHarvester() {
return this._isSupportedHarvester === undefined ? true : this._isSupportedHarvester;
}

get harvesterVersion() {
return this._harvesterVersion || this.$rootGetters['i18n/t']('generic.provisioning');
}

get stateObj() {
if (!this.isSupportedHarvester) {
return { error: true, message: this.t('harvesterManager.cluster.supportMessage') };
}

return this._stateObj;
}

Expand All @@ -29,199 +41,47 @@ export default class HciCluster extends ProvCluster {
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
}
get stateColor() {
if (!this.isSupportedHarvester) {
return colorForState(STATES_ENUM.DENIED);
}

return this._uiInfo;
return colorForState(this.state);
}

/**
* 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
};
get stateDisplay() {
if (!this.isSupportedHarvester) {
return stateDisplay(STATES_ENUM.DENIED);
}

throw new Error(`Unsupported value for ${ SETTING.UI_OFFLINE_PREFERRED }: 'uiOfflinePreferred'`);
return stateDisplay(this.state);
}

/**
* 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');
async goToCluster() {
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
}
}

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) {
async setSupportedHarvesterVersion() {
if (this._isSupportedHarvester !== undefined) {
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();
}
const url = `/k8s/clusters/${ this.status.clusterName }/v1`;

async loadClusterPlugin() {
// Skip loading if it's built in
const plugins = this.$rootState.$plugin.getPlugins();
const loadedPkgs = Object.keys(plugins);
try {
const setting = await this.$dispatch('request', { url: `${ url }/${ HCI.SETTING }s/server-version` });

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;
this._harvesterVersion = setting?.value;
this._isSupportedHarvester = this.$rootGetters['harvester-common/getFeatureEnabled']('supportHarvesterClusterVersion', setting?.value);
} catch (error) {
console.error('unable to get harvester version from settings/server-version', error); // eslint-disable-line no-console
}

// 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 });
});
}
}

0 comments on commit be1fdec

Please sign in to comment.