Latest changes from Harvester master - d7d9d4af8a88d677695d7aff47a81d52041dfcca

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia
2024-11-12 11:50:17 +01:00
parent d04cd35a08
commit dc74441d26
46 changed files with 1290 additions and 1624 deletions
@@ -13,13 +13,23 @@ import Loading from '@shell/components/Loading';
import { _CREATE, _VIEW } from '@shell/config/query-params';
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
import { STORAGE_CLASS, LONGHORN } from '@shell/config/types';
import { STORAGE_CLASS, LONGHORN, LONGHORN_DRIVER, SECRET, NAMESPACE } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { clone } from '@shell/utils/object';
import { CSI_DRIVER } from '../../types';
import Tags from '../../components/DiskTags';
const LONGHORN_DRIVER = 'driver.longhorn.io';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
export const DATA_ENGINE_V1 = 'v1';
export const DATA_ENGINE_V2 = 'v2';
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
const VOLUME_BINDING_MODE_IMMEDIATE = 'Immediate';
const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer';
export default {
name: 'HarvesterStorage',
@@ -66,47 +76,63 @@ export default {
const volumeBindingModeOptions = [
{
label: this.t('storageClass.customize.volumeBindingMode.now'),
value: 'Immediate'
value: VOLUME_BINDING_MODE_IMMEDIATE
},
{
label: this.t('harvester.storage.customize.volumeBindingMode.later'),
value: 'WaitForFirstConsumer'
value: VOLUME_BINDING_MODE_WAIT
}
];
const allowedTopologies = clone(this.value.allowedTopologies?.[0]?.matchLabelExpressions || []);
const allowedTopologies = clone(this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).filter(t => t.key !== LVM_TOPOLOGY_LABEL);
this.value['parameters'] = this.value.parameters || {};
this.value['provisioner'] = this.value.provisioner || LONGHORN_DRIVER;
this.value['allowVolumeExpansion'] = this.value.allowVolumeExpansion || allowVolumeExpansionOptions[0].value;
this.value['reclaimPolicy'] = this.value.reclaimPolicy || reclaimPolicyOptions[0].value;
this.value['volumeBindingMode'] = this.value.volumeBindingMode || volumeBindingModeOptions[0].value;
if (this.value.provisioner === LONGHORN_DRIVER) {
this.value['parameters']['dataEngine'] = this.value.longhornVersion;
this.value['volumeBindingMode'] = this.value.volumeBindingMode || VOLUME_BINDING_MODE_IMMEDIATE;
}
if (this.value.provisioner === LVM_DRIVER) {
this.value['volumeBindingMode'] = this.value.volumeBindingMode || VOLUME_BINDING_MODE_WAIT;
}
let provisioner = `${ this.value.provisioner || LONGHORN_DRIVER }`;
if (provisioner === LONGHORN_DRIVER) {
provisioner = `${ provisioner }_${ this.value.longhornVersion }`;
}
return {
LVM_DRIVER,
reclaimPolicyOptions,
allowVolumeExpansionOptions,
volumeBindingModeOptions,
mountOptions: [],
provisioner: LONGHORN_DRIVER,
STORAGE_CLASS,
provisioner,
allowedTopologies,
defaultAddValue: {
key: '',
values: [],
}
},
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = {
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
};
await allHash(hash);
await allHash({
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
longhornV2DataEngine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
});
},
computed: {
@@ -120,20 +146,37 @@ export default {
return this.isCreate ? _CREATE : _VIEW;
},
provisionerWatch() {
return this.value.provisioner;
},
provisioners() {
const csiDrivers = this.$store.getters[`${ this.inStore }/all`](CSI_DRIVER) || [];
const format = { [LONGHORN_DRIVER]: 'storageClass.longhorn.title' };
const out = [];
return csiDrivers.map((provisioner) => {
return {
label: format[provisioner.name] || provisioner.name,
value: provisioner.name,
};
const inStore = this.$store.getters['currentProduct'].inStore;
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
csiDrivers.forEach(({ name }) => {
switch (name) {
case LONGHORN_DRIVER:
out.push({
label: `harvester.storage.storageClass.longhorn.${ DATA_ENGINE_V1 }.label`,
value: `${ name }_${ DATA_ENGINE_V1 }`,
});
if (this.longhornSystemVersion === DATA_ENGINE_V2 || this.value.longhornVersion === DATA_ENGINE_V2) {
out.push({
label: `harvester.storage.storageClass.longhorn.${ DATA_ENGINE_V2 }.label`,
value: `${ name }_${ DATA_ENGINE_V2 }`,
});
}
break;
case LVM_DRIVER:
out.push({
label: 'harvester.storage.storageClass.lvm.label',
value: name,
});
break;
}
});
return out;
},
schema() {
@@ -141,11 +184,41 @@ export default {
return this.$store.getters[`${ inStore }/schemaFor`](STORAGE_CLASS);
},
longhornSystemVersion() {
const inStore = this.$store.getters['currentProduct'].inStore;
const v2DataEngine = this.$store.getters[`${ inStore }/byId`](LONGHORN.SETTINGS, LONGHORN_V2_DATA_ENGINE) || {};
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
},
},
watch: {
provisionerWatch() {
this.value['parameters'] = {};
provisioner(neu) {
const [provisioner, dataEngine] = neu?.split('_');
let parameters = {};
if (provisioner === LVM_DRIVER) {
const matchLabelExpressions = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).filter(t => t.key !== LVM_TOPOLOGY_LABEL);
if (matchLabelExpressions.length > 0) {
this.value['allowedTopologies'] = [{ matchLabelExpressions }];
} else {
delete this.value.allowedTopologies;
}
this.value['volumeBindingMode'] = VOLUME_BINDING_MODE_WAIT;
}
if (provisioner === LONGHORN_DRIVER) {
parameters = { dataEngine };
this.value['volumeBindingMode'] = VOLUME_BINDING_MODE_IMMEDIATE;
}
this.value['provisioner'] = provisioner;
this.value['allowVolumeExpansion'] = this.value.provisioner === LONGHORN_DRIVER;
this.value['parameters'] = parameters;
}
},
@@ -162,11 +235,6 @@ export default {
}
},
updateProvisioner(provisioner) {
this.value['provisioner'] = provisioner;
this.value['allowVolumeExpansion'] = provisioner === LONGHORN_DRIVER;
},
willSave() {
Object.keys(this.value.parameters).forEach((key) => {
if (this.value.parameters[key] === null || this.value.parameters[key] === '') {
@@ -178,10 +246,15 @@ export default {
},
formatAllowedTopoloties() {
const neu = this.allowedTopologies;
const neu = this.allowedTopologies.filter(t => t.key !== LVM_TOPOLOGY_LABEL);
const lvmMatchExpression = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).filter(t => t.key === LVM_TOPOLOGY_LABEL);
if (!neu || neu.length === 0) {
delete this.value.allowedTopologies;
if (lvmMatchExpression.length > 0) {
this.value.allowedTopologies = [{ matchLabelExpressions: lvmMatchExpression }];
} else {
delete this.value.allowedTopologies;
}
return;
}
@@ -189,7 +262,7 @@ export default {
const matchLabelExpressions = neu.filter(R => !!R.key.trim() && (R.values.length > 0 && !R.values.find(V => !V.trim())));
if (matchLabelExpressions.length > 0) {
this.value.allowedTopologies = [{ matchLabelExpressions }];
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
}
}
}
@@ -219,7 +292,7 @@ export default {
:register-before-hook="registerBeforeHook"
/>
<LabeledSelect
:value="value.provisioner"
v-model:value="provisioner"
label="Provisioner"
:options="provisioners"
:localized-label="true"
@@ -227,13 +300,12 @@ export default {
:searchable="true"
:taggable="true"
class="mb-20"
@update:value="updateProvisioner($event)"
/>
<Tabbed :side-tabs="true">
<Tab name="parameters" :label="t('storageClass.parameters.label')" :weight="2">
<component
:is="getComponent(value.provisioner)"
:key="value.provisioner"
:is="getComponent(provisioner)"
:key="provisioner"
:value="value"
:mode="modeOverride"
:real-mode="realMode"
@@ -268,6 +340,7 @@ export default {
:label="t('storageClass.customize.volumeBindingMode.label')"
:mode="modeOverride"
:options="volumeBindingModeOptions"
:disabled="provisioner === LVM_DRIVER"
/>
</div>
</div>
@@ -3,11 +3,12 @@ import KeyValue from '@shell/components/form/KeyValue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import RadioGroup from '@components/Form/Radio/RadioGroup';
import { SECRET, NAMESPACE, LONGHORN } from '@shell/config/types';
import { SECRET, LONGHORN } from '@shell/config/types';
import { _CREATE, _VIEW } from '@shell/config/query-params';
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
import { clone } from '@shell/utils/object';
import { uniq } from '@shell/utils/array';
import { DATA_ENGINE_V1 } from '../index.vue';
// UI components for Longhorn storage class parameters
const DEFAULT_PARAMETERS = [
@@ -17,6 +18,7 @@ const DEFAULT_PARAMETERS = [
'nodeSelector',
'migratable',
'encrypted',
'dataEngine',
];
const {
@@ -29,7 +31,7 @@ const {
} = CSI_SECRETS;
export default {
name: 'DriverLonghornIO',
name: 'DriverLonghornIOV1',
components: {
KeyValue,
@@ -53,16 +55,6 @@ export default {
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
const allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
// only show non-system secret to user to select
this.secrets = allSecrets.filter(secret => secret.isSystem === false);
},
data() {
if (this.realMode === _CREATE) {
this.value['parameters'] = {
@@ -72,12 +64,23 @@ export default {
nodeSelector: null,
encrypted: 'false',
migratable: 'true',
dataEngine: DATA_ENGINE_V1
};
}
return { secrets: [] };
return { };
},
computed: {
secrets() {
const inStore = this.$store.getters['currentProduct'].inStore;
const allSecrets = this.$store.getters[`${ inStore }/all`](SECRET);
// only show non-system secret to user to select
return allSecrets.filter(secret => secret.isSystem === false);
},
longhornNodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
@@ -0,0 +1,352 @@
<script>
import KeyValue from '@shell/components/form/KeyValue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import RadioGroup from '@components/Form/Radio/RadioGroup';
import { SECRET, LONGHORN } from '@shell/config/types';
import { _CREATE, _VIEW } from '@shell/config/query-params';
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
import { clone } from '@shell/utils/object';
import { uniq } from '@shell/utils/array';
import { DATA_ENGINE_V2 } from '../index.vue';
// UI components for Longhorn storage class parameters
const DEFAULT_PARAMETERS = [
'numberOfReplicas',
'staleReplicaTimeout',
'diskSelector',
'nodeSelector',
'migratable',
'encrypted',
'dataEngine',
];
const {
CSI_PROVISIONER_SECRET_NAME,
CSI_PROVISIONER_SECRET_NAMESPACE,
CSI_NODE_PUBLISH_SECRET_NAME,
CSI_NODE_PUBLISH_SECRET_NAMESPACE,
CSI_NODE_STAGE_SECRET_NAME,
CSI_NODE_STAGE_SECRET_NAMESPACE
} = CSI_SECRETS;
export default {
name: 'DriverLonghornIOV2',
components: {
KeyValue,
LabeledSelect,
LabeledInput,
RadioGroup,
},
props: {
value: {
type: Object,
required: true
},
mode: {
type: String,
required: true
},
realMode: {
type: String,
required: true
},
},
data() {
if (this.realMode === _CREATE) {
this.value['parameters'] = {
numberOfReplicas: '3',
staleReplicaTimeout: '30',
diskSelector: null,
nodeSelector: null,
encrypted: 'false',
migratable: 'false',
dataEngine: DATA_ENGINE_V2
};
}
return { };
},
computed: {
secrets() {
const inStore = this.$store.getters['currentProduct'].inStore;
const allSecrets = this.$store.getters[`${ inStore }/all`](SECRET);
// only show non-system secret to user to select
return allSecrets.filter(secret => secret.isSystem === false);
},
longhornNodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](LONGHORN.NODES);
},
nodeTags() {
return (this.longhornNodes || []).reduce((sum, node) => {
const tags = node.spec?.tags || [];
return uniq([...sum, ...tags]);
}, []);
},
diskTags() {
return (this.longhornNodes || []).reduce((sum, node) => {
const disks = node.spec?.disks;
const tagsOfNode = Object.keys(disks).reduce((sum, key) => {
const tags = disks[key]?.tags || [];
return uniq([...sum, ...tags]);
}, []);
return uniq([...sum, ...tagsOfNode]);
}, []);
},
isView() {
return this.mode === _VIEW;
},
migratableOptions() {
return [{
label: this.t('generic.yes'),
value: 'true'
}, {
label: this.t('generic.no'),
value: 'false'
}];
},
secretOptions() {
return this.secrets.map(secret => secret.id);
},
volumeEncryptionOptions() {
return [{
label: this.t('generic.yes'),
value: 'true'
}, {
label: this.t('generic.no'),
value: 'false'
}];
},
parameters: {
get() {
const parameters = clone(this.value?.parameters) || {};
DEFAULT_PARAMETERS.forEach((key) => {
delete parameters[key];
});
Object.values(CSI_SECRETS).forEach((key) => {
delete parameters[key];
});
return parameters;
},
set(value) {
Object.assign(this.value.parameters, value);
}
},
volumeEncryption: {
set(neu) {
this.value['parameters'] = {
...this.value.parameters,
encrypted: neu
};
},
get() {
return this.value?.parameters?.encrypted || 'false';
}
},
secret: {
get() {
const selectedNs = this.value.parameters[CSI_PROVISIONER_SECRET_NAMESPACE];
const selectedName = this.value.parameters[CSI_PROVISIONER_SECRET_NAME];
if (selectedNs && selectedName) {
return `${ selectedNs }/${ selectedName }`;
}
return '';
},
set(selectedSecret) {
const [namespace, name] = selectedSecret.split('/');
this.value['parameters'] = {
...this.value.parameters,
[CSI_PROVISIONER_SECRET_NAME]: name,
[CSI_NODE_PUBLISH_SECRET_NAME]: name,
[CSI_NODE_STAGE_SECRET_NAME]: name,
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace
};
}
},
nodeSelector: {
get() {
const nodeSelector = this.value?.parameters?.nodeSelector;
if ((nodeSelector || '').includes(',')) {
return nodeSelector.split(',');
} else if (nodeSelector) {
return [nodeSelector];
} else {
return [];
}
},
set(value) {
this.value.parameters.nodeSelector = (value || []).join(',');
}
},
diskSelector: {
get() {
const diskSelector = this.value?.parameters?.diskSelector;
if ((diskSelector || '').includes(',')) {
return diskSelector.split(',');
} else if (diskSelector) {
return [diskSelector];
} else {
return [];
}
},
set(value) {
this.value.parameters.diskSelector = (value || []).join(',');
}
},
numberOfReplicas: {
get() {
return this.value?.parameters?.numberOfReplicas;
},
set(value) {
if (value >= 1 && value <= 3) {
this.value.parameters.numberOfReplicas = String(value);
}
}
},
},
};
</script>
<template>
<div>
<div class="row">
<div class="col span-6">
<LabeledInput
v-model:value="numberOfReplicas"
:label="t('harvester.storage.parameters.numberOfReplicas.label')"
:required="true"
:mode="mode"
min="1"
max="3"
type="number"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="value.parameters.staleReplicaTimeout"
:label="t('harvester.storage.parameters.staleReplicaTimeout.label')"
:required="true"
:mode="mode"
type="number"
/>
</div>
</div>
<div class="row mt-10">
<div class="col span-6">
<LabeledSelect
v-model:value="nodeSelector"
:label="t('harvester.storage.parameters.nodeSelector.label')"
:options="nodeTags"
:taggable="true"
:multiple="true"
:mode="mode"
>
<template #no-options="{ searching }">
<span v-if="!searching" class="text-muted">
{{ t('harvester.storage.parameters.nodeSelector.no-options', null, true) }}
</span>
</template>
</LabeledSelect>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="diskSelector"
:label="t('harvester.storage.parameters.diskSelector.label')"
:options="diskTags"
:taggable="true"
:multiple="true"
:mode="mode"
>
<template #no-options="{ searching }">
<span v-if="!searching" class="text-muted">
{{ t('harvester.storage.parameters.diskSelector.no-options', null, true) }}
</span>
</template>
</LabeledSelect>
</div>
</div>
<div class="row mt-20">
<RadioGroup
v-model:value="value.parameters.migratable"
name="layer3NetworkMode"
:label="t('harvester.storage.parameters.migratable.label')"
:mode="mode"
:options="migratableOptions"
:disabled="true"
/>
</div>
<div class="row mt-20">
<RadioGroup
v-model:value="volumeEncryption"
name="volumeEncryption"
:label="t('harvester.storage.volumeEncryption')"
:mode="mode"
:options="volumeEncryptionOptions"
:disabled="true"
/>
</div>
<div v-if="value.parameters.encrypted === 'true'" class="row mt-20">
<div class="col span-6">
<LabeledSelect
v-model:value="secret"
:label="t('harvester.storage.secret')"
:options="secretOptions"
:mode="mode"
/>
</div>
</div>
<KeyValue
v-model:value="parameters"
:add-label="t('storageClass.longhorn.addLabel')"
:read-allowed="false"
:mode="mode"
class="mt-10"
/>
</div>
</template>
<style lang="scss" scoped>
.labeled-input.compact-input {
padding: 7px 10px;
}
</style>
@@ -0,0 +1,169 @@
<script>
import KeyValue from '@shell/components/form/KeyValue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { allHash } from '@shell/utils/promise';
import { clone } from '@shell/utils/object';
import { HCI } from '../../../types';
import { NODE } from '@shell/config/types';
import { LVM_TOPOLOGY_LABEL } from '../index.vue';
const DEFAULT_PARAMETERS = [
'type',
'vgName'
];
const DEFAULT_TOPOLOGIES = [{
matchLabelExpressions: [{
key: LVM_TOPOLOGY_LABEL,
values: []
}]
}];
export default {
components: {
KeyValue,
LabeledSelect,
},
props: {
value: {
type: Object,
required: true
},
mode: {
type: String,
required: true
},
realMode: {
type: String,
required: true
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
lvmVolumeGroups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LVM_VOLUME_GROUP }),
});
},
data() {
const node = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).find(t => t.key === LVM_TOPOLOGY_LABEL)?.values[0];
return {
volumeGroupTypes: ['striped', 'dm-thin'],
node,
};
},
watch: {
node(value) {
delete (this.value.parameters.vgName);
const allowedTopologies = [...DEFAULT_TOPOLOGIES];
allowedTopologies[0].matchLabelExpressions[0].values = [value];
this.value.allowedTopologies = allowedTopologies;
}
},
computed: {
nodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE) || [];
return nodes.filter(n => n.labels[LVM_TOPOLOGY_LABEL] === n.name).map(n => n.name);
},
volumeGroups() {
const inStore = this.$store.getters['currentProduct'].inStore;
const lvmVolumeGroups = this.$store.getters[`${ inStore }/all`](HCI.LVM_VOLUME_GROUP) || [];
return lvmVolumeGroups
.filter(group => group.spec.nodeName === this.node)
.map(g => g.spec.vgName);
},
parameters: {
get() {
const parameters = clone(this.value?.parameters) || {};
DEFAULT_PARAMETERS.map((key) => {
delete parameters[key];
});
return parameters;
},
set(value) {
Object.assign(this.value.parameters, value);
}
},
},
};
</script>
<template>
<div>
<div class="row mt-10">
<div class="col span-6">
<LabeledSelect
v-model:value="node"
:label="t('harvester.storage.parameters.node.label')"
:options="nodes"
:mode="mode"
:required="true"
>
<template #no-options="{ searching }">
<span v-if="!searching" class="text-muted">
{{ t('harvester.storage.parameters.diskSelector.no-options', null, true) }}
</span>
</template>
</LabeledSelect>
</div>
</div>
<div class="row mt-10">
<div class="col span-6">
<LabeledSelect
v-model:value="value.parameters.vgName"
:label="t('harvester.storage.parameters.lvmVolumeGroup.label')"
:options="volumeGroups"
:mode="mode"
:required="true"
>
<template #no-options="{ searching }">
<span v-if="!searching" class="text-muted">
{{ t('harvester.storage.parameters.lvmVolumeGroup.no-options', null, true) }}
</span>
</template>
</LabeledSelect>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.parameters.type"
:label="t('harvester.storage.parameters.lvmVolumeGroupType.label')"
:options="volumeGroupTypes"
:mode="mode"
:required="true"
/>
</div>
</div>
<KeyValue
v-model:value="parameters"
:add-label="t('storageClass.longhorn.addLabel')"
:read-allowed="false"
:mode="mode"
class="mt-10"
/>
</div>
</template>
<style lang="scss" scoped>
.labeled-input.compact-input {
padding: 7px 10px;
}
</style>