Merge pull request #6398 from n313893254/pr

HARVESTER: Fixes v1.0.3 issues
This commit is contained in:
Nancy 2022-07-20 13:19:03 -07:00 committed by GitHub
commit 7618bb5fb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 298 additions and 131 deletions

View File

@ -1156,6 +1156,9 @@ cluster:
label: KubeconfigContent
placeholder: 'Namespace/Name'
cluster: Cluster
affinity:
namespaces:
placeholder: e.g. default,system,base
description:
label: Cluster Description
placeholder: Any text you want that better describes this cluster
@ -5999,6 +6002,11 @@ harvester:
fail: Fail
ongoing: on-going
dismissMessage: Dismiss it
upgradeInfo:
warning: WARNING
doc: Before you upgrade to the newer Harvester version, you must perform the required <a href="https://docs.harvesterhci.io/v1.0/upgrade/automatic/" target="_blank"> pre-upgrade checks </a>for your cluster. Complete only those tasks that apply to your environment.
tip: Failure to perform these checks may result in a failed upgrade or hitting known issues that require a manual workaround fix.
moreNotes: For more details about the release notes, please visit -
backup:
label: Backups

View File

@ -1166,6 +1166,9 @@ cluster:
label: KubeconfigContent
placeholder: '命名空间/名称'
cluster: 集群
affinity:
namespaces:
placeholder: 例如default,system,base
description:
label: 集群描述
placeholder: 输入可以描述该集群的文本

View File

@ -1,4 +1,5 @@
<script>
import debounce from 'lodash/debounce';
import { _VIEW } from '@shell/config/query-params';
import { mapGetters } from 'vuex';
import { get, isEmpty, clone } from '@shell/utils/object';
@ -50,7 +51,10 @@ export default {
return {
allSelectorTerms,
weightedNodeSelectorTerms: preferredDuringSchedulingIgnoredDuringExecution,
defaultWeight: 1
defaultWeight: 1,
// rules in MatchExpressions.vue can not catch changes what happens on parent component
// we need re-render it via key changing
rerenderNums: randomStr(4)
};
}
},
@ -73,35 +77,38 @@ export default {
}
},
mounted() {
this.update();
created() {
this.queueUpdate = debounce(this.update, 500);
},
methods: {
update() {
this.$nextTick(() => {
const out = {};
const requiredDuringSchedulingIgnoredDuringExecution = { nodeSelectorTerms: [] };
const preferredDuringSchedulingIgnoredDuringExecution = [] ;
const out = {};
const requiredDuringSchedulingIgnoredDuringExecution = { nodeSelectorTerms: [] };
const preferredDuringSchedulingIgnoredDuringExecution = [] ;
this.allSelectorTerms.forEach((term) => {
if (term.weight) {
const neu = { weight: 1, preference: term };
this.allSelectorTerms.forEach((term) => {
if (term.weight) {
const neu = { weight: 1, preference: term };
preferredDuringSchedulingIgnoredDuringExecution.push(neu);
} else {
requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.push(term);
}
});
if (preferredDuringSchedulingIgnoredDuringExecution.length) {
out.preferredDuringSchedulingIgnoredDuringExecution = preferredDuringSchedulingIgnoredDuringExecution;
preferredDuringSchedulingIgnoredDuringExecution.push(neu);
} else {
requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.push(term);
}
if (requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.length) {
out.requiredDuringSchedulingIgnoredDuringExecution = requiredDuringSchedulingIgnoredDuringExecution;
}
this.$emit('input', out);
});
if (preferredDuringSchedulingIgnoredDuringExecution.length) {
out.preferredDuringSchedulingIgnoredDuringExecution = preferredDuringSchedulingIgnoredDuringExecution;
}
if (requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.length) {
out.requiredDuringSchedulingIgnoredDuringExecution = requiredDuringSchedulingIgnoredDuringExecution;
}
this.$emit('input', out);
},
remove() {
this.rerenderNums = randomStr(4);
this.queueUpdate();
},
changePriority(term) {
@ -126,9 +133,16 @@ export default {
</script>
<template>
<div class="row" @input="update">
<div class="row" @input="queueUpdate">
<div class="col span-12">
<ArrayListGrouped v-model="allSelectorTerms" class="mt-20" :mode="mode" :default-add-value="{matchExpressions:[]}" :add-label="t('workload.scheduling.affinity.addNodeSelector')">
<ArrayListGrouped
v-model="allSelectorTerms"
class="mt-20"
:mode="mode"
:default-add-value="{matchExpressions:[]}"
:add-label="t('workload.scheduling.affinity.addNodeSelector')"
@remove="remove"
>
<template #default="props">
<div class="row">
<div class="col span-6">
@ -142,6 +156,7 @@ export default {
</div>
</div>
<MatchExpressions
:key="rerenderNums"
v-model="props.row.value.matchExpressions"
:mode="mode"
class="col span-12 mt-20"

View File

@ -35,6 +35,11 @@ export default {
type: Array,
default: () => []
},
hasNodesAndNs: {
type: Boolean,
default: true
}
},
data() {
@ -49,6 +54,7 @@ export default {
out._anti = false;
if (term.podAffinityTerm) {
Object.assign(out, term.podAffinityTerm);
out._namespaces = (term.podAffinityTerm.namespaces || []).toString();
delete out.podAffinityTerm;
}
@ -61,6 +67,7 @@ export default {
out._anti = true;
if (term.podAffinityTerm) {
Object.assign(out, term.podAffinityTerm);
out._namespaces = (term.podAffinityTerm.namespaces || []).toString();
delete out.podAffinityTerm;
}
@ -71,7 +78,10 @@ export default {
return {
allSelectorTerms,
defaultWeight: 1
defaultWeight: 1,
// rules in MatchExpressions.vue can not catch changes what happens on parent component
// we need re-render it via key changing
rerenderNums: randomStr(4)
};
},
computed: {
@ -104,6 +114,7 @@ export default {
return getUniqueLabelKeys(this.nodes);
}
},
created() {
this.queueUpdate = debounce(this.update, 500);
},
@ -132,6 +143,12 @@ export default {
});
Object.assign(this.value.affinity, { podAffinity, podAntiAffinity });
this.$emit('update', this.value);
},
remove() {
this.rerenderNums = randomStr(4);
this.queueUpdate();
},
addSelector() {
@ -163,14 +180,20 @@ export default {
changeNamespaceMode(term, idx) {
if (term.namespaces) {
term.namespaces = null;
term._namespaces = null;
} else {
this.$set(term, 'namespaces', []);
this.$set(term, '_namespaces', '');
}
this.$set(this.allSelectorTerms, idx, term);
this.queueUpdate();
},
updateNamespaces(term, namespaces) {
this.$set(term, 'namespaces', namespaces);
const nsArray = namespaces.split(',').map(ns => ns.trim()).filter(ns => ns?.length);
this.$set(term, 'namespaces', nsArray);
this.queueUpdate();
},
isEmpty,
@ -190,6 +213,7 @@ export default {
:default-add-value="{ matchExpressions: [] }"
:mode="mode"
:add-label="t('workload.scheduling.affinity.addNodeSelector')"
@remove="remove"
>
<template #default="props">
<div class="row mt-20 mb-20">
@ -226,6 +250,7 @@ export default {
<div class="spacer"></div>
<div v-if="!!props.row.value.namespaces || !!get(props.row.value, 'podAffinityTerm.namespaces')" class="row mb-20">
<LabeledSelect
v-if="hasNodesAndNs"
v-model="props.row.value.namespaces"
:mode="mode"
:multiple="true"
@ -233,8 +258,17 @@ export default {
:options="allNamespaces"
:label="t('workload.scheduling.affinity.matchExpressions.inNamespaces')"
/>
<LabeledInput
v-else
v-model="props.row.value._namespaces"
:mode="mode"
:label="t('workload.scheduling.affinity.matchExpressions.inNamespaces')"
:placeholder="t('cluster.credential.harvester.affinity.namespaces.placeholder')"
@input="updateNamespaces(props.row.value, props.row.value._namespaces)"
/>
</div>
<MatchExpressions
:key="rerenderNums"
:mode="mode"
class=" col span-12 mt-20"
:type="pod"
@ -246,6 +280,7 @@ export default {
<div class="row">
<div class="col span-12">
<LabeledSelect
v-if="hasNodesAndNs"
v-model="props.row.value.topologyKey"
:taggable="true"
:searchable="true"
@ -258,6 +293,15 @@ export default {
:disabled="mode==='view'"
@input="update"
/>
<LabeledInput
v-else
v-model="props.row.value.topologyKey"
:mode="mode"
:label="t('workload.scheduling.affinity.topologyKey.label')"
:placeholder="t('workload.scheduling.affinity.topologyKey.placeholder')"
required
@input="update"
/>
</div>
</div>

View File

@ -166,7 +166,8 @@ export const HCI = {
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
VM_VOLUME_STATUS: 'harvesterhci.io/volume-status',
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
OS_TYPE: 'harvesterhci.io/os-type'
OS_TYPE: 'harvesterhci.io/os-type',
HOST_REQUEST: 'management.cattle.io/pod-requests',
};
// Annotations that can be on management.cattle.io.cluster to configure a custom badge

View File

@ -208,6 +208,12 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK);
},
hasLonghornSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](LONGHORN.NODES);
},
},
methods: {
@ -294,21 +300,36 @@ export default {
<hr class="divider" />
<h3>{{ t('harvester.host.tabs.monitor') }}</h3>
<div class="row mb-20">
<div class="col span-4">
<div
class="col"
:class="{
'span-4': hasLonghornSchema,
'span-6': !hasLonghornSchema,
}"
>
<HarvesterCPUUsed
:row="value"
:resource-name="t('node.detail.glance.consumptionGauge.cpu')"
:show-used="true"
/>
</div>
<div class="col span-4">
<div
class="col"
:class="{
'span-4': hasLonghornSchema,
'span-6': !hasLonghornSchema,
}"
>
<HarvesterMemoryUsed
:row="value"
:resource-name="t('node.detail.glance.consumptionGauge.memory')"
:show-used="true"
/>
</div>
<div class="col span-4">
<div
v-if="hasLonghornSchema"
class="col span-4"
>
<HarvesterStorageUsed
:row="value"
:resource-name="t('harvester.host.detail.storage')"

View File

@ -229,6 +229,7 @@ export default {
:label="t('harvester.fields.image')"
:options="imagesOption"
:mode="mode"
:searchable="true"
:required="validateRequired"
@input="onImageChange"
/>

View File

@ -5,12 +5,13 @@ import { allHash } from '@shell/utils/promise';
import ModalWithCard from '@shell/components/ModalWithCard';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner';
import UpgradeInfo from './UpgradeInfo';
export default {
name: 'HarvesterUpgrade',
components: {
ModalWithCard, LabeledSelect, Banner
ModalWithCard, LabeledSelect, Banner, UpgradeInfo
},
async fetch() {
@ -121,19 +122,19 @@ export default {
</button>
</header>
<ModalWithCard ref="deleteTip" name="deleteTip" :width="500">
<ModalWithCard ref="deleteTip" name="deleteTip" :width="850">
<template #title>
<t k="harvester.upgradePage.upgradeApp" />
</template>
<template #content>
<div class="currentVersion">
<UpgradeInfo :version="version" />
<div class="currentVersion mb-15">
<span> <t k="harvester.upgradePage.currentVersion" /> </span>
<span class="version">{{ currentVersion }}</span>
</div>
<p class="bordered-section"></p>
<div v-if="selectMode">
<LabeledSelect
v-model="version"
@ -151,10 +152,10 @@ export default {
<template #footer>
<div class="footer">
<button class="btn role-secondary btn-sm mr-20" @click.prevent="cancel">
<button class="btn role-secondary mr-20" @click.prevent="cancel">
<t k="generic.close" />
</button>
<button class="btn role-tertiary bg-primary btn-sm mr-20" @click.prevent="handleUpgrade">
<button class="btn role-tertiary bg-primary" @click.prevent="handleUpgrade">
<t k="harvester.upgradePage.upgrade" />
</button>
</div>

View File

@ -0,0 +1,40 @@
<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">
<strong>{{ t('harvester.upgradePage.upgradeInfo.warning') }}:</strong>
<p class="mb-5" v-html="t('harvester.upgradePage.upgradeInfo.doc', {}, true)">
</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>
</Banner>
</div>
</template>

View File

@ -375,50 +375,22 @@ export default {
},
cpuReserved() {
const useful = this.pods.filter((pod) => {
const nodeName = pod?.spec?.nodeName;
return this.availableNodes.includes(nodeName);
}).reduce((sum, pod) => {
const containers = pod?.spec?.containers || [];
const containerCpuReserved = containers.reduce((sum, c) => {
sum += parseSi(c?.resources?.requests?.cpu || '0m');
return sum;
}, 0);
sum += containerCpuReserved;
return sum;
const useful = this.nodes.reduce((total, node) => {
return total + node.cpuReserved;
}, 0);
return {
total: this.cpusTotal,
useful: Number(formatSi(useful)),
total: this.cpusTotal,
useful,
};
},
ramReserved() {
const useful = this.pods.filter((pod) => {
const nodeName = pod?.spec?.nodeName;
return this.availableNodes.includes(nodeName);
}).reduce((sum, pod) => {
const containers = pod?.spec?.containers || [];
const containerMemoryReserved = containers.reduce((sum, c) => {
sum += parseSi(c?.resources?.requests?.memory || '0m', { increment: 1024 });
return sum;
}, 0);
sum += containerMemoryReserved;
return sum;
const useful = this.nodes.reduce((total, node) => {
return total + node.memoryReserved;
}, 0);
return this.createMemoryValues(this.memoryTotal, useful);
return createMemoryValues(this.memoryTotal, useful);
},
availableNodes() {
@ -583,7 +555,12 @@ export default {
<h3 class="mt-40">
{{ t('clusterIndexPage.sections.capacity.label') }}
</h3>
<div class="hardware-resource-gauges">
<div
class="hardware-resource-gauges"
:class="{
live: !storageTotal,
}"
>
<HardwareResourceGauge
:name="t('harvester.dashboard.hardwareResourceGauge.cpu')"
:reserved="cpuReserved"
@ -595,6 +572,7 @@ export default {
:used="ramUsed"
/>
<HardwareResourceGauge
v-if="storageTotal"
:name="t('harvester.dashboard.hardwareResourceGauge.storage')"
:used="storageUsed"
:reserved="storageReserved"

View File

@ -44,6 +44,8 @@ export default {
if (this.$store.getters['harvester/schemaFor'](LONGHORN.NODES)) {
_hash.longhornNodes = this.$store.dispatch('harvester/findAll', { type: LONGHORN.NODES });
} else {
this.hasLonghornSchema = false;
}
if (this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE)) {
@ -57,8 +59,9 @@ export default {
data() {
return {
rows: [],
hasMetricSchema: true
rows: [],
hasMetricSchema: true,
hasLonghornSchema: true,
};
},
@ -86,12 +89,13 @@ export default {
];
if (this.hasMetricSchema) {
const width = this.hasLonghornSchema ? '230' : '345';
const metricCol = [
{
name: 'cpu',
labelKey: 'node.detail.glance.consumptionGauge.cpu',
value: 'id',
width: '230',
width,
formatter: 'HarvesterCPUUsed',
formatterOpts: { showUsed: true },
},
@ -99,23 +103,28 @@ export default {
name: 'memory',
labelKey: 'node.detail.glance.consumptionGauge.memory',
value: 'id',
width: '230',
width,
formatter: 'HarvesterMemoryUsed',
formatterOpts: { showUsed: true },
},
{
name: 'storage',
labelKey: 'tableHeaders.storage',
value: 'id',
width: '230',
formatter: 'HarvesterStorageUsed',
formatterOpts: { showReserved: true },
}
];
out.splice(-1, 0, ...metricCol);
}
if (this.hasLonghornSchema) {
const storageHeader = {
name: 'storage',
labelKey: 'tableHeaders.storage',
value: 'id',
width: '230',
formatter: 'HarvesterStorageUsed',
formatterOpts: { showReserved: true },
};
out.splice(-1, 0, storageHeader);
}
out.push(AGE);
return out;

View File

@ -1,5 +1,7 @@
<script>
import isEmpty from 'lodash/isEmpty';
import NodeAffinity from '@shell/components/form/NodeAffinity';
import PodAffinity from '@shell/components/form/PodAffinity';
import Loading from '@shell/components/Loading';
import CreateEditView from '@shell/mixins/create-edit-view';
import LabeledSelect from '@shell/components/form/LabeledSelect';
@ -11,10 +13,11 @@ import { Banner } from '@components/Banner';
import { get } from '@shell/utils/object';
import { mapGetters } from 'vuex';
import {
HCI, NAMESPACE, MANAGEMENT, CONFIG_MAP, NORMAN
HCI, NAMESPACE, MANAGEMENT, CONFIG_MAP, NORMAN, NODE
} from '@shell/config/types';
import { base64Decode, base64Encode } from '@shell/utils/crypto';
import { allHashSettled } from '@shell/utils/promise';
import { podAffinity as podAffinityValidator } from '@shell/utils/validators/pod-affinity';
import { stringify, exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@shell/config/labels-annotations';
import { isReady } from '@shell/models/harvester/harvesterhci.io.virtualmachineimage';
@ -23,7 +26,7 @@ export default {
name: 'ConfigComponentHarvester',
components: {
Loading, LabeledSelect, LabeledInput, UnitInput, Banner, YamlEditor
Loading, LabeledSelect, LabeledInput, UnitInput, Banner, YamlEditor, NodeAffinity, PodAffinity
},
mixins: [CreateEditView],
@ -70,6 +73,7 @@ export default {
if (clusterId && isImportCluster) {
const res = await allHashSettled({
nodes: this.$store.dispatch('cluster/request', { url: `${ url }/${ NODE }s` }),
namespaces: this.$store.dispatch('cluster/request', { url: `${ url }/${ NAMESPACE }s` }),
images: this.$store.dispatch('cluster/request', { url: `${ url }/${ HCI.IMAGE }s` }),
configMaps: this.$store.dispatch('cluster/request', { url: `${ url }/${ CONFIG_MAP }s` }),
@ -117,6 +121,8 @@ export default {
this.userDataOptions = userDataOptions;
this.networkDataOptions = networkDataOptions;
this.images = res.images.value?.data;
this.allNodeObjects = res.nodes.value?.data || [];
this.allNodes = this.allNodeObjects.map(node => node.id);
this.networkOptions = (res.networks.value?.data || []).map( (O) => {
let value;
@ -169,9 +175,14 @@ export default {
},
data() {
let vmAffinity = { affinity: {} };
let userData = '';
let networkData = '';
if (this.value.vmAffinity) {
vmAffinity = { affinity: JSON.parse(base64Decode(this.value.vmAffinity)) };
}
if (this.value.userData) {
userData = base64Decode(this.value.userData);
}
@ -183,6 +194,7 @@ export default {
return {
credential: null,
isImportCluster: false,
vmAffinity,
userData,
networkData,
images: [],
@ -190,6 +202,8 @@ export default {
networkOptions: [],
userDataOptions: [],
networkDataOptions: [],
allNodes: [],
allNodeObjects: [],
cpuCount: ''
};
},
@ -201,18 +215,23 @@ export default {
return this.disabled || !!(this.isEdit && this.value.id);
},
imageOptions() {
return (this.images || []).filter( (O) => {
return !O.spec.url.endsWith('.iso') && isReady.call(O);
}).sort((a, b) => a.metadata.creationTimestamp > b.metadata.creationTimestamp ? -1 : 1).map( (O) => {
const value = O.id;
const label = `${ O.spec.displayName } (${ value })`;
imageOptions: {
get() {
return (this.images || []).filter( (O) => {
return !O.spec.url.endsWith('.iso') && isReady.call(O);
}).sort((a, b) => a.metadata.creationTimestamp > b.metadata.creationTimestamp ? -1 : 1).map( (O) => {
const value = O.id;
const label = `${ O.spec.displayName } (${ value })`;
return {
label,
value
};
});
return {
label,
value
};
});
},
set(neu) {
this.images = neu;
}
},
namespaceDisabled() {
@ -226,9 +245,11 @@ export default {
this.imageOptions = [];
this.networkOptions = [];
this.namespaceOptions = [];
this.vmAffinity = { affinity: {} };
this.value.imageName = '';
this.value.networkName = '';
this.value.vmNamespace = '';
this.value.vmAffinity = '';
}
this.$fetch();
@ -304,6 +325,8 @@ export default {
errors.push(message);
}
podAffinityValidator(this.vmAffinity.affinity, this.$store.getters, errors);
return { errors };
},
@ -332,6 +355,30 @@ export default {
} catch (e) {
this.errors = exceptionToErrorsArray(e);
}
},
updateScheduling(neu) {
const { affinity } = neu;
if (!affinity.nodeAffinity && !affinity.podAffinity && !affinity.podAntiAffinity) {
this.value.vmAffinity = '';
this.vmAffinity = { affinity: {} };
return;
}
this.value.vmAffinity = base64Encode(JSON.stringify(affinity));
this.vmAffinity = neu;
},
updateNodeScheduling(nodeAffinity) {
if (!this.vmAffinity.affinity) {
Object.assign(this.vmAffinity, { affinity: { nodeAffinity } });
} else {
Object.assign(this.vmAffinity.affinity, { nodeAffinity });
}
this.updateScheduling(this.vmAffinity);
}
},
};
@ -478,7 +525,23 @@ export default {
</div>
<portal :to="'advanced-'+uuid">
<h3>{{ t("cluster.credential.harvester.userData.title") }}</h3>
<h3 class="mt-20">
{{ t("workload.container.titles.nodeScheduling") }}
</h3>
<NodeAffinity
:mode="mode"
:value="vmAffinity.affinity.nodeAffinity"
@input="updateNodeScheduling"
/>
<h3 class="mt-20">
{{ t("workload.container.titles.podScheduling") }}
</h3>
<PodAffinity :mode="mode" :value="vmAffinity" :nodes="allNodeObjects" :has-nodes-and-ns="isImportCluster" @update="updateScheduling" />
<h3 class="mt-20">
{{ t("cluster.credential.harvester.userData.title") }}
</h3>
<div>
<LabeledSelect
v-if="isImportCluster && isCreate"

View File

@ -231,40 +231,20 @@ export default class HciNode extends SteveModel {
return pods.filter(p => p?.spec?.nodeName === this.id && p?.metadata?.name !== 'removing');
}
get reserved() {
try {
return JSON.parse(this.metadata.annotations[HCI_ANNOTATIONS.HOST_REQUEST] || '{}');
} catch {
return {};
}
}
get cpuReserved() {
const out = this.pods.reduce((sum, pod) => {
const containers = pod?.spec?.containers || [];
const containerCpuReserved = containers.reduce((sum, c) => {
sum += parseSi(c?.resources?.requests?.cpu || '0m');
return sum;
}, 0);
sum += containerCpuReserved;
return sum;
}, 0);
return out;
return parseSi(this.reserved.cpu || '0');
}
get memoryReserved() {
const out = this.pods.reduce((sum, pod) => {
const containers = pod?.spec?.containers || [];
const containerMemoryReserved = containers.reduce((sum, c) => {
sum += parseSi(c?.resources?.requests?.memory || '0m', { increment: 1024 });
return sum;
}, 0);
sum += containerMemoryReserved;
return sum;
}, 0);
return out;
return parseSi(this.reserved.memory || '0');
}
get canDelete() {

View File

@ -3,6 +3,7 @@ import CruResource from '@shell/components/CruResource';
import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import UpgradeInfo from '@shell/list/harvesterhci.io.dashboard/UpgradeInfo';
import { HCI } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
@ -19,7 +20,7 @@ const UPLOAD = 'upload';
export default {
name: 'HarvesterAirgapUpgrade',
components: {
CruResource, LabeledSelect, LabeledInput, RadioGroup
CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo
},
async fetch() {
@ -216,7 +217,7 @@ export default {
>
<RadioGroup
v-model="imageSource"
class="mb-20 image-group"
class="image-group"
name="image"
:options="[
IMAGE_METHOD.NEW,
@ -228,6 +229,8 @@ export default {
]"
/>
<UpgradeInfo />
<div v-if="uploadImage">
<LabeledInput v-model.trim="imageValue.spec.displayName" class="mb-20" label-key="harvester.fields.name" required />