mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 04:36:04 +08:00
Compare commits
39
Commits
v1.6.0-rc3
...
v1.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40d16d0bf3 | ||
|
|
3e013bc4fc | ||
|
|
d31979a91f | ||
|
|
3ef5a14567 | ||
|
|
1e0e2ea867 | ||
|
|
16e5fd3177 | ||
|
|
20696f6801 | ||
|
|
e5b487ebcb | ||
|
|
f058698fb5 | ||
|
|
208496d6f7 | ||
|
|
967966e852 | ||
|
|
f19809da04 | ||
|
|
bc3d9389f2 | ||
|
|
6fbdf5244e | ||
|
|
7b21f6bb2e | ||
|
|
0f6faed2dc | ||
|
|
021de41e68 | ||
|
|
65f4405d29 | ||
|
|
4bae62d046 | ||
|
|
4cc60e22a8 | ||
|
|
19b6842b21 | ||
|
|
fa78229d3a | ||
|
|
c3bfaf00ea | ||
|
|
1a176960bc | ||
|
|
e82ba9d9f5 | ||
|
|
f7fc001abd | ||
|
|
d1c676ee2c | ||
|
|
c562dd73ba | ||
|
|
1cd5e5ca7d | ||
|
|
1aaa8f71c4 | ||
|
|
003a09d0a9 | ||
|
|
f08e632aad | ||
|
|
df1d7001bc | ||
|
|
41f78c83a7 | ||
|
|
f1a53311ce | ||
|
|
e0c6e905a9 | ||
|
|
1852e434f1 | ||
|
|
77c085d3a7 | ||
|
|
9dbe7d9015 |
@@ -3,5 +3,4 @@ addReviewers: true
|
||||
numberOfReviewers: 0
|
||||
reviewers:
|
||||
- a110605
|
||||
- houhoucoop
|
||||
- NickChungSUSE
|
||||
- houhoucoop
|
||||
@@ -0,0 +1,40 @@
|
||||
# .github/release.yml
|
||||
changelog:
|
||||
categories:
|
||||
- title: Features
|
||||
labels:
|
||||
- feat
|
||||
- title: Bug Fixes
|
||||
labels:
|
||||
- fix
|
||||
- title: Dependencies
|
||||
labels:
|
||||
- deps
|
||||
- title: Performance
|
||||
labels:
|
||||
- perf
|
||||
- title: Documentation
|
||||
labels:
|
||||
- docs
|
||||
- title: Style
|
||||
labels:
|
||||
- style
|
||||
- title: Build & Continuous Integration
|
||||
labels:
|
||||
- build
|
||||
- ci
|
||||
- title: Security
|
||||
labels:
|
||||
- security
|
||||
- title: Tests
|
||||
labels:
|
||||
- test
|
||||
- title: Refactoring
|
||||
labels:
|
||||
- refactor
|
||||
- title: Chores
|
||||
labels:
|
||||
- chore
|
||||
- title: Others
|
||||
labels:
|
||||
- other
|
||||
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":semanticCommitTypeAll(deps)",
|
||||
":semanticCommitScopeDisabled"
|
||||
],
|
||||
"baseBranches": [
|
||||
"master",
|
||||
"main",
|
||||
"/^release-harvester-v\\d+\\.\\d+$/"
|
||||
],
|
||||
"automergeMajor": false,
|
||||
"semanticCommits": "enabled",
|
||||
"semanticCommitType": "chore",
|
||||
"semanticCommitScope": "deps",
|
||||
"semanticCommitType": "deps",
|
||||
"prHourlyLimit": 12,
|
||||
"timezone": "Asia/Taipei",
|
||||
"schedule": ["after 10am on sunday"],
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||
|
||||
- name: Run Helm chart releaser
|
||||
uses: helm/chart-releaser-action@v1.4.1
|
||||
uses: helm/chart-releaser-action@v1.7.0
|
||||
with:
|
||||
charts_dir: ./charts
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
name: "[PR Management] Add PR Label"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, edited]
|
||||
branches:
|
||||
- main
|
||||
- 'release-harvester-v*'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-assign-pr-label:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
- name: Setup Nodejs and yarn install
|
||||
uses: ./.github/actions/setup
|
||||
- name: Set PR label
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||
echo "PR_LABEL = $PR_LABEL"
|
||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- main
|
||||
- 'release-*'
|
||||
pull_request:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
branches:
|
||||
- main
|
||||
- 'release-*'
|
||||
@@ -22,6 +23,11 @@ jobs:
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
- name: Validate PR title lint
|
||||
if: github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
|
||||
|
||||
- name: Validate commit messages
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
+11
-10
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "harvester-ui-extension",
|
||||
"version": "1.6.0-rc3",
|
||||
"version": "1.6.1",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-class-static-block": "7.26.0",
|
||||
"@rancher/shell": "3.0.5-rc.6",
|
||||
"@babel/plugin-transform-class-static-block": "7.28.3",
|
||||
"@rancher/shell": "3.0.5-rc.8",
|
||||
"cache-loader": "^4.1.0",
|
||||
"color": "4.2.3",
|
||||
"ip": "2.0.1",
|
||||
@@ -16,21 +16,21 @@
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "~20.10.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"@types/node": "~20.19.0",
|
||||
"cronstrue": "2.59.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.10",
|
||||
"follow-redirects": "1.15.6",
|
||||
"follow-redirects": "1.15.11",
|
||||
"glob": "7.2.3",
|
||||
"glob-parent": "6.0.2",
|
||||
"json5": "2.2.3",
|
||||
"@types/lodash": "4.17.5",
|
||||
"@types/lodash": "4.17.20",
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.3.1",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.11.1",
|
||||
"roarr": "7.0.4",
|
||||
"semver": "7.5.4",
|
||||
"qs": "6.14.0",
|
||||
"roarr": "7.21.1",
|
||||
"semver": "7.7.3",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -46,6 +46,7 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/load": "^19.8.1",
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
|
||||
@@ -98,8 +98,12 @@ export default {
|
||||
return true;
|
||||
}
|
||||
|
||||
const description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
||||
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
||||
|
||||
// remove <a> tags from description
|
||||
if (description.includes('<a')) {
|
||||
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
|
||||
}
|
||||
// filter by description
|
||||
if (description.includes(searchQuery)) {
|
||||
return true;
|
||||
|
||||
@@ -258,7 +258,7 @@ export default {
|
||||
},
|
||||
|
||||
softReboot() {
|
||||
this.vmResource.softrebootVM();
|
||||
this.vmResource.doSoftReboot();
|
||||
},
|
||||
|
||||
showKeysModal() {
|
||||
@@ -372,3 +372,12 @@ export default {
|
||||
background: rgb(40, 40, 40);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.vm-console .v-popper__arrow-container {
|
||||
display: none;
|
||||
}
|
||||
.vm-console .v-popper__popper{
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,8 +53,6 @@ export default {
|
||||
},
|
||||
|
||||
namespaceOptions() {
|
||||
if (this.availableNamespaces.length === 0) return [];
|
||||
|
||||
const allSelected =
|
||||
this.namespaces.length === this.filteredNamespaces.length &&
|
||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||
|
||||
@@ -45,8 +45,11 @@ const FEATURE_FLAGS = {
|
||||
'vmNetworkMigration',
|
||||
'kubeovnVpcSubnet',
|
||||
'rancherClusterSetting',
|
||||
'cpuMemoryHotplug'
|
||||
]
|
||||
'cpuMemoryHotplug',
|
||||
'cdiSettings',
|
||||
'vmCloneRunStrategy',
|
||||
],
|
||||
'v1.6.1': [],
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { HCI } from '../types';
|
||||
|
||||
// TODO: delete this not used variable
|
||||
export const MemoryUnit = [{
|
||||
label: 'Mi',
|
||||
@@ -81,3 +83,14 @@ export const CSI_SECRETS = {
|
||||
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
||||
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
||||
};
|
||||
|
||||
// Some harvester CRD type is not equal to model file name, define the mapping here
|
||||
export const HARVESTER_CRD_MAP = {
|
||||
node: HCI.HOST,
|
||||
configmap: HCI.CLOUD_TEMPLATE,
|
||||
persistentvolumeclaim: HCI.VOLUME,
|
||||
'snapshot.storage.k8s.io.volumesnapshot': HCI.SNAPSHOT,
|
||||
// specific groupable table detail page
|
||||
'network.harvesterhci.io.vlanconfig': HCI.CLUSTER_NETWORK,
|
||||
'kubeovn.io.subnet': HCI.VPC
|
||||
};
|
||||
|
||||
@@ -72,4 +72,8 @@ export const HCI = {
|
||||
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
||||
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
|
||||
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
|
||||
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
|
||||
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
||||
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
||||
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
||||
};
|
||||
|
||||
@@ -113,7 +113,10 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
[HCI_SETTING.RANCHER_CLUSTER]: {
|
||||
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
|
||||
},
|
||||
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
||||
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
||||
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
||||
},
|
||||
};
|
||||
|
||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||
|
||||
@@ -47,20 +47,26 @@ export default {
|
||||
STATE,
|
||||
NAME,
|
||||
{
|
||||
name: 'vmCPU',
|
||||
labelKey: 'tableHeaders.cpu',
|
||||
search: false,
|
||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||
value: 'spec.template.spec.domain.cpu.cores',
|
||||
width: 120
|
||||
name: 'CPU',
|
||||
label: 'CPU',
|
||||
sort: ['displayCPU'],
|
||||
value: 'displayCPU',
|
||||
align: 'center',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
{
|
||||
name: 'vmRAM',
|
||||
labelKey: 'glance.memory',
|
||||
search: false,
|
||||
sort: ['memorySort'],
|
||||
value: 'spec.template.spec.domain.resources.limits.memory',
|
||||
width: 120
|
||||
name: 'Memory',
|
||||
value: 'displayMemory',
|
||||
sort: ['memorySort'],
|
||||
align: 'center',
|
||||
labelKey: 'tableHeaders.memory',
|
||||
formatter: 'Si',
|
||||
formatterOpts: {
|
||||
opts: {
|
||||
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
|
||||
},
|
||||
needParseSi: true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ip',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
@@ -14,7 +15,7 @@ export default {
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -26,9 +27,11 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: [],
|
||||
runStrategy: runStrategyOptions[1],
|
||||
runStrategyOptions
|
||||
};
|
||||
},
|
||||
|
||||
@@ -38,6 +41,9 @@ export default {
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
vmCloneRunStrategyEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -58,7 +64,8 @@ export default {
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
|
||||
const res = await this.actionResource.doAction('clone', payload, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
@@ -106,6 +113,13 @@ export default {
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-if="vmCloneRunStrategyEnabled"
|
||||
v-model:value="runStrategy"
|
||||
label-key="harvester.virtualMachine.runStrategy"
|
||||
:options="runStrategyOptions"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async create(buttonCb) {
|
||||
// shadow clone
|
||||
if (!this.cloneData) {
|
||||
this.resources[0].goToClone();
|
||||
buttonCb(false);
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('harvester.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this['errors'] = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this['errors'] = message;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.cloneVM.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Checkbox
|
||||
v-model:value="cloneData"
|
||||
class="mb-10"
|
||||
label-key="harvester.modal.cloneVM.type"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-show="cloneData"
|
||||
v-model:value="name"
|
||||
class="mb-20"
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
||||
:disabled="cloneData && !name"
|
||||
@click="create"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,319 @@
|
||||
<script>
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
|
||||
import { HCI_SETTING } from '../../config/settings';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
|
||||
export default {
|
||||
name: 'CDISettings',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
provisioner: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
ArrayList,
|
||||
Checkbox,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
emits: ['update:cdiSettings'],
|
||||
|
||||
async fetch() {
|
||||
const hash = {
|
||||
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
|
||||
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
|
||||
};
|
||||
|
||||
await allHash(hash);
|
||||
|
||||
if (this.mode === _CREATE ) {
|
||||
this.setDefaultVolumeSnapshotClass();
|
||||
} else {
|
||||
this.initCDISettingsFromAnnotations();
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
cdiSettings: {
|
||||
volumeModeAccessModes: [],
|
||||
volumeSnapshotClass: null,
|
||||
cloneStrategy: null,
|
||||
filesystemOverhead: null,
|
||||
},
|
||||
defaultAddValue: { volumeMode: null, accessModes: [] },
|
||||
noneOption: { label: 'None', value: '' },
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
inStore() {
|
||||
return this.$store.getters['currentProduct'].inStore;
|
||||
},
|
||||
|
||||
allVolumeModeOptions() {
|
||||
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
|
||||
},
|
||||
|
||||
selectedVolumeModes() {
|
||||
return this.cdiSettings.volumeModeAccessModes
|
||||
.map((item) => item.volumeMode)
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
volumeModeOptions() {
|
||||
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
|
||||
},
|
||||
|
||||
allVolumeModesSelected() {
|
||||
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
|
||||
},
|
||||
|
||||
accessModeOptions() {
|
||||
return [
|
||||
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
||||
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
||||
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
||||
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
|
||||
];
|
||||
},
|
||||
|
||||
cloneStrategyOptions() {
|
||||
return [
|
||||
this.noneOption,
|
||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
|
||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
|
||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
|
||||
];
|
||||
},
|
||||
|
||||
volumeSnapshotClassOptions() {
|
||||
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
|
||||
|
||||
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
|
||||
},
|
||||
|
||||
isFilesystemOverheadValid() {
|
||||
const val = this.cdiSettings.filesystemOverhead;
|
||||
|
||||
if (val === null || val === '') return true;
|
||||
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
|
||||
|
||||
return regex.test(val);
|
||||
},
|
||||
|
||||
isCustomClass() {
|
||||
return this.mode === _CREATE || this.mode === _EDIT;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
provisioner() {
|
||||
this.resetCdiSettings();
|
||||
this.$nextTick(this.setDefaultVolumeSnapshotClass);
|
||||
},
|
||||
|
||||
cdiSettings: {
|
||||
handler(val) {
|
||||
this.$emit('update:cdiSettings', val);
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setDefaultVolumeSnapshotClass() {
|
||||
try {
|
||||
const setting = this.$store.getters[`${ this.inStore }/byId`](HCI.SETTING, HCI_SETTING.CSI_DRIVER_CONFIG);
|
||||
const config = JSON.parse(setting?.value || setting?.default || '{}');
|
||||
const defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
|
||||
|
||||
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
|
||||
|
||||
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
|
||||
this.cdiSettings.volumeSnapshotClass = null;
|
||||
}
|
||||
},
|
||||
|
||||
initCDISettingsFromAnnotations() {
|
||||
const annotations = this.value?.metadata?.annotations || {};
|
||||
|
||||
let volumeModeAccessModes = [];
|
||||
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||
|
||||
if (rawVolumeMode) {
|
||||
try {
|
||||
const parsed = JSON.parse(rawVolumeMode);
|
||||
|
||||
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
|
||||
volumeMode,
|
||||
accessModes: Array.isArray(accessModes) ? accessModes : [],
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
if (volumeModeAccessModes.length) {
|
||||
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
|
||||
}
|
||||
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
|
||||
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||
}
|
||||
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
|
||||
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||
}
|
||||
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
|
||||
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||
}
|
||||
},
|
||||
|
||||
resetCdiSettings() {
|
||||
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
|
||||
this.cdiSettings.volumeSnapshotClass = null;
|
||||
this.cdiSettings.cloneStrategy = null;
|
||||
this.cdiSettings.filesystemOverhead = null;
|
||||
},
|
||||
|
||||
validateFilesystemOverhead(e) {
|
||||
const val = e.target.value;
|
||||
|
||||
this.cdiSettings.filesystemOverhead = val;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ArrayList
|
||||
v-model:value="cdiSettings.volumeModeAccessModes"
|
||||
:initial-empty-row="true"
|
||||
:show-header="true"
|
||||
:mode="mode"
|
||||
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
|
||||
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
|
||||
:default-add-value="defaultAddValue"
|
||||
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
|
||||
:add-disabled="allVolumeModesSelected"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="column-headers">
|
||||
<div
|
||||
class="row"
|
||||
:class="{ custom: isCustomClass }"
|
||||
>
|
||||
<label
|
||||
class="col span-3 value text-label mb-10"
|
||||
for="volumeMode"
|
||||
>
|
||||
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
|
||||
</label>
|
||||
<label
|
||||
class="col span-9 value text-label mb-10"
|
||||
for="accessModes"
|
||||
>
|
||||
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #columns="{ row }">
|
||||
<div class="row">
|
||||
<div class="col span-3">
|
||||
<LabeledSelect
|
||||
id="volumeMode"
|
||||
v-model:value="row.value.volumeMode"
|
||||
:mode="mode"
|
||||
:options="volumeModeOptions"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="accessModes"
|
||||
class="col span-9"
|
||||
>
|
||||
<Checkbox
|
||||
v-for="opt in accessModeOptions"
|
||||
:key="opt.value"
|
||||
v-model:value="row.value.accessModes"
|
||||
:value-when-true="opt.value"
|
||||
:label="opt.label"
|
||||
type="checkbox"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="cdiSettings.volumeSnapshotClass"
|
||||
class="select mt-20 mb-20"
|
||||
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
|
||||
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
|
||||
:mode="mode"
|
||||
:options="volumeSnapshotClassOptions"
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="cdiSettings.cloneStrategy"
|
||||
class="select mb-20"
|
||||
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
|
||||
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
|
||||
:mode="mode"
|
||||
:options="cloneStrategyOptions"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model:value="cdiSettings.filesystemOverhead"
|
||||
class="select mb-20"
|
||||
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
|
||||
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
|
||||
:mode="mode"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
|
||||
:status="isFilesystemOverheadValid ? null : 'error'"
|
||||
@input="validateFilesystemOverhead"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.column-headers .row.custom {
|
||||
max-width: calc(100% - 75px);
|
||||
}
|
||||
|
||||
.row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
max-width: 480px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
@@ -6,11 +7,9 @@ import ArrayList from '@shell/components/form/ArrayList';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import Loading from '@shell/components/Loading';
|
||||
|
||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||
import {
|
||||
@@ -22,6 +21,7 @@ import { CSI_DRIVER } from '../../types';
|
||||
import Tags from '../../components/DiskTags';
|
||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||
import CDISettings from './CDISettings';
|
||||
|
||||
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||
|
||||
@@ -45,6 +45,7 @@ export default {
|
||||
Tabbed,
|
||||
Loading,
|
||||
Tags,
|
||||
CDISettings,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
@@ -117,6 +118,7 @@ export default {
|
||||
key: '',
|
||||
values: [],
|
||||
},
|
||||
cdiSettings: {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -124,10 +126,10 @@ export default {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||
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 })
|
||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||
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 }),
|
||||
};
|
||||
|
||||
if (this.value.longhornV2LVMSupport) {
|
||||
@@ -197,6 +199,14 @@ export default {
|
||||
|
||||
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
||||
},
|
||||
|
||||
isCDISettingsFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
|
||||
},
|
||||
|
||||
shouldShowCDISettingsTab() {
|
||||
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -249,6 +259,10 @@ export default {
|
||||
});
|
||||
|
||||
this.formatAllowedTopoloties();
|
||||
|
||||
if (this.shouldShowCDISettingsTab) {
|
||||
this.formatCDISettings();
|
||||
}
|
||||
},
|
||||
|
||||
formatAllowedTopoloties() {
|
||||
@@ -270,6 +284,42 @@ export default {
|
||||
if (matchLabelExpressions.length > 0) {
|
||||
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
||||
}
|
||||
},
|
||||
formatCDISettings() {
|
||||
const annotations = this.value.metadata.annotations || {};
|
||||
const volumeModeAccessModes = {};
|
||||
|
||||
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
|
||||
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
|
||||
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(volumeModeAccessModes).length > 0) {
|
||||
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||
}
|
||||
|
||||
if (this.cdiSettings.volumeSnapshotClass) {
|
||||
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||
}
|
||||
|
||||
if (this.cdiSettings.cloneStrategy) {
|
||||
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||
}
|
||||
|
||||
if (this.cdiSettings.filesystemOverhead) {
|
||||
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||
}
|
||||
|
||||
this.value.metadata.annotations = annotations;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -404,6 +454,19 @@ export default {
|
||||
</template>
|
||||
</ArrayList>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="shouldShowCDISettingsTab"
|
||||
name="cdiSettings"
|
||||
:label="t('harvester.storage.cdiSettings.label')"
|
||||
:weight="-2"
|
||||
>
|
||||
<CDISettings
|
||||
v-model:cdi-settings="cdiSettings"
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:provisioner="value.provisioner"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
@@ -257,6 +257,7 @@ export default {
|
||||
:label="t('harvester.vmTemplate.tabs.basics')"
|
||||
>
|
||||
<CpuMemory
|
||||
class="mb-20"
|
||||
:cpu="cpu"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
|
||||
@@ -240,7 +240,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-20">
|
||||
<div>
|
||||
<LabeledSelect
|
||||
v-model:value="checkedSsh"
|
||||
:label="t('harvester.virtualMachine.input.sshKey')"
|
||||
|
||||
@@ -338,6 +338,15 @@ export default {
|
||||
saveVM(buttonCb) {
|
||||
clear(this.errors);
|
||||
|
||||
this.validateCPUMemory();
|
||||
|
||||
// block create VM flow if has validation errors
|
||||
if (this.errors.length) {
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSingle) {
|
||||
this.saveSingle(buttonCb);
|
||||
} else {
|
||||
@@ -345,6 +354,18 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
validateCPUMemory() {
|
||||
const { cpu, memory } = this;
|
||||
|
||||
if ((!cpu)) {
|
||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
|
||||
}
|
||||
|
||||
if ((!memory)) {
|
||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
|
||||
}
|
||||
},
|
||||
|
||||
async saveSingle(buttonCb) {
|
||||
this.parseVM();
|
||||
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
||||
@@ -466,9 +487,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const cpuMemoryErrors = this.getCPUMemoryValidation();
|
||||
const accessCredentialsErrors = this.getAccessCredentialsValidation();
|
||||
const errors = [...cpuMemoryErrors, ...accessCredentialsErrors];
|
||||
const errors = this.getAccessCredentialsValidation();
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(errors);
|
||||
@@ -605,6 +624,7 @@ export default {
|
||||
:label="t('harvester.virtualMachine.detail.tabs.basics')"
|
||||
>
|
||||
<CpuMemory
|
||||
class="mb-20"
|
||||
:cpu="cpu"
|
||||
:max-cpu="maxCpu"
|
||||
:memory="memory"
|
||||
|
||||
@@ -1029,7 +1029,7 @@ harvester:
|
||||
progress: Restore in progress
|
||||
complete: Restore completed
|
||||
|
||||
subnet:
|
||||
subnet:
|
||||
cidrBlock:
|
||||
tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap.
|
||||
label: CIDR Block
|
||||
@@ -1080,8 +1080,6 @@ harvester:
|
||||
placeholder: e.g. 169.254.0.1/16
|
||||
remoteVpc:
|
||||
label: Remote VPC
|
||||
|
||||
|
||||
network:
|
||||
label: Virtual Machine Networks
|
||||
tabs:
|
||||
@@ -1388,6 +1386,28 @@ harvester:
|
||||
label: Internal Storage Class
|
||||
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
|
||||
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
|
||||
cdiSettings:
|
||||
label: CDI Settings
|
||||
volumeModeAccessModes:
|
||||
label: Volume Mode / Access Modes
|
||||
tooltip: Specifies the default volume mode and access modes.
|
||||
add: Add
|
||||
volumeMode: Volume Mode
|
||||
accessModes:
|
||||
label: Access Modes
|
||||
volumeSnapshotClass:
|
||||
label: Volume Snapshot Class
|
||||
tooltip: Sets the Volume Snapshot Class name to be used when taking snapshots of PVCs under this StorageClass.
|
||||
cloneStrategy:
|
||||
label: Clone Strategy
|
||||
tooltip: Defines the clone strategy to use for volumes created with this StorageClass.
|
||||
copy: Copy
|
||||
snapshot: Snapshot
|
||||
csiClone: CSI Clone
|
||||
fileSystemOverhead:
|
||||
label: File System Overhead
|
||||
tooltip: Specifies the percentage of filesystem overhead to consider when calculating PVC size.
|
||||
placeholder: e.g. 0.05 (up to 3 decimal places, between 0 and 1)
|
||||
|
||||
vlanConfig:
|
||||
title: Network Configuration
|
||||
|
||||
@@ -687,14 +687,15 @@ export default {
|
||||
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
|
||||
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
|
||||
} else {
|
||||
this.spec.template.spec.domain.cpu.maxSockets = 1;
|
||||
this.spec.template.spec.domain.cpu.sockets = 1;
|
||||
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
||||
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
|
||||
this.spec.template.spec.domain.resources.limits.memory = this.memory;
|
||||
// clean
|
||||
delete this.spec.template.spec.resources;
|
||||
delete this.spec.template.spec.domain.memory;
|
||||
delete this.spec.template.spec.domain.cpu.maxSockets;
|
||||
// clean below fields as we don't need them if not enable CPU and memory hotplug
|
||||
if (this.spec?.template?.spec?.domain?.memory?.maxGuest) {
|
||||
delete this.spec.template.spec.domain.memory.maxGuest;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1358,21 +1359,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
getCPUMemoryValidation() {
|
||||
const errors = [];
|
||||
const { cpu, memory } = this;
|
||||
|
||||
if ((!cpu)) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
|
||||
}
|
||||
|
||||
if ((!memory)) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
getAccessCredentialsValidation() {
|
||||
const errors = [];
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import SteveModel from '@shell/plugins/steve/steve-class';
|
||||
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
import { HARVESTER_CRD_MAP } from '../config/harvester-map';
|
||||
|
||||
export default class HarvesterResource extends SteveModel {
|
||||
get listLocation() {
|
||||
const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
const resource = HARVESTER_CRD_MAP[this.type] || this.type;
|
||||
|
||||
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || {
|
||||
name,
|
||||
params: {
|
||||
product: this.$rootGetters['productId'],
|
||||
cluster: this.$rootGetters['clusterId'],
|
||||
resource: this.type,
|
||||
resource
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file is Cloud Configuration Templates page model
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { HCI } from '../../types';
|
||||
import HarvesterResource from '../harvester';
|
||||
|
||||
@@ -12,7 +12,7 @@ export default class HarvesterNetworkAttachmentDef extends NetworkAttachmentDef
|
||||
params: {
|
||||
product: HARVESTER_PRODUCT,
|
||||
cluster: this.$rootGetters['clusterId'],
|
||||
resource: this.type,
|
||||
resource: HCI.NETWORK_ATTACHMENT,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,19 @@ export default class HciClusterflow extends HarvesterFlow {
|
||||
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.CLUSTER_FLOW;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get _detailLocation() {
|
||||
const schema = this.$getters['schemaFor'](this.type);
|
||||
|
||||
|
||||
@@ -21,6 +21,19 @@ export default class HciClusteroutput extends LogOutput {
|
||||
};
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.CLUSTER_OUTPUT;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get detailLocation() {
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
|
||||
@@ -14,6 +14,19 @@ export default class HarvesterLogFlow extends Flow {
|
||||
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.FLOW;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get _detailLocation() {
|
||||
const schema = this.$getters['schemaFor'](this.type);
|
||||
|
||||
|
||||
@@ -22,6 +22,19 @@ export default class HarvesterLogOutput extends LogOutput {
|
||||
};
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.OUTPUT;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get detailLocation() {
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
|
||||
@@ -19,6 +19,19 @@ export default class HciAlertmanagerConfig extends SteveModel {
|
||||
};
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.ALERTMANAGERCONFIG;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get doneOverride() {
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
|
||||
@@ -5,6 +5,25 @@ import Secret from '@shell/models/secret';
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
|
||||
export default class HciSecret extends Secret {
|
||||
// prevent harvester secret detail page be overridden.
|
||||
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
|
||||
get fullDetailPageOverride() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.SECRET;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get _detailLocation() {
|
||||
const schema = this.$getters['schemaFor'](this.type);
|
||||
|
||||
|
||||
@@ -9,6 +9,19 @@ import { isInternalStorageClass } from '../../utils/storage-class';
|
||||
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
|
||||
|
||||
export default class HciStorageClass extends StorageClass {
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.STORAGE;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get detailLocation() {
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
@@ -94,13 +107,14 @@ export default class HciStorageClass extends StorageClass {
|
||||
get availableActions() {
|
||||
let out = super.availableActions || [];
|
||||
|
||||
out = out.map((action) => {
|
||||
if ((action.action === 'setDefault' || action.action === 'setAsDefault' || action.action === 'promptRemove') && this.isInternalStorageClass()) {
|
||||
return { ...action, enabled: false };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
if (this.isInternalStorageClass()) {
|
||||
out = out.filter((action) => {
|
||||
return !['setDefault', 'setAsDefault', 'promptRemove'].includes(action.action);
|
||||
});
|
||||
}
|
||||
if (out[0] && out[0].divider === true) {
|
||||
out = out.slice(1);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -156,4 +156,8 @@ export default class HciSetting extends HarvesterResource {
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
get disableResourceDetailDrawer() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ export default class HciVmBackup extends HarvesterResource {
|
||||
return detailLocation;
|
||||
}
|
||||
|
||||
get disableResourceDetailDrawerConfigTab() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get doneOverride() {
|
||||
const route = this.currentRoute();
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
@@ -32,8 +32,6 @@ const PAUSED_VM_MODAL_MESSAGE =
|
||||
|
||||
const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE';
|
||||
const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const POD_STATUS_NOT_READY = 'POD_NOT_READY';
|
||||
|
||||
const POD_STATUS_FAILED = 'POD_FAILED';
|
||||
const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF';
|
||||
@@ -329,6 +327,10 @@ export default class VirtVm extends HarvesterResource {
|
||||
});
|
||||
}
|
||||
|
||||
doSoftReboot() {
|
||||
this.doActionGrowl('softreboot', {});
|
||||
}
|
||||
|
||||
softrebootVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
@@ -742,6 +744,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
if (this &&
|
||||
!this.isVMExpectedRunning &&
|
||||
this.isVMCreated &&
|
||||
this.vmi?.status?.phase !== undefined &&
|
||||
this.vmi?.status?.phase !== VMIPhase.Succeeded &&
|
||||
this.vmi?.status?.phase !== VMIPhase.Pending
|
||||
) {
|
||||
@@ -1165,6 +1168,13 @@ export default class VirtVm extends HarvesterResource {
|
||||
return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
|
||||
}
|
||||
|
||||
const restoreConditions = this.restoreResource?.status?.conditions || [];
|
||||
const restoreError = restoreConditions.find((s) => s.error === true && s.reason === 'Error' && s.type === 'Ready');
|
||||
|
||||
if (restoreError?.message) {
|
||||
return restoreError.message;
|
||||
}
|
||||
|
||||
return this.ingoreVMMessage ? '' : super.stateDescription;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "harvester",
|
||||
"description": "Rancher UI Extension for Harvester",
|
||||
"version": "1.6.0-rc3",
|
||||
"version": "1.6.1",
|
||||
"private": false,
|
||||
"rancher": {
|
||||
"annotations": {
|
||||
|
||||
@@ -3,17 +3,19 @@ import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations
|
||||
export function getVmCPUMemoryValues(vm) {
|
||||
if (!vm) {
|
||||
return {
|
||||
cpu: 0,
|
||||
cpu: null,
|
||||
memory: null,
|
||||
isHotplugEnabled: false
|
||||
};
|
||||
}
|
||||
|
||||
const isHotplugEnabled = isCPUMemoryHotPlugEnabled(vm);
|
||||
const { sockets = 1, threads = 1, cores = null } = vm.spec.template.spec.domain.cpu || {};
|
||||
const cpu = cores === null ? null : sockets * threads * cores;
|
||||
|
||||
if (isHotplugEnabled) {
|
||||
return {
|
||||
cpu: vm.spec.template.spec.domain.cpu.sockets,
|
||||
cpu,
|
||||
memory: vm.spec.template.spec.domain?.memory?.guest || null,
|
||||
maxCpu: vm.spec.template.spec.domain.cpu?.maxSockets || 0,
|
||||
maxMemory: vm.spec.template.spec.domain?.memory?.maxGuest || null,
|
||||
@@ -21,7 +23,7 @@ export function getVmCPUMemoryValues(vm) {
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
cpu: vm.spec.template.spec.domain.cpu.cores,
|
||||
cpu,
|
||||
memory: vm.spec.template.spec.domain.resources?.limits?.memory || null,
|
||||
isHotplugEnabled
|
||||
};
|
||||
@@ -29,8 +31,5 @@ export function getVmCPUMemoryValues(vm) {
|
||||
}
|
||||
|
||||
export function isCPUMemoryHotPlugEnabled(vm) {
|
||||
return vm?.metadata?.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] === 'true' ||
|
||||
!!vm?.spec?.template?.spec?.domain.cpu?.maxSockets ||
|
||||
!!vm?.spec?.template?.spec?.domain?.memory?.maxGuest ||
|
||||
false;
|
||||
return vm?.metadata?.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] === 'true' || !!vm?.spec?.template?.spec?.domain?.memory?.maxGuest || false;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ export const docLink = (suffix, version) => {
|
||||
};
|
||||
|
||||
export function getVersion(v) {
|
||||
// e.g v1.4.0
|
||||
if (process.env.VUE_APP_SERVER_VERSION) {
|
||||
return process.env.VUE_APP_SERVER_VERSION;
|
||||
}
|
||||
@@ -18,20 +17,21 @@ export function getVersion(v) {
|
||||
return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`;
|
||||
} catch (error) {
|
||||
// fallback to the latest version
|
||||
return latestVersion(Object.keys(RELEASE_FEATURES));
|
||||
return getLatestVersion();
|
||||
}
|
||||
}
|
||||
|
||||
function latestVersion(versions) {
|
||||
return versions.sort((a, b) => semver.compare(a, b)).pop();
|
||||
function getLatestVersion() {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid).sort(semver.rcompare);
|
||||
|
||||
return allVersions[0] || '';
|
||||
}
|
||||
|
||||
// v1.3.3 => latest v1.3.x, v1.4.2 => latest v1.4.x
|
||||
function latestMinorVersion(v) {
|
||||
const minor = `v${ semver.major(v) }.${ semver.minor(v) }`;
|
||||
const minorVersions = Object.keys(RELEASE_FEATURES).filter((version) => version.startsWith(minor));
|
||||
function getLatestCompatibleVersion(version) {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid);
|
||||
const compatible = allVersions.filter((v) => semver.lte(v, version)).sort(semver.rcompare);
|
||||
|
||||
return latestVersion(minorVersions);
|
||||
return compatible[0] || getLatestVersion();
|
||||
}
|
||||
|
||||
export const featureEnabled = (featureKey, serverVersion) => {
|
||||
@@ -46,12 +46,16 @@ export const featureEnabled = (featureKey, serverVersion) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
let releasedFeatures = RELEASE_FEATURES[version];
|
||||
const compatibleVersion = getLatestCompatibleVersion(version);
|
||||
const releasedFeatures = RELEASE_FEATURES?.[compatibleVersion];
|
||||
|
||||
if (!releasedFeatures) {
|
||||
const fallback = latestMinorVersion(version);
|
||||
if (!Array.isArray(releasedFeatures)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Feature flags for version ${ version } are not defined. Please upgrade Harvester UI extension and check the support matrix.`
|
||||
);
|
||||
|
||||
releasedFeatures = RELEASE_FEATURES[fallback];
|
||||
return false;
|
||||
}
|
||||
|
||||
return releasedFeatures.includes(featureKey);
|
||||
|
||||
@@ -15,7 +15,7 @@ if [[ "$VERSION" != "$EXTENSION_VERSION" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${TAG_VERSION}" != "$PKG_VERSION" ]]; then
|
||||
if [[ "${TAG_VERSION}" != "${PKG_VERSION}"* ]]; then
|
||||
echo "Package version $PKG_VERSION in pkg/harvester/package.json does not match tag version ${TAG_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/* eslint-disable no-console */
|
||||
import load from "@commitlint/load";
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.error('No PR title as first argument');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const prTitle = process.argv[2];
|
||||
const prLabel = prTitle.split(':')[0].trim();
|
||||
|
||||
const config = await load({ extends: ["./commitlint.config.js"] });
|
||||
const commitPrefix = config?.rules?.['type-enum']?.[2] || [];
|
||||
const label = commitPrefix.includes(prLabel) ? prLabel : 'other';
|
||||
|
||||
console.log(label);
|
||||
Reference in New Issue
Block a user