mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-06-13 13:32:20 +00:00
feat: support filesystem volume in create VM page (#910)
* feat: add filesystem tab Signed-off-by: Andy Lee <andy.lee@suse.com> * feat: add filesystem tab in create VM page Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: update some wordings Signed-off-by: Andy Lee <andy.lee@suse.com> * feat: add support for filesystem feature flag and enable filesystem tab Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: remove unneeded wordings Signed-off-by: Andy Lee <andy.lee@suse.com> * feat: add support for filesystem feature flag and update icon button positioning Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: based on copilot review Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: remove wrong feature flag Signed-off-by: Andy Lee <andy.lee@suse.com> * fix: vm template Signed-off-by: Andy Lee <andy.lee@suse.com> --------- Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
parent
0908c7fc6b
commit
836b04f222
@ -66,10 +66,12 @@ const FEATURE_FLAGS = {
|
|||||||
'instanceManagerResourcesSetting',
|
'instanceManagerResourcesSetting',
|
||||||
'rwxNetworkSetting',
|
'rwxNetworkSetting',
|
||||||
'createPVCWithDataVolume',
|
'createPVCWithDataVolume',
|
||||||
'clusterPodSecurityStandardSetting'
|
'clusterPodSecurityStandardSetting',
|
||||||
],
|
],
|
||||||
'v1.8.1': [],
|
'v1.8.1': [],
|
||||||
'v1.9.0': [],
|
'v1.9.0': [
|
||||||
|
'supportFilesystem',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFeatureFlags = () => {
|
const generateFeatureFlags = () => {
|
||||||
|
|||||||
@ -46,3 +46,9 @@ export const CDI_POPULATOR_KIND = {
|
|||||||
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
||||||
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FILESYSTEM_SOURCE_TYPE = {
|
||||||
|
CONFIGMAP: 'configmap',
|
||||||
|
SECRET: 'secret',
|
||||||
|
SERVICEACCOUNT: 'serviceaccount',
|
||||||
|
};
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
|||||||
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
|
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
|
||||||
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
|
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
|
||||||
|
import Filesystem from './kubevirt.io.virtualmachine/VirtualMachineFilesystem';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVMTemplate',
|
name: 'HarvesterEditVMTemplate',
|
||||||
@ -51,6 +52,7 @@ export default {
|
|||||||
UnitInput,
|
UnitInput,
|
||||||
Banner,
|
Banner,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
|
Filesystem,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView, VM_MIXIN],
|
mixins: [CreateEditView, VM_MIXIN],
|
||||||
@ -95,6 +97,10 @@ export default {
|
|||||||
secretNamePrefix() {
|
secretNamePrefix() {
|
||||||
return this.templateValue?.metadata?.name;
|
return this.templateValue?.metadata?.name;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
filesystemEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -154,6 +160,7 @@ export default {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.imageId = this.diskRows[0]?.image || '';
|
this.imageId = this.diskRows[0]?.image || '';
|
||||||
|
this['filesystemRows'] = this.getFilesystemRows(this.value.spec.vm);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -349,6 +356,19 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="filesystemEnabled"
|
||||||
|
name="filesystem"
|
||||||
|
:label="t('harvester.tab.filesystem')"
|
||||||
|
:weight="-8"
|
||||||
|
>
|
||||||
|
<Filesystem
|
||||||
|
v-model:value="filesystemRows"
|
||||||
|
:mode="mode"
|
||||||
|
:namespace="templateValue.metadata.namespace"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
name="labels"
|
name="labels"
|
||||||
:label="t('generic.labels')"
|
:label="t('generic.labels')"
|
||||||
|
|||||||
@ -0,0 +1,310 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { CONFIG_MAP, SECRET, SERVICE_ACCOUNT } from '@shell/config/types';
|
||||||
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
import CopyToClipboard from '@shell/components/CopyToClipboard';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import { FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
|
const MAX_FILESYSTEMS = 3;
|
||||||
|
|
||||||
|
const { CONFIGMAP: FS_TYPE_CONFIGMAP, SECRET: FS_TYPE_SECRET, SERVICEACCOUNT: FS_TYPE_SERVICEACCOUNT } = FILESYSTEM_SOURCE_TYPE;
|
||||||
|
|
||||||
|
const DEFAULT_VOLUME_NAMES = {
|
||||||
|
[FS_TYPE_CONFIGMAP]: 'appconfigfs',
|
||||||
|
[FS_TYPE_SECRET]: 'appsecretfs',
|
||||||
|
[FS_TYPE_SERVICEACCOUNT]: 'appserviceaccountfs',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FS_TYPE_OPTIONS = [
|
||||||
|
{ label: 'ConfigMap', value: FS_TYPE_CONFIGMAP },
|
||||||
|
{ label: 'Secret', value: FS_TYPE_SECRET },
|
||||||
|
{ label: 'ServiceAccount', value: FS_TYPE_SERVICEACCOUNT },
|
||||||
|
];
|
||||||
|
|
||||||
|
function emptyRow() {
|
||||||
|
return {
|
||||||
|
fsType: FS_TYPE_CONFIGMAP,
|
||||||
|
volumeName: DEFAULT_VOLUME_NAMES[FS_TYPE_CONFIGMAP],
|
||||||
|
resourceName: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VirtualMachineFilesystem',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
LabeledSelect,
|
||||||
|
LabeledInput,
|
||||||
|
CopyToClipboard,
|
||||||
|
MessageLink,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
namespace: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { rows: this.value.length > 0 ? this.value.map((r) => ({ ...r })) : [emptyRow()] };
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
const incoming = JSON.stringify(newVal);
|
||||||
|
const current = JSON.stringify(this.rows);
|
||||||
|
|
||||||
|
if (incoming !== current) {
|
||||||
|
this.rows = newVal.map((r) => ({ ...r }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rows: {
|
||||||
|
deep: true,
|
||||||
|
handler(val) {
|
||||||
|
this.$emit('update:value', val.map((r) => ({ ...r })));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
inStore() {
|
||||||
|
return this.$store.getters['currentProduct'].inStore;
|
||||||
|
},
|
||||||
|
|
||||||
|
configMaps() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/all`](CONFIG_MAP)
|
||||||
|
.filter((cm) => !this.namespace || cm.metadata.namespace === this.namespace)
|
||||||
|
.map((cm) => ({ label: cm.metadata.name, value: cm.metadata.name }));
|
||||||
|
},
|
||||||
|
|
||||||
|
secrets() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/all`](SECRET)
|
||||||
|
.filter((s) => !this.namespace || s.metadata.namespace === this.namespace)
|
||||||
|
.map((s) => ({ label: s.metadata.name, value: s.metadata.name }));
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceAccounts() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/all`](SERVICE_ACCOUNT)
|
||||||
|
.filter((sa) => !this.namespace || sa.metadata.namespace === this.namespace)
|
||||||
|
.map((sa) => ({ label: sa.metadata.name, value: sa.metadata.name }));
|
||||||
|
},
|
||||||
|
|
||||||
|
canAddRow() {
|
||||||
|
return this.rows.length < MAX_FILESYSTEMS;
|
||||||
|
},
|
||||||
|
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
completedRows() {
|
||||||
|
return this.rows.filter((r) => r.fsType && r.volumeName && r.resourceName);
|
||||||
|
},
|
||||||
|
|
||||||
|
allMountCommands() {
|
||||||
|
return this.completedRows.map((r) => this.mountCommands(r)).join('\n');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fsTypeOptions(currentIndex) {
|
||||||
|
const usedTypes = this.rows
|
||||||
|
.filter((_, i) => i !== currentIndex)
|
||||||
|
.map((r) => r.fsType);
|
||||||
|
|
||||||
|
return FS_TYPE_OPTIONS.filter((opt) => !usedTypes.includes(opt.value));
|
||||||
|
},
|
||||||
|
|
||||||
|
resourceOptions(fsType) {
|
||||||
|
if (fsType === FS_TYPE_CONFIGMAP) return this.configMaps;
|
||||||
|
if (fsType === FS_TYPE_SECRET) return this.secrets;
|
||||||
|
if (fsType === FS_TYPE_SERVICEACCOUNT) return this.serviceAccounts;
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
onFsTypeChange(row, newType) {
|
||||||
|
row.fsType = newType;
|
||||||
|
row.volumeName = DEFAULT_VOLUME_NAMES[newType] || '';
|
||||||
|
row.resourceName = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
addRow() {
|
||||||
|
if (this.canAddRow) {
|
||||||
|
const usedTypes = this.rows.map((r) => r.fsType);
|
||||||
|
const nextType = FS_TYPE_OPTIONS.find((opt) => !usedTypes.includes(opt.value))?.value || FS_TYPE_CONFIGMAP;
|
||||||
|
|
||||||
|
this.rows.push({
|
||||||
|
fsType: nextType,
|
||||||
|
volumeName: DEFAULT_VOLUME_NAMES[nextType] || '',
|
||||||
|
resourceName: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeRow(index) {
|
||||||
|
this.rows.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
mountCommands(row) {
|
||||||
|
const vol = row.volumeName || '<volume-name>';
|
||||||
|
|
||||||
|
return `- mkdir -p /mnt/${ vol }\n- mount -t virtiofs ${ vol } /mnt/${ vol }`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="vm-filesystem">
|
||||||
|
<p class="mb-20">
|
||||||
|
{{ t('harvester.virtualMachine.filesystem.description') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(row, index) in rows"
|
||||||
|
:key="index"
|
||||||
|
class="filesystem-row mb-15"
|
||||||
|
>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledSelect
|
||||||
|
:value="row.fsType"
|
||||||
|
:label="t('harvester.virtualMachine.filesystem.type')"
|
||||||
|
:options="fsTypeOptions(index)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
@update:value="onFsTypeChange(row, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="row.volumeName"
|
||||||
|
:label="t('harvester.virtualMachine.filesystem.volume')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-5">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="row.resourceName"
|
||||||
|
:label="t('harvester.virtualMachine.filesystem.resource')"
|
||||||
|
:options="resourceOptions(row.fsType)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isView"
|
||||||
|
class="col span-1 remove-col"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-link remove-btn"
|
||||||
|
@click="removeRow(index)"
|
||||||
|
>
|
||||||
|
{{ t('generic.remove') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-if="completedRows.length > 0"
|
||||||
|
color="warning"
|
||||||
|
class="mt-10"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<MessageLink
|
||||||
|
:to="{ hash: '#advanced' }"
|
||||||
|
prefix-label="harvester.virtualMachine.filesystem.mountBannerHint"
|
||||||
|
middle-label="harvester.virtualMachine.filesystem.mountBannerHintLink"
|
||||||
|
suffix-label="harvester.virtualMachine.filesystem.mountBannerHintSuffix"
|
||||||
|
/>
|
||||||
|
<div class="pre-wrapper mt-10">
|
||||||
|
<pre class="mt-5 mb-0">{{ allMountCommands }}</pre>
|
||||||
|
<CopyToClipboard
|
||||||
|
:text="allMountCommands"
|
||||||
|
:show-label="false"
|
||||||
|
class="icon-btn"
|
||||||
|
action-color="bg-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="!isView && canAddRow"
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
@click="addRow"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.filesystem.add') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vm-filesystem {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filesystem-row {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-col {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-wrapper {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding-right: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 5px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--success) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -37,6 +37,7 @@ import Network from './VirtualMachineNetwork';
|
|||||||
import Volume from './VirtualMachineVolume';
|
import Volume from './VirtualMachineVolume';
|
||||||
import SSHKey from './VirtualMachineSSHKey';
|
import SSHKey from './VirtualMachineSSHKey';
|
||||||
import Reserved from './VirtualMachineReserved';
|
import Reserved from './VirtualMachineReserved';
|
||||||
|
import Filesystem from './VirtualMachineFilesystem';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ export default {
|
|||||||
Banner,
|
Banner,
|
||||||
MessageLink,
|
MessageLink,
|
||||||
UsbDevices,
|
UsbDevices,
|
||||||
|
Filesystem,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView, VM_MIXIN],
|
mixins: [CreateEditView, VM_MIXIN],
|
||||||
@ -218,6 +220,9 @@ export default {
|
|||||||
usbPassthroughEnabled() {
|
usbPassthroughEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
||||||
},
|
},
|
||||||
|
filesystemEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -321,6 +326,7 @@ export default {
|
|||||||
const diskRows = this.getDiskRows(this.value);
|
const diskRows = this.getDiskRows(this.value);
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
|
this['filesystemRows'] = this.getFilesystemRows(this.value);
|
||||||
const templateId = this.$route.query.templateId;
|
const templateId = this.$route.query.templateId;
|
||||||
const templateVersionId = this.$route.query.versionId;
|
const templateVersionId = this.$route.query.versionId;
|
||||||
|
|
||||||
@ -783,10 +789,23 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="filesystemEnabled"
|
||||||
|
name="filesystem"
|
||||||
|
:label="t('harvester.tab.filesystem')"
|
||||||
|
:weight="-9"
|
||||||
|
>
|
||||||
|
<Filesystem
|
||||||
|
v-model:value="filesystemRows"
|
||||||
|
:mode="mode"
|
||||||
|
:namespace="value.metadata.namespace"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
name="labels"
|
name="labels"
|
||||||
:label="t('generic.labels')"
|
:label="t('generic.labels')"
|
||||||
:weight="-9"
|
:weight="-10"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.virtualMachine.labels.banner" />
|
<t k="harvester.virtualMachine.labels.banner" />
|
||||||
@ -805,7 +824,7 @@ export default {
|
|||||||
<Tab
|
<Tab
|
||||||
name="instanceLabel"
|
name="instanceLabel"
|
||||||
:label="t('harvester.tab.instanceLabel')"
|
:label="t('harvester.tab.instanceLabel')"
|
||||||
:weight="-10"
|
:weight="-11"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
||||||
@ -826,7 +845,7 @@ export default {
|
|||||||
<Tab
|
<Tab
|
||||||
name="annotations"
|
name="annotations"
|
||||||
:label="t('harvester.tab.annotations')"
|
:label="t('harvester.tab.annotations')"
|
||||||
:weight="-11"
|
:weight="-12"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.virtualMachine.annotations.banner" />
|
<t k="harvester.virtualMachine.annotations.banner" />
|
||||||
@ -847,7 +866,7 @@ export default {
|
|||||||
<Tab
|
<Tab
|
||||||
name="advanced"
|
name="advanced"
|
||||||
:label="t('harvester.tab.advanced')"
|
:label="t('harvester.tab.advanced')"
|
||||||
:weight="-12"
|
:weight="-13"
|
||||||
>
|
>
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
|
|||||||
@ -355,6 +355,7 @@ harvester:
|
|||||||
snapshots: Snapshots
|
snapshots: Snapshots
|
||||||
instanceLabel: Instance Labels
|
instanceLabel: Instance Labels
|
||||||
annotations: Annotations
|
annotations: Annotations
|
||||||
|
filesystem: Filesystem Volume
|
||||||
fields:
|
fields:
|
||||||
version: Version
|
version: Version
|
||||||
name: Name
|
name: Name
|
||||||
@ -830,6 +831,15 @@ harvester:
|
|||||||
username: Username
|
username: Username
|
||||||
password: Password
|
password: Password
|
||||||
reservedMemory: Reserved Memory
|
reservedMemory: Reserved Memory
|
||||||
|
filesystem:
|
||||||
|
description: Harvester supports filesystem volumes for VM via virtiofs.
|
||||||
|
type: Filesystem Type
|
||||||
|
volume: Volume
|
||||||
|
resource: Resource
|
||||||
|
add: Add
|
||||||
|
mountBannerHint: "Please update the mount path (e.g. /mnt/appconfigfs) to your preferred location, then add the corresponding commands to the"
|
||||||
|
mountBannerHintLink: "runcmd"
|
||||||
|
mountBannerHintSuffix: "in Advanced tab User Data."
|
||||||
machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?'
|
machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?'
|
||||||
detail:
|
detail:
|
||||||
tabs:
|
tabs:
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { base64Decode } from '@shell/utils/crypto';
|
|||||||
import { formatSi, parseSi } from '@shell/utils/units';
|
import { formatSi, parseSi } from '@shell/utils/units';
|
||||||
import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
import {
|
import {
|
||||||
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
|
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, SERVICE_ACCOUNT, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
|
||||||
} from '@shell/config/types';
|
} from '@shell/config/types';
|
||||||
import { HOSTNAME } from '@shell/config/labels-annotations';
|
import { HOSTNAME } from '@shell/config/labels-annotations';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
@ -25,7 +25,7 @@ import { HCI } from '../../types';
|
|||||||
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
||||||
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
||||||
import { GIBIBYTE } from '../../utils/unit';
|
import { GIBIBYTE } from '../../utils/unit';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||||
|
|
||||||
@ -102,6 +102,8 @@ export default {
|
|||||||
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
|
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
|
||||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||||
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
||||||
|
configMaps: this.$store.dispatch(`${ inStore }/findAll`, { type: CONFIG_MAP }),
|
||||||
|
serviceAccounts: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE_ACCOUNT }),
|
||||||
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||||
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
|
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
|
||||||
};
|
};
|
||||||
@ -156,6 +158,7 @@ export default {
|
|||||||
imageId: '',
|
imageId: '',
|
||||||
diskRows: [],
|
diskRows: [],
|
||||||
networkRows: [],
|
networkRows: [],
|
||||||
|
filesystemRows: [],
|
||||||
machineType: '',
|
machineType: '',
|
||||||
machineTypes: [],
|
machineTypes: [],
|
||||||
secretName: '',
|
secretName: '',
|
||||||
@ -440,6 +443,7 @@ export default {
|
|||||||
this['imageId'] = imageId;
|
this['imageId'] = imageId;
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
|
this['filesystemRows'] = this.getFilesystemRows(vm);
|
||||||
|
|
||||||
this.refreshYamlEditor();
|
this.refreshYamlEditor();
|
||||||
},
|
},
|
||||||
@ -641,6 +645,80 @@ export default {
|
|||||||
this.parseAccessCredentials();
|
this.parseAccessCredentials();
|
||||||
this.parseNetworkRows(this.networkRows);
|
this.parseNetworkRows(this.networkRows);
|
||||||
this.parseDiskRows(this.diskRows);
|
this.parseDiskRows(this.diskRows);
|
||||||
|
this.parseFilesystemRows();
|
||||||
|
},
|
||||||
|
|
||||||
|
getFilesystemRows(vm) {
|
||||||
|
const _filesystems = vm.spec.template.spec.domain.devices?.filesystems || [];
|
||||||
|
const _volumes = vm.spec.template.spec.volumes || [];
|
||||||
|
|
||||||
|
return _filesystems.map((fs) => {
|
||||||
|
const volume = _volumes.find((v) => v.name === fs.name);
|
||||||
|
let fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
|
||||||
|
let resourceName = '';
|
||||||
|
|
||||||
|
if (volume?.configMap) {
|
||||||
|
fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
|
||||||
|
resourceName = volume.configMap.name;
|
||||||
|
} else if (volume?.secret) {
|
||||||
|
fsType = FILESYSTEM_SOURCE_TYPE.SECRET;
|
||||||
|
resourceName = volume.secret.secretName;
|
||||||
|
} else if (volume?.serviceAccount) {
|
||||||
|
fsType = FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT;
|
||||||
|
resourceName = volume.serviceAccount.serviceAccountName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fsType,
|
||||||
|
volumeName: fs.name,
|
||||||
|
resourceName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
parseFilesystemRows() {
|
||||||
|
const completedRows = this.filesystemRows.filter(
|
||||||
|
(r) => r.fsType && r.volumeName && r.resourceName
|
||||||
|
);
|
||||||
|
|
||||||
|
const filesystems = completedRows.map((r) => ({
|
||||||
|
name: r.volumeName,
|
||||||
|
virtiofs: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const fsVolumes = completedRows.map((r) => {
|
||||||
|
if (r.fsType === FILESYSTEM_SOURCE_TYPE.CONFIGMAP) {
|
||||||
|
return {
|
||||||
|
name: r.volumeName,
|
||||||
|
configMap: { name: r.resourceName },
|
||||||
|
};
|
||||||
|
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SECRET) {
|
||||||
|
return {
|
||||||
|
name: r.volumeName,
|
||||||
|
secret: { secretName: r.resourceName },
|
||||||
|
};
|
||||||
|
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT) {
|
||||||
|
return {
|
||||||
|
name: r.volumeName,
|
||||||
|
serviceAccount: { serviceAccountName: r.resourceName },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
if (filesystems.length > 0) {
|
||||||
|
this.spec.template.spec.domain.devices['filesystems'] = filesystems;
|
||||||
|
} else {
|
||||||
|
delete this.spec.template.spec.domain.devices['filesystems'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fsVolumes.length > 0) {
|
||||||
|
if (!this.spec.template.spec.volumes) {
|
||||||
|
this.spec.template.spec['volumes'] = [];
|
||||||
|
}
|
||||||
|
this.spec.template.spec.volumes.push(...fsVolumes);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseOther() {
|
parseOther() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user