mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-02-04 06:51:44 +00:00
Add Dashboard page
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
parent
9b5bdeb85c
commit
f8408469f7
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
dev
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
/dist
|
/dist
|
||||||
/dist-pkg
|
/dist-pkg
|
||||||
|
|||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -13,7 +13,6 @@
|
|||||||
"babel.config.js": true,
|
"babel.config.js": true,
|
||||||
"jsconfig.json": true,
|
"jsconfig.json": true,
|
||||||
"LICENSE": true,
|
"LICENSE": true,
|
||||||
"node_modules": true,
|
|
||||||
"pkg/**/.shell": true,
|
"pkg/**/.shell": true,
|
||||||
"pkg/**/node_modules": true,
|
"pkg/**/node_modules": true,
|
||||||
"yarn-error.log": true
|
"yarn-error.log": true
|
||||||
|
|||||||
235
pkg/harvester/components/HarvesterUpgrade.vue
Normal file
235
pkg/harvester/components/HarvesterUpgrade.vue
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import UpgradeInfo from './UpgradeInfo';
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterUpgrade',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Checkbox, ModalWithCard, LabeledSelect, Banner, UpgradeInfo
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const res = await allHash({
|
||||||
|
upgradeVersion: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }),
|
||||||
|
versions: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VERSION }),
|
||||||
|
upgrade: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.UPGRADE }),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.upgrade = res.upgrade;
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
upgrade: [],
|
||||||
|
upgradeMessage: [],
|
||||||
|
errors: '',
|
||||||
|
selectMode: true,
|
||||||
|
version: '',
|
||||||
|
enableLogging: true,
|
||||||
|
readyReleaseNote: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['currentCluster']),
|
||||||
|
|
||||||
|
versionOptions() {
|
||||||
|
const versions = this.$store.getters['harvester/all'](HCI.VERSION);
|
||||||
|
|
||||||
|
return versions.map(V => V.metadata.name);
|
||||||
|
},
|
||||||
|
|
||||||
|
currentVersion() {
|
||||||
|
const serverVersion = this.$store.getters['harvester/byId'](HCI.SETTING, 'server-version');
|
||||||
|
|
||||||
|
return serverVersion.currentVersion || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
canEnableLogging() {
|
||||||
|
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
|
||||||
|
},
|
||||||
|
|
||||||
|
releaseLink() {
|
||||||
|
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
upgrade: {
|
||||||
|
handler(neu) {
|
||||||
|
let upgradeMessage = [];
|
||||||
|
const list = neu || [];
|
||||||
|
|
||||||
|
const currentResource = list.find( O => !!O.isLatestUpgrade);
|
||||||
|
|
||||||
|
upgradeMessage = currentResource ? currentResource.upgradeMessage : [];
|
||||||
|
|
||||||
|
this.$set(this, 'upgradeMessage', upgradeMessage);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
|
||||||
|
version() {
|
||||||
|
this.readyReleaseNote = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async handleUpgrade() {
|
||||||
|
const upgradeValue = {
|
||||||
|
type: HCI.UPGRADE,
|
||||||
|
metadata: {
|
||||||
|
generateName: 'hvst-upgrade-',
|
||||||
|
namespace: 'harvester-system'
|
||||||
|
},
|
||||||
|
spec: { version: this.version }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.canEnableLogging) {
|
||||||
|
upgradeValue.spec.logEnabled = this.enableLogging;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyResource = await this.$store.dispatch('harvester/create', upgradeValue);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await proxyResource.save();
|
||||||
|
|
||||||
|
this.cancel();
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.message !== '') {
|
||||||
|
this.errors = err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.$refs.deleteTip.hide();
|
||||||
|
this.errors = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.$refs.deleteTip.open();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="currentCluster">
|
||||||
|
<header class="header-layout header mb-0">
|
||||||
|
<h1>
|
||||||
|
<t
|
||||||
|
k="harvester.dashboard.header"
|
||||||
|
:cluster="currentCluster.nameDisplay"
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
<button v-if="versionOptions.length" type="button" class="btn bg-warning btn-sm" @click="open">
|
||||||
|
<t k="harvester.upgradePage.upgrade" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ModalWithCard ref="deleteTip" name="deleteTip" :width="850">
|
||||||
|
<template #title>
|
||||||
|
<t k="harvester.upgradePage.upgradeApp" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<UpgradeInfo :version="version" />
|
||||||
|
|
||||||
|
<div class="currentVersion mb-15">
|
||||||
|
<span> <t k="harvester.upgradePage.currentVersion" /> </span>
|
||||||
|
<span class="version">{{ currentVersion }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="version"
|
||||||
|
class="mb-10"
|
||||||
|
:label="t('harvester.upgradePage.versionLabel')"
|
||||||
|
:options="versionOptions"
|
||||||
|
:clearable="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="canEnableLogging" class="mb-5">
|
||||||
|
<Checkbox v-model="enableLogging" class="check" type="checkbox" :label="t('harvester.upgradePage.enableLogging')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="version">
|
||||||
|
<p v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)" class="mb-10"></p>
|
||||||
|
|
||||||
|
<Checkbox v-model="readyReleaseNote" class="check" type="checkbox" label-key="harvester.upgradePage.checkReady" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-if="errors.length" color="warning">
|
||||||
|
{{ errors }}
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="footer">
|
||||||
|
<button class="btn role-secondary mr-20" @click.prevent="cancel">
|
||||||
|
<t k="generic.close" />
|
||||||
|
</button>
|
||||||
|
<button :disabled="!readyReleaseNote" class="btn role-tertiary bg-primary" @click.prevent="handleUpgrade">
|
||||||
|
<t k="harvester.upgradePage.upgrade" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ModalWithCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
line-height: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currentVersion {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
.version {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
348
pkg/harvester/components/HarvesterUpgradeHeader.vue
Normal file
348
pkg/harvester/components/HarvesterUpgradeHeader.vue
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
<script>
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
|
import BadgeStateFormatter from '@shell/components/formatter/BadgeStateFormatter';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { PRODUCT_NAME as HARVESTER } from '../config/harvester';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import ProgressBarList from './HarvesterUpgradeProgressBarList';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterUpgradeHeader',
|
||||||
|
components: {
|
||||||
|
PercentageBar, ProgressBarList, BadgeStateFormatter
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const hash = {};
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](HCI.IMAGE)) {
|
||||||
|
hash.images = this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](HCI.UPGRADE)) {
|
||||||
|
hash.upgrades = this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](NODE)) {
|
||||||
|
hash.nodes = this.$store.dispatch('harvester/findAll', { type: NODE });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG)) {
|
||||||
|
hash.upgradeLogs = this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE_LOG });
|
||||||
|
}
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
filename: '',
|
||||||
|
logDownloading: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['currentProduct', 'isVirtualCluster']),
|
||||||
|
|
||||||
|
enabled() {
|
||||||
|
return this.isVirtualCluster && this.currentProduct.name === HARVESTER;
|
||||||
|
},
|
||||||
|
|
||||||
|
latestResource() {
|
||||||
|
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( U => U.isLatestUpgrade);
|
||||||
|
},
|
||||||
|
|
||||||
|
latestUpgradeLogResource() {
|
||||||
|
const upgradeLogId = `${ this.latestResource.id }-upgradelog`;
|
||||||
|
|
||||||
|
return this.$store.getters['harvester/all'](HCI.UPGRADE_LOG).find( U => U.id === upgradeLogId);
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadLogFailReason() {
|
||||||
|
if (!this.filename) {
|
||||||
|
const filename = this.latestUpgradeLogResource?.latestArchivesFileName;
|
||||||
|
|
||||||
|
return this.latestUpgradeLogResource?.downloadArchivesStatus(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.latestUpgradeLogResource?.downloadArchivesStatus(this.filename);
|
||||||
|
},
|
||||||
|
|
||||||
|
canStartedDownload() {
|
||||||
|
return this.latestUpgradeLogResource?.canStartedDownload || false;
|
||||||
|
},
|
||||||
|
|
||||||
|
overallMessage() {
|
||||||
|
return this.latestResource?.overallMessage;
|
||||||
|
},
|
||||||
|
|
||||||
|
upgradeImage() {
|
||||||
|
const id = this.latestResource?.upgradeImage;
|
||||||
|
|
||||||
|
return this.$store.getters['harvester/all'](HCI.IMAGE).find(I => I.id === id);
|
||||||
|
},
|
||||||
|
|
||||||
|
imageProgress() {
|
||||||
|
return this.upgradeImage?.progress || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
showImage() {
|
||||||
|
return !this.latestResource.isUpgradeSucceeded;
|
||||||
|
},
|
||||||
|
|
||||||
|
imageMessage() {
|
||||||
|
return this.latestResource?.upgradeImageMessage;
|
||||||
|
},
|
||||||
|
|
||||||
|
repoReady() {
|
||||||
|
return this.latestResource.createRepo;
|
||||||
|
},
|
||||||
|
|
||||||
|
isShow() {
|
||||||
|
return this.latestResource && !this.latestResource.hasReadMessage;
|
||||||
|
},
|
||||||
|
|
||||||
|
nodesStatus() {
|
||||||
|
return this.latestResource?.nodeUpgradeMessage;
|
||||||
|
},
|
||||||
|
|
||||||
|
sysServiceUpgradeMessage() {
|
||||||
|
return this.latestResource?.sysServiceUpgradeMessage;
|
||||||
|
},
|
||||||
|
|
||||||
|
sysServiceTotal() {
|
||||||
|
return this.sysServiceUpgradeMessage?.[0].percent || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
nodesPercent() {
|
||||||
|
return this.latestResource?.nodeTotalPercent || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
repoInfo() {
|
||||||
|
return this.latestResource.repoInfo;
|
||||||
|
},
|
||||||
|
|
||||||
|
releaseLink() {
|
||||||
|
return `https://github.com/harvester/harvester/releases/tag/${ this.latestResource?.spec?.version }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
upgradeVersion() {
|
||||||
|
return this.latestResource?.spec?.version;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
ignoreMessage() {
|
||||||
|
this.latestResource.setLabel(HCI_ANNOTATIONS.REAY_MESSAGE, 'true');
|
||||||
|
this.latestResource.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
async generateLogFileName() {
|
||||||
|
const res = await this.latestUpgradeLogResource.doActionGrowl('generate');
|
||||||
|
|
||||||
|
this.filename = res?.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
waitFileGeneratedReady() {
|
||||||
|
const id = this.latestUpgradeLogResource.id;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let log;
|
||||||
|
|
||||||
|
const timer = setInterval(async() => {
|
||||||
|
log = await this.$store.dispatch('harvester/find', {
|
||||||
|
type: HCI.UPGRADE_LOG,
|
||||||
|
id,
|
||||||
|
opt: { force: true }
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
if (log.fileIsReady(this.filename)) {
|
||||||
|
this.logDownloading = false;
|
||||||
|
clearInterval(timer);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 2500);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadLog() {
|
||||||
|
this.logDownloading = true;
|
||||||
|
await this.generateLogFileName();
|
||||||
|
this.waitFileGeneratedReady().then(() => {
|
||||||
|
if (!this.downloadLogFailReason) {
|
||||||
|
this.latestUpgradeLogResource.downloadLog(this.filename);
|
||||||
|
}
|
||||||
|
this.logDownloading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="enabled && isShow" class="upgrade">
|
||||||
|
<v-popover
|
||||||
|
v-clean-tooltip="{
|
||||||
|
placement: 'bottom-left',
|
||||||
|
}"
|
||||||
|
class="hand"
|
||||||
|
>
|
||||||
|
<slot name="button-content">
|
||||||
|
<i class="warning icon-fw icon icon-dot-open dot-icon" />
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<template slot="popover">
|
||||||
|
<div class="upgrade-info mb-10">
|
||||||
|
<div v-if="repoInfo" class="repoInfo">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<a :href="releaseLink" target="_blank">{{ upgradeVersion }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="latestResource" class="row mb-5">
|
||||||
|
<div class="col span-12">
|
||||||
|
<p class="state">
|
||||||
|
{{ t('harvester.upgradePage.repoInfo.upgradeStatus') }}: <BadgeStateFormatter class="ml-5" :row="latestResource" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="downloadLogFailReason" class="row mb-5">
|
||||||
|
<div class="col span-12">
|
||||||
|
<p class="state">
|
||||||
|
{{ t('harvester.upgradePage.repoInfo.logStatus') }}: <span class="error ml-5">{{ downloadLogFailReason }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="bordered-section"></p>
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col span-6">
|
||||||
|
{{ t('harvester.upgradePage.repoInfo.os') }}: <span class="text-muted">{{ repoInfo.release.os }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
{{ t('harvester.productLabel') }}: <span class="text-muted">{{ repoInfo.release.harvester }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col span-6">
|
||||||
|
{{ t('harvester.upgradePage.repoInfo.harvesterChart') }}: <span class="text-muted">{{ repoInfo.release.harvesterChart }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
{{ t('harvester.upgradePage.repoInfo.monitoringChart') }}: <span class="text-muted">{{ repoInfo.release.monitoringChart }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col span-6">
|
||||||
|
{{ t('harvester.upgradePage.repoInfo.kubernetes') }}: <span class="text-muted">{{ repoInfo.release.kubernetes }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
{{ t('product.rancher') }}: <span class="text-muted">{{ repoInfo.release.rancher }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="bordered-section"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="overallMessage" class="text-warning mb-20">
|
||||||
|
{{ overallMessage }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="showImage">
|
||||||
|
<h4>{{ t('harvester.upgradePage.upgradeImage') }}<span class="float-r text-info">{{ imageProgress }}%</span></h4>
|
||||||
|
<PercentageBar :value="imageProgress" preferred-direction="MORE" />
|
||||||
|
<p class="text-warning">
|
||||||
|
{{ imageMessage }}
|
||||||
|
</p>
|
||||||
|
<p class="bordered-section"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>{{ t('harvester.upgradePage.createRepository') }} <span class="float-r text-info">{{ repoReady.isReady ? t('harvester.upgradePage.succeeded') : t('harvester.upgradePage.pending') }}</span></h4>
|
||||||
|
<p v-if="repoReady.message" class="text-warning">
|
||||||
|
{{ repoReady.message }}
|
||||||
|
</p>
|
||||||
|
<p class="bordered-section"></p>
|
||||||
|
|
||||||
|
<ProgressBarList :title="t('harvester.upgradePage.upgradeNode')" :precent="nodesPercent" :list="nodesStatus" />
|
||||||
|
<p class="bordered-section"></p>
|
||||||
|
|
||||||
|
<ProgressBarList :title="t('harvester.upgradePage.upgradeSysService')" :precent="sysServiceTotal" :list="sysServiceUpgradeMessage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<button v-if="canStartedDownload" :disabled="logDownloading" class="btn role-primary mr-10" @click="downloadLog()">
|
||||||
|
<i class="icon mr-10" :class="[logDownloading ? 'icon-spinner icon-spin' : 'icon-download']"></i> {{ t('harvester.upgradePage.repoInfo.downloadLog') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="latestResource.isUpgradeSucceeded" class="btn role-primary" @click="ignoreMessage()">
|
||||||
|
{{ t('harvester.upgradePage.dismissMessage') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
a {
|
||||||
|
float: right;
|
||||||
|
color: var(--link) !important;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade {
|
||||||
|
height: 100%;
|
||||||
|
min-width: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.dot-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: #00a483;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-info {
|
||||||
|
min-width: 550px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
.repoInfo {
|
||||||
|
.col span {
|
||||||
|
word-break: break-all
|
||||||
|
}
|
||||||
|
|
||||||
|
p.state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-r {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
word-break: break-word;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
109
pkg/harvester/components/HarvesterUpgradeProgressBarList.vue
Normal file
109
pkg/harvester/components/HarvesterUpgradeProgressBarList.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<script>
|
||||||
|
import Collapse from '@shell/components/Collapse';
|
||||||
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterUpgradeProgressList',
|
||||||
|
components: { PercentageBar, Collapse },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
precent: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
list: {
|
||||||
|
type: Array,
|
||||||
|
default: () => {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { open: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleSwitch() {
|
||||||
|
this.open = !this.open;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bar-list">
|
||||||
|
<h4>{{ title }} <span class="float-r text-info">{{ precent }}%</span></h4>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Collapse :open.sync="open">
|
||||||
|
<template #title>
|
||||||
|
<div class="total-bar">
|
||||||
|
<span class="bar"><PercentageBar :value="precent" preferred-direction="MORE" /></span>
|
||||||
|
<span class="on-off" @click="handleSwitch"> {{ open ? t('harvester.generic.close') : t('harvester.generic.open') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="custom-content">
|
||||||
|
<div v-for="item in list" :key="item.name" class="item">
|
||||||
|
<p>{{ item.name }} <span class="status" :class="{ [item.state]: true }">{{ item.state }}</span></p>
|
||||||
|
<PercentageBar :value="item.percent" preferred-direction="MORE" />
|
||||||
|
<p class="warning">
|
||||||
|
{{ item.message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bar-list {
|
||||||
|
.float-r {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-bar {
|
||||||
|
display: flex;
|
||||||
|
user-select: none;
|
||||||
|
.bar {
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
.on-off {
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.custom-content {
|
||||||
|
.item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
p {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.Succeeded, .Upgrading, .Pending {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
.failed {
|
||||||
|
color: var(--error)
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
42
pkg/harvester/components/UpgradeInfo.vue
Normal file
42
pkg/harvester/components/UpgradeInfo.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script>
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterUpgradeInfo',
|
||||||
|
|
||||||
|
components: { Banner },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
version: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
releaseVersion() {
|
||||||
|
return !!this.version ? `https://github.com/harvester/harvester/releases/tag/${ this.version }` : `https://github.com/harvester/harvester/releases`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner color="warning">
|
||||||
|
<div>
|
||||||
|
<strong>{{ t('harvester.upgradePage.upgradeInfo.warning') }}:</strong>
|
||||||
|
<p v-clean-html="t('harvester.upgradePage.upgradeInfo.doc', {}, true)" class="mb-5">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-5">
|
||||||
|
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-5">
|
||||||
|
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a :href="releaseVersion" target="_blank">{{ t('generic.moreInfo') }} </a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,6 +1,9 @@
|
|||||||
import { importTypes } from '@rancher/auto-import';
|
import { importTypes } from '@rancher/auto-import';
|
||||||
import { IPlugin } from '@shell/core/types';
|
import { IPlugin } from '@shell/core/types';
|
||||||
import extensionRouting from './routing/extension-routing';
|
import extensionRouting from './routing/extension-routing';
|
||||||
|
import harvesterCommonStore from './store/harvester-common';
|
||||||
|
import harvesterStore from './store/harvester-store';
|
||||||
|
import customValidators from './validators';
|
||||||
|
|
||||||
// Init the package
|
// Init the package
|
||||||
export default function(plugin: IPlugin): void {
|
export default function(plugin: IPlugin): void {
|
||||||
@ -15,4 +18,11 @@ export default function(plugin: IPlugin): void {
|
|||||||
|
|
||||||
// Add Vue Routes
|
// Add Vue Routes
|
||||||
plugin.addRoutes(extensionRouting);
|
plugin.addRoutes(extensionRouting);
|
||||||
|
|
||||||
|
plugin.addDashboardStore(harvesterCommonStore.config.namespace, harvesterCommonStore.specifics, harvesterCommonStore.config);
|
||||||
|
plugin.addDashboardStore(harvesterStore.config.namespace, harvesterStore.specifics, harvesterStore.config, harvesterStore.init);
|
||||||
|
|
||||||
|
// plugin.validators = customValidators;
|
||||||
|
|
||||||
|
plugin.register('component', 'NavHeaderRight', () => import(/* webpackChunkName: "pkg/harvester/components" */ `./components/HarvesterUpgradeHeader.vue`));
|
||||||
}
|
}
|
||||||
|
|||||||
840
pkg/harvester/list/harvesterhci.io.dashboard.vue
Normal file
840
pkg/harvester/list/harvesterhci.io.dashboard.vue
Normal file
@ -0,0 +1,840 @@
|
|||||||
|
<script>
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import minMax from 'dayjs/plugin/minMax';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
import { allHash, setPromiseResult } from '@shell/utils/promise';
|
||||||
|
import {
|
||||||
|
parseSi, formatSi, exponentNeeded, UNITS, createMemoryValues
|
||||||
|
} from '@shell/utils/units';
|
||||||
|
import { REASON } from '@shell/config/table-headers';
|
||||||
|
import {
|
||||||
|
EVENT, METRIC, NODE, SERVICE, PVC, LONGHORN, POD, COUNT, NETWORK_ATTACHMENT
|
||||||
|
} from '@shell/config/types';
|
||||||
|
import ResourceSummary, { resourceCounts, colorToCountName } from '@shell/components/ResourceSummary';
|
||||||
|
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
|
||||||
|
import HardwareResourceGauge from '@shell/components/HardwareResourceGauge';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import DashboardMetrics from '@shell/components/DashboardMetrics';
|
||||||
|
import metricPoller from '@shell/mixins/metric-poller';
|
||||||
|
import { allDashboardsExist } from '@shell/utils/grafana';
|
||||||
|
import { isEmpty } from '@shell/utils/object';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import HarvesterUpgrade from '../components/HarvesterUpgrade.vue';
|
||||||
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(minMax);
|
||||||
|
|
||||||
|
const PARSE_RULES = {
|
||||||
|
memory: {
|
||||||
|
format: {
|
||||||
|
addSuffix: true,
|
||||||
|
firstSuffix: 'B',
|
||||||
|
increment: 1024,
|
||||||
|
maxExponent: 99,
|
||||||
|
maxPrecision: 2,
|
||||||
|
minExponent: 0,
|
||||||
|
startingExponent: 0,
|
||||||
|
suffix: 'iB',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const RESOURCES = [{
|
||||||
|
type: NODE,
|
||||||
|
spoofed: {
|
||||||
|
location: {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.HOST }
|
||||||
|
},
|
||||||
|
name: HCI.HOST,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: HCI.VM,
|
||||||
|
spoofed: {
|
||||||
|
location: {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VM }
|
||||||
|
},
|
||||||
|
name: HCI.VM,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NETWORK_ATTACHMENT,
|
||||||
|
spoofed: {
|
||||||
|
location: {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.NETWORK_ATTACHMENT }
|
||||||
|
},
|
||||||
|
name: HCI.NETWORK_ATTACHMENT,
|
||||||
|
filterNamespace: ['harvester-system']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: HCI.IMAGE,
|
||||||
|
spoofed: {
|
||||||
|
location: {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.IMAGE }
|
||||||
|
},
|
||||||
|
name: HCI.IMAGE,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: PVC,
|
||||||
|
spoofed: {
|
||||||
|
location: {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VOLUME }
|
||||||
|
},
|
||||||
|
name: HCI.VOLUME,
|
||||||
|
filterNamespace: ['cattle-monitoring-system']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: HCI.BLOCK_DEVICE,
|
||||||
|
spoofed: {
|
||||||
|
location: {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.HOST }
|
||||||
|
},
|
||||||
|
name: HCI.BLOCK_DEVICE,
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
const CLUSTER_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-cluster-nodes-1/rancher-cluster-nodes?orgId=1';
|
||||||
|
const CLUSTER_METRICS_SUMMARY_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-cluster-1/rancher-cluster?orgId=1';
|
||||||
|
const VM_DASHBOARD_METRICS_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-dashboard-1/vm-dashboard?orgId=1';
|
||||||
|
|
||||||
|
const MONITORING_ID = 'cattle-monitoring-system/rancher-monitoring';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [metricPoller],
|
||||||
|
components: {
|
||||||
|
Loading,
|
||||||
|
HardwareResourceGauge,
|
||||||
|
SortableTable,
|
||||||
|
HarvesterUpgrade,
|
||||||
|
ResourceSummary,
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
DashboardMetrics,
|
||||||
|
Banner,
|
||||||
|
MessageLink,
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = {
|
||||||
|
vms: this.fetchClusterResources(HCI.VM),
|
||||||
|
nodes: this.fetchClusterResources(NODE),
|
||||||
|
events: this.fetchClusterResources(EVENT),
|
||||||
|
metricNodes: this.fetchClusterResources(METRIC.NODE),
|
||||||
|
settings: this.fetchClusterResources(HCI.SETTING),
|
||||||
|
services: this.fetchClusterResources(SERVICE),
|
||||||
|
metric: this.fetchClusterResources(METRIC.NODE),
|
||||||
|
longhornNode: this.fetchClusterResources(LONGHORN.NODES) || [],
|
||||||
|
_pods: this.$store.dispatch('harvester/findAll', { type: POD }),
|
||||||
|
};
|
||||||
|
|
||||||
|
(this.accessibleResources || []).map((a) => {
|
||||||
|
hash[a.type] = this.$store.dispatch(`${ inStore }/findAll`, { type: a.type });
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS)) {
|
||||||
|
hash.addons = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await allHash(hash);
|
||||||
|
|
||||||
|
for ( const k in res ) {
|
||||||
|
this[k] = res[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
setPromiseResult(
|
||||||
|
allDashboardsExist(this.$store, this.currentCluster.id, [CLUSTER_METRICS_DETAIL_URL, CLUSTER_METRICS_SUMMARY_URL], 'harvester'),
|
||||||
|
this,
|
||||||
|
'showClusterMetrics',
|
||||||
|
'Determine cluster metrics'
|
||||||
|
);
|
||||||
|
setPromiseResult(
|
||||||
|
allDashboardsExist(this.$store, this.currentCluster.id, [VM_DASHBOARD_METRICS_URL], 'harvester'),
|
||||||
|
this,
|
||||||
|
'showVmMetrics',
|
||||||
|
'Determine vm metrics'
|
||||||
|
);
|
||||||
|
|
||||||
|
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||||
|
|
||||||
|
this.monitoring = addons.find(addon => addon.id === MONITORING_ID);
|
||||||
|
this.enabledMonitoringAddon = this.monitoring?.spec?.enabled;
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const reason = {
|
||||||
|
...REASON,
|
||||||
|
...{ canBeVariable: true },
|
||||||
|
width: 130
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventHeaders = [
|
||||||
|
reason,
|
||||||
|
{
|
||||||
|
name: 'resource',
|
||||||
|
label: 'Resource',
|
||||||
|
labelKey: 'clusterIndexPage.sections.events.resource.label',
|
||||||
|
value: 'displayInvolvedObject',
|
||||||
|
sort: ['involvedObject.kind', 'involvedObject.name'],
|
||||||
|
canBeVariable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'right',
|
||||||
|
name: 'date',
|
||||||
|
label: 'Date',
|
||||||
|
labelKey: 'clusterIndexPage.sections.events.date.label',
|
||||||
|
value: 'lastTimestamp',
|
||||||
|
sort: 'lastTimestamp:desc',
|
||||||
|
formatter: 'LiveDate',
|
||||||
|
formatterOpts: { addSuffix: true },
|
||||||
|
width: 125,
|
||||||
|
defaultSort: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventHeaders,
|
||||||
|
constraints: [],
|
||||||
|
events: [],
|
||||||
|
nodeMetrics: [],
|
||||||
|
nodes: [],
|
||||||
|
metricNodes: [],
|
||||||
|
vms: [],
|
||||||
|
monitoring: {},
|
||||||
|
VM_DASHBOARD_METRICS_URL,
|
||||||
|
CLUSTER_METRICS_SUMMARY_URL,
|
||||||
|
CLUSTER_METRICS_DETAIL_URL,
|
||||||
|
showClusterMetrics: false,
|
||||||
|
showVmMetrics: false,
|
||||||
|
enabledMonitoringAddon: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['currentCluster']),
|
||||||
|
|
||||||
|
accessibleResources() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return RESOURCES.filter(resource => this.$store.getters[`${ inStore }/schemaFor`](resource.type));
|
||||||
|
},
|
||||||
|
|
||||||
|
totalCountGaugeInput() {
|
||||||
|
const out = {};
|
||||||
|
|
||||||
|
this.accessibleResources.forEach((resource) => {
|
||||||
|
const counts = resourceCounts(this.$store, resource.type);
|
||||||
|
|
||||||
|
out[resource.type] = { resource: resource.type };
|
||||||
|
|
||||||
|
Object.entries(counts).forEach((entry) => {
|
||||||
|
out[resource.type][entry[0]] = entry[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resource.spoofed) {
|
||||||
|
if (resource.spoofed?.filterNamespace && Array.isArray(resource.spoofed.filterNamespace)) {
|
||||||
|
const clusterCounts = this.$store.getters['harvester/all'](COUNT)[0].counts;
|
||||||
|
const statistics = clusterCounts[resource.type] || {};
|
||||||
|
|
||||||
|
for (let i = 0; i < resource.spoofed.filterNamespace.length; i++) {
|
||||||
|
const nsStatistics = statistics?.namespaces?.[resource.spoofed.filterNamespace[i]] || {};
|
||||||
|
|
||||||
|
if (nsStatistics.count) {
|
||||||
|
out[resource.type]['useful'] -= nsStatistics.count;
|
||||||
|
out[resource.type]['total'] -= nsStatistics.count;
|
||||||
|
}
|
||||||
|
Object.entries(nsStatistics?.states || {}).forEach((entry) => {
|
||||||
|
const color = colorForState(entry[0]);
|
||||||
|
const count = entry[1];
|
||||||
|
const countName = colorToCountName(color);
|
||||||
|
|
||||||
|
out[resource.type]['useful'] -= count;
|
||||||
|
out[resource.type][countName] += count;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out[resource.type] = {
|
||||||
|
...out[resource.type],
|
||||||
|
...resource.spoofed,
|
||||||
|
isSpoofed: true
|
||||||
|
};
|
||||||
|
|
||||||
|
out[resource.type].name = this.t(`typeLabel."${ resource.spoofed.name }"`, { count: out[resource.type].total });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource.type === HCI.BLOCK_DEVICE) {
|
||||||
|
let total = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
(this.nodes || []).map((node) => {
|
||||||
|
total += node.diskStatusCount?.total;
|
||||||
|
errorCount += node.diskStatusCount?.errorCount;
|
||||||
|
});
|
||||||
|
|
||||||
|
out[resource.type] = {
|
||||||
|
...out[resource.type],
|
||||||
|
total,
|
||||||
|
errorCount,
|
||||||
|
useful: total - errorCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
currentVersion() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const settings = this.$store.getters[`${ inStore }/all`](HCI.SETTING);
|
||||||
|
const setting = settings.find( S => S.id === 'server-version');
|
||||||
|
|
||||||
|
return setting?.value || setting?.default;
|
||||||
|
},
|
||||||
|
|
||||||
|
firstNodeCreationTimestamp() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const days = this.$store.getters[`${ inStore }/all`](NODE).map( (N) => {
|
||||||
|
return dayjs(N.metadata.creationTimestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!days.length) {
|
||||||
|
return dayjs().utc().format();
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs.min(days).utc().format();
|
||||||
|
},
|
||||||
|
|
||||||
|
cpusTotal() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
this.metricNodes.forEach((node) => {
|
||||||
|
out += node.cpuCapacity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
cpusUsageTotal() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
this.metricNodes.forEach((node) => {
|
||||||
|
out += node.cpuUsage;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
memoryTotal() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
this.metricNodes.forEach((node) => {
|
||||||
|
out += node.memoryCapacity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
memoryUsageTotal() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
this.metricNodes.forEach((node) => {
|
||||||
|
out += node.memoryUsage;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
storageUsage() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const longhornNodes = this.$store.getters[`${ inStore }/all`](LONGHORN.NODES) || [];
|
||||||
|
|
||||||
|
return longhornNodes.filter(node => node.spec?.allowScheduling).reduce((total, node) => {
|
||||||
|
return total + node.used;
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
storageReservedTotal() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
(this.longhornNode || []).filter(node => node.spec?.allowScheduling).forEach((node) => {
|
||||||
|
const disks = node?.spec?.disks || {};
|
||||||
|
|
||||||
|
Object.values(disks).map((disk) => {
|
||||||
|
if (disk.allowScheduling) {
|
||||||
|
out += disk.storageReserved;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
storageTotal() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
(this.longhornNode || []).forEach((node) => {
|
||||||
|
const diskStatus = node?.status?.diskStatus || {};
|
||||||
|
|
||||||
|
Object.values(diskStatus).map((disk) => {
|
||||||
|
if (disk?.storageMaximum) {
|
||||||
|
out += disk.storageMaximum;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
storageUsed() {
|
||||||
|
return this.createMemoryValues(this.storageTotal, this.storageUsage);
|
||||||
|
},
|
||||||
|
|
||||||
|
storageReserved() {
|
||||||
|
return this.createMemoryValues(this.storageTotal, this.storageReservedTotal);
|
||||||
|
},
|
||||||
|
|
||||||
|
vmEvents() {
|
||||||
|
return this.events.filter( E => ['VirtualMachineInstance', 'VirtualMachine'].includes(E.involvedObject.kind));
|
||||||
|
},
|
||||||
|
|
||||||
|
volumeEvents() {
|
||||||
|
return this.events.filter( E => ['PersistentVolumeClaim'].includes(E.involvedObject.kind));
|
||||||
|
},
|
||||||
|
|
||||||
|
hostEvents() {
|
||||||
|
return this.events.filter( E => ['Node'].includes(E.involvedObject.kind));
|
||||||
|
},
|
||||||
|
|
||||||
|
imageEvents() {
|
||||||
|
return this.events.filter( E => ['VirtualMachineImage'].includes(E.involvedObject.kind));
|
||||||
|
},
|
||||||
|
|
||||||
|
hasMetricsTabs() {
|
||||||
|
return this.showClusterMetrics || this.showVmMetrics;
|
||||||
|
},
|
||||||
|
|
||||||
|
pods() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const pods = this.$store.getters[`${ inStore }/all`](POD) || [];
|
||||||
|
|
||||||
|
return pods.filter(p => p?.metadata?.name !== 'removing');
|
||||||
|
},
|
||||||
|
|
||||||
|
cpuReserved() {
|
||||||
|
const useful = this.nodes.reduce((total, node) => {
|
||||||
|
return total + node.cpuReserved;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: this.cpusTotal,
|
||||||
|
useful,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
ramReserved() {
|
||||||
|
const useful = this.nodes.reduce((total, node) => {
|
||||||
|
return total + node.memoryReserved;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return createMemoryValues(this.memoryTotal, useful);
|
||||||
|
},
|
||||||
|
|
||||||
|
availableNodes() {
|
||||||
|
return (this.metricNodes || []).map(node => node.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
metricAggregations() {
|
||||||
|
const nodes = this.nodes;
|
||||||
|
const someNonWorkerRoles = this.nodes.some(node => node.hasARole && !node.isWorker);
|
||||||
|
const metrics = this.nodeMetrics.filter((nodeMetrics) => {
|
||||||
|
const node = nodes.find(nd => nd.id === nodeMetrics.id);
|
||||||
|
|
||||||
|
return node && (!someNonWorkerRoles || node.isWorker);
|
||||||
|
});
|
||||||
|
const initialAggregation = {
|
||||||
|
cpu: 0,
|
||||||
|
memory: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEmpty(metrics)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics.reduce((agg, metric) => {
|
||||||
|
agg.cpu += parseSi(metric.usage.cpu);
|
||||||
|
agg.memory += parseSi(metric.usage.memory);
|
||||||
|
|
||||||
|
return agg;
|
||||||
|
}, initialAggregation);
|
||||||
|
},
|
||||||
|
|
||||||
|
cpuUsed() {
|
||||||
|
return {
|
||||||
|
total: this.cpusTotal,
|
||||||
|
useful: this.metricAggregations?.cpu,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
ramUsed() {
|
||||||
|
return createMemoryValues(this.memoryTotal, this.metricAggregations?.memory);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasMetricNodeSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](METRIC.NODE);
|
||||||
|
},
|
||||||
|
|
||||||
|
toEnableMonitoringAddon() {
|
||||||
|
return `${ HCI.ADD_ONS }/cattle-monitoring-system/rancher-monitoring?mode=edit#alertmanager`;
|
||||||
|
},
|
||||||
|
|
||||||
|
canEnableMonitoringAddon() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const hasSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
|
||||||
|
|
||||||
|
return hasSchema && this.monitoring;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
createMemoryValues(total, useful) {
|
||||||
|
const parsedTotal = parseSi((total || '0').toString());
|
||||||
|
const parsedUseful = parseSi((useful || '0').toString());
|
||||||
|
const format = this.createMemoryFormat(parsedTotal);
|
||||||
|
const formattedTotal = formatSi(parsedTotal, format);
|
||||||
|
let formattedUseful = formatSi(parsedUseful, {
|
||||||
|
...format,
|
||||||
|
addSuffix: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Number.parseFloat(formattedUseful) > 0) {
|
||||||
|
formattedUseful = formatSi(parsedUseful, {
|
||||||
|
...format,
|
||||||
|
canRoundToZero: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: Number(parsedTotal),
|
||||||
|
useful: Number(parsedUseful),
|
||||||
|
formattedTotal,
|
||||||
|
formattedUseful,
|
||||||
|
units: this.createMemoryUnits(parsedTotal),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
createMemoryFormat(n) {
|
||||||
|
const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...PARSE_RULES.memory.format,
|
||||||
|
maxExponent: exponent,
|
||||||
|
minExponent: exponent,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
createMemoryUnits(n) {
|
||||||
|
const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
|
||||||
|
|
||||||
|
return `${ UNITS[exponent] }${ PARSE_RULES.memory.format.suffix }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchClusterResources(type, opt = {}, store) {
|
||||||
|
const inStore = store || this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const schema = this.$store.getters[`${ inStore }/schemaFor`](type);
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
|
try {
|
||||||
|
const resources = await this.$store.dispatch(`${ inStore }/findAll`, { type, opt });
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed fetching cluster resource ${ type } with error:`, err); // eslint-disable-line no-console
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadMetrics() {
|
||||||
|
this.nodeMetrics = await this.fetchClusterResources(METRIC.NODE, { force: true } );
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending || !currentCluster" />
|
||||||
|
<section v-else>
|
||||||
|
<HarvesterUpgrade />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="cluster-dashboard-glance"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
{{ t('harvester.dashboard.version') }}:
|
||||||
|
</label>
|
||||||
|
<span>
|
||||||
|
<span v-clean-tooltip="{content: currentVersion}">
|
||||||
|
{{ currentVersion }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
{{ t('glance.created') }}:
|
||||||
|
</label>
|
||||||
|
<span>
|
||||||
|
<LiveDate
|
||||||
|
:value="firstNodeCreationTimestamp"
|
||||||
|
:add-suffix="true"
|
||||||
|
:show-tooltip="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!enabledMonitoringAddon && canEnableMonitoringAddon">
|
||||||
|
<Banner color="info">
|
||||||
|
<MessageLink
|
||||||
|
:to="toEnableMonitoringAddon"
|
||||||
|
prefix-label="harvester.monitoring.alertmanagerConfig.disabledAddon.prefix"
|
||||||
|
middle-label="harvester.monitoring.alertmanagerConfig.disabledAddon.middle"
|
||||||
|
suffix-label="harvester.monitoring.alertmanagerConfig.disabledAddon.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resource-gauges">
|
||||||
|
<ResourceSummary
|
||||||
|
v-for="resource in totalCountGaugeInput"
|
||||||
|
:key="resource.resource"
|
||||||
|
:spoofed-counts="resource.isSpoofed ? resource : null"
|
||||||
|
:resource="resource.resource"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="nodes.length && hasMetricNodeSchema">
|
||||||
|
<h3 class="mt-40">
|
||||||
|
{{ t('clusterIndexPage.sections.capacity.label') }}
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
class="hardware-resource-gauges"
|
||||||
|
:class="{
|
||||||
|
live: !storageTotal,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<HardwareResourceGauge
|
||||||
|
:name="t('harvester.dashboard.hardwareResourceGauge.cpu')"
|
||||||
|
:reserved="cpuReserved"
|
||||||
|
:used="cpuUsed"
|
||||||
|
/>
|
||||||
|
<HardwareResourceGauge
|
||||||
|
:name="t('harvester.dashboard.hardwareResourceGauge.memory')"
|
||||||
|
:reserved="ramReserved"
|
||||||
|
:used="ramUsed"
|
||||||
|
/>
|
||||||
|
<HardwareResourceGauge
|
||||||
|
v-if="storageTotal"
|
||||||
|
:name="t('harvester.dashboard.hardwareResourceGauge.storage')"
|
||||||
|
:used="storageUsed"
|
||||||
|
:reserved="storageReserved"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Tabbed
|
||||||
|
v-if="hasMetricsTabs && enabledMonitoringAddon"
|
||||||
|
class="mt-30"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
v-if="showClusterMetrics"
|
||||||
|
name="cluster-metrics"
|
||||||
|
:label="t('clusterIndexPage.sections.clusterMetrics.label')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<template #default="props">
|
||||||
|
<DashboardMetrics
|
||||||
|
v-if="props.active"
|
||||||
|
:detail-url="CLUSTER_METRICS_DETAIL_URL"
|
||||||
|
:summary-url="CLUSTER_METRICS_SUMMARY_URL"
|
||||||
|
graph-height="825px"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="showVmMetrics"
|
||||||
|
name="vm-metric"
|
||||||
|
:label="t('harvester.dashboard.sections.vmMetrics.label')"
|
||||||
|
:weight="98"
|
||||||
|
>
|
||||||
|
<template #default="props">
|
||||||
|
<DashboardMetrics
|
||||||
|
v-if="props.active"
|
||||||
|
:detail-url="VM_DASHBOARD_METRICS_URL"
|
||||||
|
graph-height="825px"
|
||||||
|
:has-summary-and-detail="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
|
||||||
|
<div class="mb-40 mt-40">
|
||||||
|
<h3>
|
||||||
|
{{ t('clusterIndexPage.sections.events.label') }}
|
||||||
|
</h3>
|
||||||
|
<Tabbed class="mt-20">
|
||||||
|
<Tab
|
||||||
|
name="host"
|
||||||
|
label="Hosts"
|
||||||
|
:weight="98"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
:rows="hostEvents"
|
||||||
|
:headers="eventHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:search="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:paging="true"
|
||||||
|
:rows-per-page="10"
|
||||||
|
default-sort-by="date"
|
||||||
|
>
|
||||||
|
<template #cell:resource="{row, value}">
|
||||||
|
<div class="text-info">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
<div v-if="row.message">
|
||||||
|
{{ row.displayMessage }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SortableTable>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="vm"
|
||||||
|
label="VMs"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
:rows="vmEvents"
|
||||||
|
:headers="eventHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:search="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:paging="true"
|
||||||
|
:rows-per-page="10"
|
||||||
|
default-sort-by="date"
|
||||||
|
>
|
||||||
|
<template #cell:resource="{row, value}">
|
||||||
|
<div class="text-info">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
<div v-if="row.message">
|
||||||
|
{{ row.displayMessage }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SortableTable>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="volume"
|
||||||
|
label="Volumes"
|
||||||
|
:weight="97"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
:rows="volumeEvents"
|
||||||
|
:headers="eventHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:search="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:paging="true"
|
||||||
|
:rows-per-page="10"
|
||||||
|
default-sort-by="date"
|
||||||
|
>
|
||||||
|
<template #cell:resource="{row, value}">
|
||||||
|
<div class="text-info">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
<div v-if="row.message">
|
||||||
|
{{ row.displayMessage }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SortableTable>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="image"
|
||||||
|
label="Images"
|
||||||
|
:weight="96"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
:rows="imageEvents"
|
||||||
|
:headers="eventHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:search="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:paging="true"
|
||||||
|
:rows-per-page="10"
|
||||||
|
default-sort-by="date"
|
||||||
|
>
|
||||||
|
<template #cell:resource="{row, value}">
|
||||||
|
<div class="text-info">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
<div v-if="row.message">
|
||||||
|
{{ row.displayMessage }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SortableTable>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cluster-dashboard-glance {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 20px 0px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&>*{
|
||||||
|
margin-right: 40px;
|
||||||
|
|
||||||
|
& SPAN {
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.events {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
5
pkg/harvester/model-loader-require.lib.js
Normal file
5
pkg/harvester/model-loader-require.lib.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// Import all shell models. We could try to be smart here and only pull in the one's that harvester uses... but there's a lot across a
|
||||||
|
// number of stores
|
||||||
|
export default function modelLoaderRequire(type) {
|
||||||
|
return require(`@shell/models/${ type }`);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { IPlugin } from '@shell/core/types';
|
import { IPlugin } from '@shell/core/types';
|
||||||
import { PRODUCT_NAME, BLANK_CLUSTER, LOGO as logo } from './config/harvester';
|
import { PRODUCT_NAME, BLANK_CLUSTER, LOGO as logo } from './config/harvester';
|
||||||
import { HCI } from './config/types';
|
import { HCI } from './types';
|
||||||
|
|
||||||
export function init($plugin: IPlugin, store: any) {
|
export function init($plugin: IPlugin, store: any) {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
// Don't forget to create a VueJS page called index.vue in the /pages folder!!!
|
// Don't forget to create a VueJS page called index.vue in the /pages folder!!!
|
||||||
import Dashboard from '../pages/index.vue';
|
import Dashboard from '../pages/index.vue';
|
||||||
|
import Home from '../list/harvesterhci.io.dashboard.vue';
|
||||||
import { PRODUCT_NAME, BLANK_CLUSTER } from '../config/harvester';
|
import { PRODUCT_NAME, BLANK_CLUSTER } from '../config/harvester';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
name: `${ PRODUCT_NAME }-c-cluster`,
|
name: `${ PRODUCT_NAME }-c-cluster`,
|
||||||
path: `/${ PRODUCT_NAME }/c/:cluster`,
|
path: `/${ PRODUCT_NAME }/c/:cluster`,
|
||||||
component: Dashboard,
|
component: Home,
|
||||||
meta: {
|
meta: {
|
||||||
product: PRODUCT_NAME,
|
product: PRODUCT_NAME,
|
||||||
cluster: BLANK_CLUSTER,
|
cluster: BLANK_CLUSTER,
|
||||||
|
|||||||
183
pkg/harvester/store/harvester-common.js
Normal file
183
pkg/harvester/store/harvester-common.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import Parse from 'url-parse';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { PRODUCT_NAME } from '../config/harvester';
|
||||||
|
|
||||||
|
const state = function() {
|
||||||
|
return {
|
||||||
|
latestBundleId: '',
|
||||||
|
bundlePending: false,
|
||||||
|
showBundleModal: false,
|
||||||
|
bundlePercentage: 0,
|
||||||
|
uploadingImages: [],
|
||||||
|
uploadingImageError: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
setLatestBundleId(state, bundleId) {
|
||||||
|
state.latestBundleId = bundleId;
|
||||||
|
},
|
||||||
|
|
||||||
|
setBundlePending(state, value) {
|
||||||
|
state.bundlePending = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleBundleModal(state, value) {
|
||||||
|
state.showBundleModal = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
setBundlePercentage(state, value) {
|
||||||
|
state.bundlePercentage = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadStart(state, value) {
|
||||||
|
state.uploadingImages.push(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadError(state, { name, message }) {
|
||||||
|
state.uploadingImageError[name] = message;
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadEnd(state, value) {
|
||||||
|
const filtered = state.uploadingImages.filter(l => l !== value);
|
||||||
|
|
||||||
|
Vue.set(state, 'uploadingImages', filtered);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
getBundleId(state) {
|
||||||
|
return state.latestBundleId;
|
||||||
|
},
|
||||||
|
|
||||||
|
isBundlePending(state) {
|
||||||
|
return state.bundlePending;
|
||||||
|
},
|
||||||
|
|
||||||
|
isShowBundleModal(state) {
|
||||||
|
return state.showBundleModal;
|
||||||
|
},
|
||||||
|
|
||||||
|
getBundlePercentage(state) {
|
||||||
|
return state.bundlePercentage;
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadingImages(state) {
|
||||||
|
return state.uploadingImages;
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadingImageError(state) {
|
||||||
|
return name => state.uploadingImageError[name];
|
||||||
|
},
|
||||||
|
|
||||||
|
getHarvesterClusterUrl: (state, getters, rootState, rootGetters) => (url) => {
|
||||||
|
// returns in multiple clusters: /k8s/clusters/${ clusterId }/${url}
|
||||||
|
// Directly return the passed url in a single cluster
|
||||||
|
if (rootGetters['isMultiCluster']) {
|
||||||
|
const clusterId = rootGetters['clusterId'];
|
||||||
|
const multipleClusterUrl = `/k8s/clusters/${ clusterId }/${ url }`;
|
||||||
|
|
||||||
|
return `${ multipleClusterUrl }`;
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
async bundleProgress({
|
||||||
|
state, dispatch, commit, rootGetters
|
||||||
|
}) {
|
||||||
|
const parse = Parse(window.history.href);
|
||||||
|
|
||||||
|
const id = state.latestBundleId;
|
||||||
|
let bundleCrd = await dispatch(
|
||||||
|
'harvester/find',
|
||||||
|
{ type: HCI.SUPPORT_BUNDLE, id },
|
||||||
|
{ root: true }
|
||||||
|
);
|
||||||
|
const t = rootGetters['i18n/t'];
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
await commit('setBundlePending', true);
|
||||||
|
const timer = setInterval(async() => {
|
||||||
|
count = count + 1;
|
||||||
|
if (count % 3 === 0) {
|
||||||
|
// ws mayby disconnect
|
||||||
|
bundleCrd = await dispatch(
|
||||||
|
'harvester/find',
|
||||||
|
{
|
||||||
|
type: HCI.SUPPORT_BUNDLE,
|
||||||
|
id,
|
||||||
|
opt: { force: true }
|
||||||
|
},
|
||||||
|
{ root: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bundleCrd.bundleState !== 'ready') {
|
||||||
|
bundleCrd = rootGetters['harvester/byId'](HCI.SUPPORT_BUNDLE, id);
|
||||||
|
const percentage = bundleCrd.precent;
|
||||||
|
|
||||||
|
commit('setBundlePercentage', percentage);
|
||||||
|
|
||||||
|
if (bundleCrd?.bundleMessage) {
|
||||||
|
const err = bundleCrd?.bundleMessage;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
'growl/fromError',
|
||||||
|
{ title: t('generic.notification.title.error'), err },
|
||||||
|
{ root: true }
|
||||||
|
);
|
||||||
|
clearInterval(timer);
|
||||||
|
commit('setBundlePending', false);
|
||||||
|
commit('toggleBundleModal', false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const name = id.split('/')[1];
|
||||||
|
|
||||||
|
commit('setBundlePercentage', 1);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
commit('toggleBundleModal', false);
|
||||||
|
commit('setBundlePending', false);
|
||||||
|
commit('setBundlePercentage', 0);
|
||||||
|
}, 600);
|
||||||
|
|
||||||
|
if (rootGetters['isMultiCluster']) {
|
||||||
|
const clusterId = rootGetters['clusterId'];
|
||||||
|
const prefix = `/k8s/clusters/${ clusterId }`;
|
||||||
|
|
||||||
|
window.location.href = `${ parse.origin }${ prefix }/v1/harvester/supportbundles/${ name }/download`;
|
||||||
|
} else {
|
||||||
|
window.location.href = `${ parse.origin }/v1/harvester/supportbundles/${ name }/download`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const harvesterFactory = () => {
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
|
||||||
|
getters: { ...getters },
|
||||||
|
|
||||||
|
mutations: { ...mutations },
|
||||||
|
|
||||||
|
actions: { ...actions }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const config = {
|
||||||
|
namespace: `${ PRODUCT_NAME }-common`,
|
||||||
|
isClusterStore: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
specifics: harvesterFactory(),
|
||||||
|
config
|
||||||
|
};
|
||||||
142
pkg/harvester/store/harvester-store/actions.ts
Normal file
142
pkg/harvester/store/harvester-store/actions.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { ClusterNotFoundError } from '@shell/utils/error';
|
||||||
|
import { SETTING } from '@shell/config/settings';
|
||||||
|
import { COUNT, NAMESPACE, MANAGEMENT } from '@shell/config/types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { DEV } from '@shell/store/prefs';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async loadCluster({
|
||||||
|
state, commit, dispatch, getters, rootGetters, rootState
|
||||||
|
}: any, { id }: any) {
|
||||||
|
// This is a workaround for a timing issue where the mgmt cluster schema may not be available
|
||||||
|
// Try and wait until the schema exists before proceeding
|
||||||
|
await dispatch('management/waitForSchema', { type: MANAGEMENT.CLUSTER }, { root: true });
|
||||||
|
|
||||||
|
// See if it really exists
|
||||||
|
const cluster = await dispatch('management/find', {
|
||||||
|
type: MANAGEMENT.CLUSTER,
|
||||||
|
id,
|
||||||
|
opt: { url: `${ MANAGEMENT.CLUSTER }s/${ escape(id) }` }
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
let virtualBase = `/k8s/clusters/${ escape(id) }/v1/harvester`;
|
||||||
|
|
||||||
|
if (id === 'local') {
|
||||||
|
virtualBase = `/v1/harvester`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !cluster ) {
|
||||||
|
commit('clusterId', null, { root: true });
|
||||||
|
commit('applyConfig', { baseUrl: null });
|
||||||
|
throw new ClusterNotFoundError(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the Steve client URLs
|
||||||
|
commit('applyConfig', { baseUrl: virtualBase });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
dispatch('loadSchemas', true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
dispatch('subscribe');
|
||||||
|
|
||||||
|
const projectArgs = {
|
||||||
|
type: MANAGEMENT.PROJECT,
|
||||||
|
opt: {
|
||||||
|
url: `${ MANAGEMENT.PROJECT }/${ escape(id) }`,
|
||||||
|
watchNamespace: id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchProjects = async() => {
|
||||||
|
let limit = 30000;
|
||||||
|
const sleep = 100;
|
||||||
|
|
||||||
|
while ( limit > 0 && !rootState.managementReady ) {
|
||||||
|
await setTimeout(() => {}, sleep);
|
||||||
|
limit -= sleep;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( rootGetters['management/schemaFor'](MANAGEMENT.PROJECT) ) {
|
||||||
|
return dispatch('management/findAll', projectArgs, { root: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (id !== 'local' && getters['schemaFor'](MANAGEMENT.SETTING)) { // multi-cluster
|
||||||
|
const settings = await dispatch('findAll', {
|
||||||
|
type: MANAGEMENT.SETTING,
|
||||||
|
id: SETTING.SYSTEM_NAMESPACES,
|
||||||
|
opt: { url: `${ virtualBase }/${ MANAGEMENT.SETTING }s/`, force: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const systemNamespaces = settings?.find((x: any) => x.id === SETTING.SYSTEM_NAMESPACES);
|
||||||
|
|
||||||
|
if (systemNamespaces) {
|
||||||
|
const namespace = (systemNamespaces.value || systemNamespaces.default)?.split(',');
|
||||||
|
|
||||||
|
commit('setSystemNamespaces', namespace, { root: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash: { [key: string]: Promise<any>} = {
|
||||||
|
projects: fetchProjects(),
|
||||||
|
virtualCount: dispatch('findAll', { type: COUNT }),
|
||||||
|
virtualNamespaces: dispatch('findAll', { type: NAMESPACE }),
|
||||||
|
settings: dispatch('findAll', { type: HCI.SETTING }),
|
||||||
|
clusters: dispatch('management/findAll', {
|
||||||
|
type: MANAGEMENT.CLUSTER,
|
||||||
|
opt: { force: true }
|
||||||
|
}, { root: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (getters['schemaFor'](HCI.UPGRADE)) {
|
||||||
|
hash.upgrades = dispatch('findAll', { type: HCI.UPGRADE });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await allHash(hash);
|
||||||
|
|
||||||
|
await dispatch('cleanNamespaces', null, { root: true });
|
||||||
|
|
||||||
|
commit('updateNamespaces', {
|
||||||
|
filters: [],
|
||||||
|
all: getters.filterNamespace(),
|
||||||
|
getters
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
// Solve compatibility with Rancher v2.6.x, fell remove these codes after not support v2.6.x
|
||||||
|
const definition = {
|
||||||
|
def: false,
|
||||||
|
parseJSON: true,
|
||||||
|
inheritFrom: DEV,
|
||||||
|
asUserPreference: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
commit('prefs/setDefinition', {
|
||||||
|
name: 'view-in-api',
|
||||||
|
definition,
|
||||||
|
}, { root: true });
|
||||||
|
commit('prefs/setDefinition', {
|
||||||
|
name: 'all-namespaces',
|
||||||
|
definition,
|
||||||
|
}, { root: true });
|
||||||
|
commit('prefs/setDefinition', {
|
||||||
|
name: 'theme-shortcut',
|
||||||
|
definition,
|
||||||
|
}, { root: true });
|
||||||
|
commit('prefs/setDefinition', {
|
||||||
|
name: 'plugin-developer',
|
||||||
|
definition,
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
const isMultiCluster = !(res.clusters.length === 1 && res.clusters[0].metadata?.name === 'local');
|
||||||
|
|
||||||
|
if (isMultiCluster) {
|
||||||
|
commit('managementChanged', {
|
||||||
|
ready: true,
|
||||||
|
isMultiCluster: true,
|
||||||
|
isRancher: true,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
126
pkg/harvester/store/harvester-store/getters.ts
Normal file
126
pkg/harvester/store/harvester-store/getters.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
NAMESPACE_FILTER_KINDS,
|
||||||
|
NAMESPACE_FILTER_ALL as ALL,
|
||||||
|
NAMESPACE_FILTER_ALL_ORPHANS as ALL_ORPHANS,
|
||||||
|
} from '@shell/utils/namespace-filter';
|
||||||
|
import { MANAGEMENT } from '@shell/config/types';
|
||||||
|
import { sortBy } from '@shell/utils/sort';
|
||||||
|
import { filterBy } from '@shell/utils/array';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaceFilterOptions: (state: any, getters: any, rootState: any, rootGetters: any) => ({
|
||||||
|
addNamespace,
|
||||||
|
divider,
|
||||||
|
notFilterNamespaces
|
||||||
|
}: any) => {
|
||||||
|
const out = [{
|
||||||
|
id: ALL,
|
||||||
|
kind: NAMESPACE_FILTER_KINDS.SPECIAL,
|
||||||
|
label: rootGetters['i18n/t']('nav.ns.all'),
|
||||||
|
}];
|
||||||
|
|
||||||
|
divider(out);
|
||||||
|
|
||||||
|
const namespaces = getters.filterNamespace(notFilterNamespaces);
|
||||||
|
|
||||||
|
if (!rootGetters['isStandaloneHarvester'] && rootGetters['currentCluster'] && rootGetters['currentCluster']?.id !== '_') {
|
||||||
|
const cluster = rootGetters['currentCluster'];
|
||||||
|
let projects = rootGetters['management/all'](
|
||||||
|
MANAGEMENT.PROJECT
|
||||||
|
);
|
||||||
|
|
||||||
|
projects = sortBy(filterBy(projects, 'spec.clusterName', cluster.id), [
|
||||||
|
'nameDisplay',
|
||||||
|
]).filter((project: any) => project.nameDisplay !== 'System');
|
||||||
|
|
||||||
|
const projectsById: any = {};
|
||||||
|
const namespacesByProject: any = {};
|
||||||
|
let firstProject = true;
|
||||||
|
|
||||||
|
namespacesByProject['null'] = []; // For namespaces not in a project
|
||||||
|
for (const project of projects) {
|
||||||
|
projectsById[project.metadata.name] = project;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const namespace of namespaces) {
|
||||||
|
let projectId = namespace.projectId;
|
||||||
|
|
||||||
|
if (!projectId || !projectsById[projectId]) {
|
||||||
|
// If there's a projectId but that project doesn't exist, treat it like no project
|
||||||
|
projectId = 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = namespacesByProject[projectId];
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
entry = [];
|
||||||
|
namespacesByProject[namespace.projectId] = entry;
|
||||||
|
}
|
||||||
|
entry.push(namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const project of projects) {
|
||||||
|
const id = project.metadata.name;
|
||||||
|
|
||||||
|
if (firstProject) {
|
||||||
|
firstProject = false;
|
||||||
|
} else {
|
||||||
|
divider(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push({
|
||||||
|
id: `project://${ id }`,
|
||||||
|
kind: 'project',
|
||||||
|
label: project.nameDisplay,
|
||||||
|
});
|
||||||
|
|
||||||
|
const forThisProject = namespacesByProject[id] || [];
|
||||||
|
|
||||||
|
addNamespace(out, forThisProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orphans = namespacesByProject['null'];
|
||||||
|
|
||||||
|
if (orphans.length) {
|
||||||
|
if (!firstProject) {
|
||||||
|
divider(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push({
|
||||||
|
id: ALL_ORPHANS,
|
||||||
|
kind: 'project',
|
||||||
|
label: rootGetters['i18n/t']('nav.ns.orphan'),
|
||||||
|
});
|
||||||
|
|
||||||
|
addNamespace(out, orphans);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addNamespace(out, namespaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* filter system/fleet/cattle namespace
|
||||||
|
*/
|
||||||
|
filterNamespace(state: any, getters: any, rootState: any, rootGetters: any, action: any) {
|
||||||
|
const allNamespaces = getters.all('namespace');
|
||||||
|
|
||||||
|
return (notFilterNamespaces: any = []) => {
|
||||||
|
return allNamespaces.filter((namespace: any) => {
|
||||||
|
return !namespace.isSystem || notFilterNamespaces.includes(namespace.id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
filterProject(state: any, getters: any, rootState: any, rootGetters: any) {
|
||||||
|
const projectsInAllClusters = rootGetters['management/all'](
|
||||||
|
MANAGEMENT.PROJECT
|
||||||
|
);
|
||||||
|
const currentCluster = rootGetters['currentCluster'];
|
||||||
|
const clusterId = currentCluster.id;
|
||||||
|
|
||||||
|
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
|
||||||
|
}
|
||||||
|
};
|
||||||
39
pkg/harvester/store/harvester-store/index.ts
Normal file
39
pkg/harvester/store/harvester-store/index.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { CoreStoreSpecifics, CoreStoreConfig } from '@shell/core/types';
|
||||||
|
|
||||||
|
import { SteveFactory, steveStoreInit } from '@shell/plugins/steve/index';
|
||||||
|
import { PRODUCT_NAME } from '../../config/harvester';
|
||||||
|
import getters from './getters';
|
||||||
|
import mutations from './mutations';
|
||||||
|
import actions from './actions';
|
||||||
|
|
||||||
|
|
||||||
|
const harvesterFactory = (): CoreStoreSpecifics => {
|
||||||
|
const steveFactory = SteveFactory();
|
||||||
|
|
||||||
|
steveFactory.getters = {
|
||||||
|
...steveFactory.getters,
|
||||||
|
...getters,
|
||||||
|
};
|
||||||
|
|
||||||
|
steveFactory.mutations = {
|
||||||
|
...steveFactory.mutations,
|
||||||
|
...mutations,
|
||||||
|
};
|
||||||
|
|
||||||
|
steveFactory.actions = {
|
||||||
|
...steveFactory.actions,
|
||||||
|
...actions,
|
||||||
|
};
|
||||||
|
|
||||||
|
return steveFactory;
|
||||||
|
};
|
||||||
|
const config: CoreStoreConfig = {
|
||||||
|
namespace: PRODUCT_NAME,
|
||||||
|
isClusterStore: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
specifics: harvesterFactory(),
|
||||||
|
config,
|
||||||
|
init: steveStoreInit
|
||||||
|
};
|
||||||
1
pkg/harvester/store/harvester-store/mutations.ts
Normal file
1
pkg/harvester/store/harvester-store/mutations.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default {};
|
||||||
36
pkg/harvester/store/harvester-store/subscribe-shims.ts
Normal file
36
pkg/harvester/store/harvester-store/subscribe-shims.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { _MERGE } from '@shell/plugins/dashboard-store/actions';
|
||||||
|
import PollerSequential from '@shell/utils/poller-sequential';
|
||||||
|
|
||||||
|
const polling: any = {};
|
||||||
|
const POLL_INTERVAL = 10000;
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
unsubscribe() {
|
||||||
|
Object.entries(polling).forEach(([type, poll]: [any, any]) => {
|
||||||
|
console.warn('Epinio: Polling stopped for: ', type); // eslint-disable-line no-console
|
||||||
|
poll.stop();
|
||||||
|
delete polling[type];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
watch({ dispatch, rootGetters }: any, { type }: any) {
|
||||||
|
if (rootGetters['type-map/isSpoofed'](type) || polling[type]) {
|
||||||
|
// Ignore spoofed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('Epinio: Polling started for: ', type);// eslint-disable-line no-console
|
||||||
|
|
||||||
|
polling[type] = new PollerSequential(
|
||||||
|
async() => {
|
||||||
|
console.debug('Epinio: Polling: ', type); // eslint-disable-line no-console
|
||||||
|
// NOTE - In order for lists to automatically update resources opt to MERGE data in place instead of replace
|
||||||
|
// (in rancher land these are all handled individually, here we have bulk changes)
|
||||||
|
await dispatch('findAll', { type, opt: { force: true, load: _MERGE } });
|
||||||
|
},
|
||||||
|
POLL_INTERVAL,
|
||||||
|
5
|
||||||
|
);
|
||||||
|
polling[type].start();
|
||||||
|
}
|
||||||
|
};
|
||||||
7
pkg/harvester/validators/hash.js
Normal file
7
pkg/harvester/validators/hash.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function hashSHA512(value, getters, errors, validatorArgs, displayKey) {
|
||||||
|
if (!/^[a-f0-9]{128}$/i.test(value)) {
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.hash.sha512'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
23
pkg/harvester/validators/index.js
Normal file
23
pkg/harvester/validators/index.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { imageUrl, fileRequired } from './vm-image';
|
||||||
|
import { vmNetworks, vmDisks } from './vm';
|
||||||
|
import { dataVolumeSize } from './vm-datavolumes';
|
||||||
|
import { backupTarget, ntpServers } from './setting';
|
||||||
|
import { volumeSize } from './volume';
|
||||||
|
import { rancherMonitoring, rancherLogging } from './monitoringAndLogging';
|
||||||
|
import { ranges } from './network';
|
||||||
|
import { hashSHA512 } from './hash';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
imageUrl,
|
||||||
|
dataVolumeSize,
|
||||||
|
vmNetworks,
|
||||||
|
vmDisks,
|
||||||
|
fileRequired,
|
||||||
|
backupTarget,
|
||||||
|
ntpServers,
|
||||||
|
volumeSize,
|
||||||
|
rancherMonitoring,
|
||||||
|
rancherLogging,
|
||||||
|
ranges,
|
||||||
|
hashSHA512,
|
||||||
|
};
|
||||||
146
pkg/harvester/validators/monitoringAndLogging.js
Normal file
146
pkg/harvester/validators/monitoringAndLogging.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import jsyaml from 'js-yaml';
|
||||||
|
import { get } from '@shell/utils/object';
|
||||||
|
|
||||||
|
export function rancherMonitoring(valuesContent, getters, errors, validatorArgs) {
|
||||||
|
const valueJson = jsyaml.load(valuesContent);
|
||||||
|
const requiredFields = [
|
||||||
|
{
|
||||||
|
path: 'prometheus.prometheusSpec.scrapeInterval',
|
||||||
|
translationKey: 'monitoring.prometheus.config.scrape'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus.prometheusSpec.evaluationInterval',
|
||||||
|
translationKey: 'monitoring.prometheus.config.evaluation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus.prometheusSpec.retention',
|
||||||
|
translationKey: 'monitoring.prometheus.config.retention'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus.prometheusSpec.retentionSize',
|
||||||
|
translationKey: 'monitoring.prometheus.config.retentionSize'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus.prometheusSpec.resources.requests.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus.prometheusSpec.resources.requests.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus.prometheusSpec.resources.limits.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus.prometheusSpec.resources.limits.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus-node-exporter.resources.requests.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus-node-exporter.resources.requests.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus-node-exporter.resources.limits.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'prometheus-node-exporter.resources.limits.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'grafana.resources.requests.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'grafana.resources.requests.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'grafana.resources.limits.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'grafana.resources.limits.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'alertmanager.alertmanagerSpec.retention',
|
||||||
|
translationKey: 'monitoring.prometheus.config.retention'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'alertmanager.alertmanagerSpec.resources.requests.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'alertmanager.alertmanagerSpec.resources.requests.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'alertmanager.alertmanagerSpec.resources.limits.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'alertmanager.alertmanagerSpec.resources.limits.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
requiredFields.forEach((rule) => {
|
||||||
|
if (!get(valueJson, rule.path)) {
|
||||||
|
errors.push(getters['i18n/t']('validation.required', { key: getters['i18n/t'](rule.translationKey) }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rancherLogging(valuesContent, getters, errors, validatorArgs) {
|
||||||
|
const valueJson = jsyaml.load(valuesContent);
|
||||||
|
const requiredFields = [
|
||||||
|
{
|
||||||
|
path: 'fluentbit.resources.requests.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'fluentbit.resources.requests.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'fluentbit.resources.limits.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'fluentbit.resources.limits.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'fluentd.resources.requests.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'fluentd.resources.requests.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'fluentd.resources.limits.cpu',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'fluentd.resources.limits.memory',
|
||||||
|
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
requiredFields.forEach((rule) => {
|
||||||
|
if (!get(valueJson, rule.path)) {
|
||||||
|
errors.push(getters['i18n/t']('validation.required', { key: getters['i18n/t'](rule.translationKey) }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
15
pkg/harvester/validators/network.js
Normal file
15
pkg/harvester/validators/network.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export function ranges(ranges = [], getters, errors, validatorArgs) {
|
||||||
|
const t = getters['i18n/t'];
|
||||||
|
|
||||||
|
if (ranges.length === 0) {
|
||||||
|
errors.push(t('validation.required', { key: t('harvester.ipPool.tabs.range') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges.map((r) => {
|
||||||
|
if (!r.subnet) {
|
||||||
|
errors.push(t('validation.required', { key: t('harvester.ipPool.subnet.label') }, true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
61
pkg/harvester/validators/setting.js
Normal file
61
pkg/harvester/validators/setting.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
export function backupTarget(value, getters, errors, validatorArgs) {
|
||||||
|
const t = getters['i18n/t'];
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseValue = JSON.parse(value);
|
||||||
|
const type = parseValue.type;
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 's3') {
|
||||||
|
if (!parseValue.accessKeyId) {
|
||||||
|
errors.push(t('validation.required', { key: 'accessKeyId' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseValue.secretAccessKey) {
|
||||||
|
errors.push(t('validation.required', { key: 'secretAccessKey' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseValue.bucketRegion) {
|
||||||
|
errors.push(t('validation.required', { key: 'bucketRegion' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseValue.bucketName) {
|
||||||
|
errors.push(t('validation.required', { key: 'bucketName' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseValue.endpoint && type !== 's3') {
|
||||||
|
errors.push(t('validation.required', { key: 'endpoint' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ntpServers(value, getters, errors, validatorArgs) {
|
||||||
|
const { ntpServers } = JSON.parse(value);
|
||||||
|
const t = getters['i18n/t'];
|
||||||
|
const ipv4Regex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||||
|
const hostRegex = /^(?!:\/\/)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,63}$/;
|
||||||
|
|
||||||
|
if (!ntpServers) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ntpServersSet = new Set(ntpServers);
|
||||||
|
|
||||||
|
if (ntpServers.length !== ntpServersSet.size) {
|
||||||
|
errors.push(t('harvester.setting.ntpServers.isDuplicate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ntpServers.find(V => !ipv4Regex.test(V) && !hostRegex.test(V))) {
|
||||||
|
errors.push(t('harvester.setting.ntpServers.isNotIPV4'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
38
pkg/harvester/validators/vm-datavolumes.js
Normal file
38
pkg/harvester/validators/vm-datavolumes.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { formatSi, parseSi } from '@shell/utils/units';
|
||||||
|
|
||||||
|
export function dataVolumeSize(storage, getters, errors, validatorArgs) {
|
||||||
|
const t = getters['i18n/t'];
|
||||||
|
|
||||||
|
if (!storage || storage === '') {
|
||||||
|
const key = t('harvester.volume.size');
|
||||||
|
|
||||||
|
errors.push(t('validation.required', { key }));
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = getSize(storage);
|
||||||
|
const max = 999999;
|
||||||
|
const integerRegex = /^[1-9]\d*$/;
|
||||||
|
|
||||||
|
if (!integerRegex.test(size) || size > max) {
|
||||||
|
errors.push(t('harvester.validation.volume.sizeRange'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSize(storage) {
|
||||||
|
if (!storage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kibUnitSize = parseSi(storage);
|
||||||
|
|
||||||
|
return formatSi(kibUnitSize, {
|
||||||
|
addSuffix: false,
|
||||||
|
increment: 1024,
|
||||||
|
minExponent: 3,
|
||||||
|
maxExponent: 3
|
||||||
|
});
|
||||||
|
}
|
||||||
32
pkg/harvester/validators/vm-image.js
Normal file
32
pkg/harvester/validators/vm-image.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { HCI } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
|
export const VM_IMAGE_FILE_FORMAT = ['qcow', 'qcow2', 'raw', 'img', 'iso'];
|
||||||
|
|
||||||
|
export function imageUrl(url, getters, errors, validatorArgs, type) {
|
||||||
|
const t = getters['i18n/t'];
|
||||||
|
|
||||||
|
if (!url || url === '') {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffixName = url.split('/').pop();
|
||||||
|
const fileSuffix = suffixName.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
if (!VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) {
|
||||||
|
const tipString = type === 'file' ? 'harvester.validation.image.ruleFileTip' : 'harvester.validation.image.ruleTip';
|
||||||
|
|
||||||
|
errors.push(t(tipString));
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileRequired(annotations = {}, getters, errors, validatorArgs, type) {
|
||||||
|
const t = getters['i18n/t'];
|
||||||
|
|
||||||
|
if (!annotations[HCI.IMAGE_NAME]) {
|
||||||
|
errors.push(t('validation.required', { key: t('harvester.image.fileName') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
228
pkg/harvester/validators/vm.js
Normal file
228
pkg/harvester/validators/vm.js
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import { PVC } from '@shell/config/types';
|
||||||
|
import { isValidMac, isValidDNSLabelName } from '@pkg/utils/regular';
|
||||||
|
import { SOURCE_TYPE } from '@pkg/config/harvester-map';
|
||||||
|
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
||||||
|
|
||||||
|
const maxNameLength = 63;
|
||||||
|
|
||||||
|
export function vmNetworks(spec, getters, errors, validatorArgs) {
|
||||||
|
const { domain: { devices: { interfaces } }, networks } = spec;
|
||||||
|
|
||||||
|
const networkNames = [];
|
||||||
|
|
||||||
|
interfaces.map( (I, index) => {
|
||||||
|
const N = networks.find( N => I.name === N.name);
|
||||||
|
const prefix = (I.name || N.name) || `Network ${ index + 1 }`;
|
||||||
|
|
||||||
|
const type = getters['i18n/t']('harvester.fields.network');
|
||||||
|
|
||||||
|
const lowerType = getters['i18n/t']('harvester.validation.vm.network.lowerType');
|
||||||
|
const upperType = getters['i18n/t']('harvester.validation.vm.network.upperType');
|
||||||
|
|
||||||
|
validName(getters, errors, I.name, networkNames, prefix, type, lowerType, upperType);
|
||||||
|
|
||||||
|
if (N.multus) {
|
||||||
|
if (!N.multus.networkName) {
|
||||||
|
const key = getters['i18n/t']('harvester.fields.network');
|
||||||
|
const message = getters['i18n/t']('validation.required', { key });
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (I.macAddress && !isValidMac(I.macAddress) && !N.pod) {
|
||||||
|
const message = getters['i18n/t']('harvester.validation.vm.network.macFormat');
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value) {
|
||||||
|
const isVMTemplate = validatorArgs.includes('isVMTemplate');
|
||||||
|
const data = isVMTemplate ? this.value.spec.vm : value;
|
||||||
|
|
||||||
|
const _volumeClaimTemplates = parseVolumeClaimTemplates(data);
|
||||||
|
|
||||||
|
const _volumes = spec.template.spec.volumes || [];
|
||||||
|
const _disks = spec.template.spec.domain.devices.disks || [];
|
||||||
|
|
||||||
|
const diskNames = [];
|
||||||
|
|
||||||
|
_disks.forEach((D, idx) => {
|
||||||
|
const prefix = D.name || _volumes[idx]?.name || `Volume ${ idx + 1 }`;
|
||||||
|
|
||||||
|
if (!D.disk && !D.cdrom) {
|
||||||
|
const key = getters['i18n/t']('harvester.fields.type');
|
||||||
|
const message = getters['i18n/t']('validation.required', { key });
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = getters['i18n/t']('harvester.fields.volume');
|
||||||
|
const lowerType = getters['i18n/t']('harvester.validation.vm.volume.lowerType');
|
||||||
|
const upperType = getters['i18n/t']('harvester.validation.vm.volume.upperType');
|
||||||
|
|
||||||
|
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
|
||||||
|
});
|
||||||
|
|
||||||
|
let requiredVolume = false;
|
||||||
|
|
||||||
|
_volumes.forEach((V, idx) => {
|
||||||
|
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
|
||||||
|
|
||||||
|
const prefix = V.name || idx + 1;
|
||||||
|
|
||||||
|
if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) {
|
||||||
|
requiredVolume = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
|
||||||
|
if (!/([1-9]|[1-9][0-9]+)[a-zA-Z]+/.test(typeValue?.spec?.resources?.requests?.storage)) {
|
||||||
|
const key = getters['i18n/t']('harvester.fields.size');
|
||||||
|
const message = getters['i18n/t']('validation.required', { key });
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeValue?.spec?.resources?.requests?.storage && !/^([1-9][0-9]{0,8})[a-zA-Z]+$/.test(typeValue?.spec?.resources?.requests?.storage)) {
|
||||||
|
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === SOURCE_TYPE.IMAGE && !typeValue?.spec?.storageClassName && !isVMTemplate) { // type === SOURCE_TYPE.IMAGE
|
||||||
|
const key = getters['i18n/t']('harvester.fields.image');
|
||||||
|
const message = getters['i18n/t']('validation.required', { key });
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!typeValue?.spec?.storageClassName && V?.persistentVolumeClaim?.claimName && type !== SOURCE_TYPE.IMAGE) {
|
||||||
|
const key = getters['i18n/t']('harvester.fields.storageClass');
|
||||||
|
const message = getters['i18n/t']('validation.required', { key });
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === SOURCE_TYPE.ATTACH_VOLUME) {
|
||||||
|
const allPVCs = getters['harvester/all'](PVC);
|
||||||
|
|
||||||
|
const selectedVolumeName = V?.persistentVolumeClaim?.claimName;
|
||||||
|
const hasExistingVolume = allPVCs.find(P => P.id === `${ value.metadata.namespace }/${ selectedVolumeName }`);
|
||||||
|
|
||||||
|
if (!hasExistingVolume && selectedVolumeName) { // selected volume may have been deleted. e.g: use template
|
||||||
|
const type = getters['i18n/t']('harvester.fields.volume');
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.hasDelete', { type, name: selectedVolumeName }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedVolumeName) { // volume is not selected.
|
||||||
|
const key = getters['i18n/t']('harvester.virtualMachine.volume.volume');
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('validation.required', { key }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === SOURCE_TYPE.CONTAINER && !V.containerDisk.image) {
|
||||||
|
const key = getters['i18n/t']('harvester.fields.dockerImage');
|
||||||
|
const message = getters['i18n/t']('validation.required', { key });
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* At least one volume must be create. (Verify only when create.)
|
||||||
|
*/
|
||||||
|
if ((!requiredVolume || _volumes.length === 0) && !value.links) {
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needImageOrExisting'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVolumeType(getters, V, DVTS, value) {
|
||||||
|
let outValue = null;
|
||||||
|
const allPVCs = getters['harvester/all'](PVC);
|
||||||
|
|
||||||
|
if (V.persistentVolumeClaim) {
|
||||||
|
const selectedVolumeName = V?.persistentVolumeClaim?.claimName;
|
||||||
|
const hasExistingVolume = allPVCs.find(P => P.id === `${ value.metadata.namespace }/${ selectedVolumeName }`);
|
||||||
|
|
||||||
|
if (hasExistingVolume) {
|
||||||
|
// In other cases, claimName will not be empty, so we can judge whether this is an exiting volume based on this attribute
|
||||||
|
return {
|
||||||
|
type: SOURCE_TYPE.ATTACH_VOLUME,
|
||||||
|
typeValue: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
outValue = DVTS.find((DVT) => {
|
||||||
|
return V.persistentVolumeClaim.claimName === DVT.metadata.name && DVT.metadata?.annotations && Object.prototype.hasOwnProperty.call(DVT.metadata.annotations, 'harvesterhci.io/imageId');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (outValue) {
|
||||||
|
return {
|
||||||
|
type: SOURCE_TYPE.IMAGE,
|
||||||
|
typeValue: outValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// new type
|
||||||
|
outValue = DVTS.find(DVT => V.persistentVolumeClaim.claimName === DVT.metadata.name);
|
||||||
|
|
||||||
|
if (outValue) {
|
||||||
|
return {
|
||||||
|
type: SOURCE_TYPE.NEW,
|
||||||
|
typeValue: outValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (V.containerDisk) {
|
||||||
|
return {
|
||||||
|
type: SOURCE_TYPE.CONTAINER,
|
||||||
|
typeValue: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validName(getters, errors, name, names = [], prefix, type, lowerType, upperType) {
|
||||||
|
// Verify that the name is duplicate
|
||||||
|
if (names.findIndex( N => name === N) !== -1) {
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.vm.duplicatedName', { type, name }));
|
||||||
|
}
|
||||||
|
|
||||||
|
names.push(name);
|
||||||
|
|
||||||
|
// The maximum length of volume name is 63 characters.
|
||||||
|
if (name && name?.length > maxNameLength) {
|
||||||
|
const key = getters['i18n/t']('harvester.fields.name');
|
||||||
|
const message = getters['i18n/t']('harvester.validation.generic.maxLength', { key, max: maxNameLength });
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// name required
|
||||||
|
if (!name) {
|
||||||
|
const key = getters['i18n/t']('harvester.fields.name');
|
||||||
|
const message = getters['i18n/t']('validation.required', { key });
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid RFC 1123
|
||||||
|
if (!isValidDNSLabelName(name)) {
|
||||||
|
const regex = '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$';
|
||||||
|
|
||||||
|
errors.push(getters['i18n/t']('harvester.validation.generic.regex', {
|
||||||
|
lowerType, name, regex, upperType
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
9
pkg/harvester/validators/volume.js
Normal file
9
pkg/harvester/validators/volume.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export function volumeSize(size, getters, errors, validatorArgs, displayKey, value) {
|
||||||
|
if (!/^([1-9][0-9]{0,8})[a-zA-Z]+$/.test(size)) {
|
||||||
|
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
|
||||||
|
|
||||||
|
errors.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user