Compare commits

..
Author SHA1 Message Date
Andy Lee 5d996c42dc ci: allow any string as commit scope (#571)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-23 14:18:02 +08:00
Andy Lee 88cd302ce4 fix: trunk input field is missing when creating L2VlanTrunkNetwork (#570)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-23 09:47:47 +08:00
Yiya Chen e6dd8d6771 feat: online Volume Resizing from the VM Page (#568)
* feat: add conditions tab
* feat: ignore restart for resizing
* refactor: remove unused code
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-22 17:33:28 +08:00
Andy Lee 1a3822881e fix: view and edit mode for l2vlan trunk (#563)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-20 14:52:32 +08:00
renovate[bot] ee8bb21a10 deps: update dependency @types/node to v20.19.22 (#564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 10:44:54 +08:00
Yiya Chen e3d30a0eec feat: make size editable (#562)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-17 15:36:36 +08:00
Andy Lee 532b6c4d50 feat: add access / trunk mode in create VM network page (#510)
* feat: add l2VlanTrunkMode feature

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: remove unneeded code

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: fix edit l2vlan trunk mode edit page

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: hide Route tab when trunk mode

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-17 15:28:21 +08:00
Yiya Chen f22644e058 fix: reset filesystem (#559)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-17 12:15:58 +08:00
Andy Lee cd128d0444 chore: limit v1.7.0 to install on Rancher 2.13.0+ (#558)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-15 17:38:09 +08:00
Yiya Chen 0278f51260 fix: WebVNC console shortcut keys not working correctly (#557)
* style: disable overflow

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* style: fix class name

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-13 16:03:16 +08:00
Andy Lee 98efd63110 fix: abort upload when switch to URL option (#552)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-13 14:48:37 +08:00
renovate[bot] 78e78ac8dd deps: update patch digest dependencies (#553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 22:27:42 +08:00
Yiya Chen 2aaa0a55a2 feat: add default value (#548)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-09 13:37:06 +08:00
Andy Lee 5c2a23924d fix: allow to edit as yaml if has empty CPU or memory (#547)
* fix: allow edit as yaml in create VM page if empty CPU or memory

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: remove getCPUMemoryValidation

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-08 16:11:27 +08:00
Yiya Chen 7af8a82838 feat: remove machine type dropdown (#546)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-08 13:41:35 +08:00
Caio Torres 3b343bcaca fix: create new secret on vm creation (#537)
Signed-off-by: Caio Torres <caio.torres@suse.com>
2025-10-07 17:23:18 +08:00
renovate[bot] 6ddd35d661 deps: update dependency @types/node to v20.19.19 (#542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 12:01:21 +08:00
Andy Lee bd28ba6f71 feat: add network policy page (#536)
* feat: add Network Policiies page

Signed-off-by: Andy Lee <andy.lee@suse.com>

* ci: add build

Signed-off-by: Andy Lee <andy.lee@suse.com>

* ci: ensure FROM and TO exist

Signed-off-by: Andy Lee <andy.lee@suse.com>

* ci: skip commitlint if FROM and TO emtpy

Signed-off-by: Andy Lee <andy.lee@suse.com>

* revert: unnecessary change

* feat: add banner

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-03 09:38:31 +08:00
Andy Lee 4bb67153ce fix: set maxSockets : 1 if not enabled cpuMemoryhotplug (#539) 2025-10-02 17:50:16 +08:00
Andy Lee 7dbd442519 fix: force fetch VM data (#535) 2025-09-25 18:55:47 +08:00
Andy Lee 5a0d7f283d fix: unable to create VM when choosing L2VlanNetwork (#534)
* fix: unable to create VM via l2vlan network

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: based on discussion

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: add comment

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-23 16:32:39 +08:00
Andy Lee 9fdbe9c58f feat: add ACL tab in create subnet page (#527)
* feat: add ACL tab in create subnet page

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: typo

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-22 16:13:13 +08:00
renovate[bot] f652ed9d4b deps: update dependency @types/node to v20.19.17 (#530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 10:11:21 +08:00
Andy Lee 21a1cd4e89 feat: skip ksmtuned updated if there no related change (#526)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-22 10:03:34 +08:00
renovate[bot] facc74ca51 deps: update dependency @types/node to v20.19.14 (#522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 10:14:17 +08:00
Andy Lee 74c12a0d1b feat: add info banner static route tab (#520) 2025-09-13 14:00:18 +08:00
Andy Lee 3277ab4a2b refactor: remove ui-plugin-index setting (#518)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:55:23 +08:00
Andy Lee 4aabf0b7a3 fix: hide VM take backup action if backup target is not available (#512)
* fix: hide VM take backup action if backup target is not available

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: use extracted func

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:36:23 +08:00
Andy Lee 4e8cb31e9d fix: unable to attach Ti volume in create VM page (#513)
* fix: unable to attach Ti volume in create VM page

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: only consider Ti / Gi for attached volume

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:27:40 +08:00
renovate[bot] 1b214b2b6f deps: update dependency @types/node to v20.19.13 (#514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 14:39:13 +08:00
Andy Lee 748c88866a feat: display N/A in vlanID for overlay network (#511)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-06 11:21:16 +08:00
Andy Lee c3e5c2161e feat: add DHCP ui config in subnet page (#504)
* feat: add dhcp ui setting in subnet page

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add dhcp option banner link

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-03 12:04:20 +08:00
Andy Lee f932afee68 refactor: disable the setting detail drawer (#507)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-02 17:47:53 +08:00
renovate[bot] 0bbdf3bf17 deps: update minor dependencies (#493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 14:44:52 +08:00
renovate[bot] bdf3ac2803 deps: update patch digest dependencies (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 12:14:18 +08:00
Andy Lee 0d183a8174 ci: update renovate.json to remove semanticCommitScope (#506)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-02 12:09:14 +08:00
Andy Lee 8f72c33f4b ci: update renovate.json to let PR title start with deps: (#505) 2025-09-02 11:07:49 +08:00
Caio Torres 22c99211f1 fix: preserve YAML device parameters on config editing (#487)
Signed-off-by: Caio Torres <caio.torres@suse.com>
2025-09-01 14:20:12 +08:00
Andy Lee 775330d829 feat: add reconnect button in VNC console (#489)
* feat: add reconnect button in VNC console

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: reconnnect logic

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: call reconnnect

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-01 13:51:56 +08:00
freeze 18be022a9f chore: modify the check-version script (#490)
- to pass the dev build

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2025-08-31 15:11:35 +08:00
Andy Lee 56b4b46b5a chore: add v1.6.1 and v1.7.0 feature flags (#483)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-27 15:50:17 +08:00
Andy Lee db398ecad3 chore: bump version to v1.7.0-dev (#482)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-26 14:54:30 +08:00
Andy Lee d175e3b11a chore: bump version to v1.6.0 (#480)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-26 11:17:58 +08:00
Andy Lee 905db4b12c fix: reboot button not work in VNC console (#475)
* fix: reboot button in VNC console

Signed-off-by: Andy Lee <andy.lee@suse.com>

* style: hide the arrow in VNC console

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-25 18:07:26 +08:00
Andy Lee fc31b4bb9d ci: setup env using actions/setup (#478)
* ci: setup env using actions/setup

Signed-off-by: Andy Lee <andy.lee@suse.com>

* ci: rerun lint if PR force push

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-25 18:04:32 +08:00
Andy Lee db29a7c31b ci: correct set PR label script name (#476)
* fix: set label script name

Signed-off-by: Andy Lee <andy.lee@suse.com>

* ci: rerun PR label after PR edited

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-25 17:40:03 +08:00
Andy Lee 2b3541164d ci: add .github/release.yml (#468)
* ci: add release.yml

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: update release.yml

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 15:50:11 +08:00
Andy Lee ea5e9aa1f4 ci: auto add PR label workflow and script (#466)
* ci: add auto PR label workflow and script

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: bug fix prefix

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: update label mapping

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: read commitlint prefix from commitlint.config.js

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: rename to scripts/extract-release-label.mjs

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 15:45:54 +08:00
Andy Lee 18a5608e72 ci: only run PR lint in PR stage (#471)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 14:42:50 +08:00
Andy Lee 18599fc94c deps: upgrade @rancher/shell to 3.0.5-rc.8 (#469)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 10:40:08 +08:00
Andy Lee e155d46483 ci: add PR title lint (#458)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 10:38:39 +08:00
Andy Lee 795e5d178f fix: resource page detail title link (#459)
* fix: resource page detail title link

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: extract harvester type in a map

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: put in config/harvester-map.js

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-15 16:48:38 +08:00
Andy Lee 2e03ac3cf7 fix: spec.domain.memory.guest is lost issue (#462)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-15 11:58:11 +08:00
Andy Lee 76605fdc07 refactor: remove unneeded divider in storage class action menu (#460)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-15 10:51:13 +08:00
Andy Lee c289b001db chore: bump version to v1.6.0-rc6 (#456)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-13 15:05:16 +08:00
Yiya Chen e8d63da1eb fix: fallback default value (#454)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-08-12 13:53:09 +08:00
Andy Lee 4a456ac07d chore: update reviewer (#452)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-05 12:08:06 +08:00
Andy Lee 57dfddd593 chore: bump to 1.6.0-rc5 (#450)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-04 21:31:31 +08:00
Andy Lee 66eac96575 feat: add restore failed message in VM state description (#448)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-04 14:09:26 +08:00
Andy Lee 182d92d80b fix: featureFlags is undefined when installing lower version of ui-ext (#443)
* fix: featureFlags is undefined if install lower version of ui-ext

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-01 15:31:20 +08:00
Yiya Chen 1b362f570b fix: update cdiSettings (#444)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-08-01 12:04:14 +08:00
Andy Lee cf798048f8 chore: bump version to v1.6.0-rc4 (#439)
* chore: bump version to v1.6.0-rc4

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: missing vm network migration setting

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-30 16:05:29 +08:00
Andy Lee 93c8399935 fix: cpu/ memory columns in host detail VM tab (#436)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-29 16:58:22 +08:00
Andy Lee b72f523ddf fix: isStopping state condition (#431)
* fix: isStopping state condition

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: remove unused variable

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-29 12:58:38 +08:00
Andy Lee e5ffb08df3 chore: upgrade rancher/shell to 3.0.5-rc.7 (#429)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-28 18:33:37 +08:00
Andy Lee 98a6322c11 refactor: filter anchor link in setting description when searching (#428)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-28 18:19:24 +08:00
Andy Lee be7e4bd80b feat: add runStragety field when cloning a VM (#424)
* feat: add runStragety field when cloning a VM

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: remove unneeded change

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-28 14:18:03 +08:00
Yiya ChenandAndy Lee ed2bc3100b feat: expose CDI settings (#418)
* feat: add cdi settings tab

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: basic layout

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: add fields mutation

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: rename keys

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: add edit mode

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: remove isCreate

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: filter volume mode options

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-25 13:55:54 +08:00
Yiya Chen b6ffb3e9f1 fix: exception error (#422)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-24 12:37:56 +08:00
Andy Lee 3d8a6bba7e chore: bump version to 1.6.0-rc3 (#420)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-23 15:00:28 +08:00
Andy Lee be9311dc0c feat: CPU / Memory hotplug support (#413)
* feat: add maxCPU and maxMemory

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add hotplug dialog

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add restart message

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: let VM template support cpuMemoryHotplug

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add feature flag

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add max-hotplug-ratio setting

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-23 14:08:59 +08:00
Andy Lee e294f4c00f refactor: remove unneeded translation keys (#416)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:41:40 +08:00
Nick ChungandAndy Lee a9fa928912 feat: Remove guest cluster in Rancher (#391)
* feat: Remove guest cluster in Rancher

Signed-off-by: Nick Chung <nick.chung@suse.com>

* feat: add feature flag 1.6.0

Signed-off-by: Nick Chung <nick.chung@suse.com>

* feat: update for review

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: fix for review

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: fix for review

Signed-off-by: Nick Chung <nick.chung@suse.com>

* refactor: reduce redundant code

Signed-off-by: Andy Lee <andy.lee@suse.com>

* chore: change text area to yaml editor

Signed-off-by: Nick Chung <nick.chung@suse.com>

* refactor: change radio and yaml editor position

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:25:51 +08:00
Yiya Chen ef2b4d1589 feat: disable vm volume resize (#409)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-17 18:29:02 +08:00
Andy Lee a73e9f0ac1 ci: disable subject-max-length commitlint rule (#408)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-17 13:51:06 +08:00
Andy Lee 11b3bf4c1f fix: lint in config/types.js (#407)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 19:09:25 +08:00
Nick Chung b775ce5f50 chore: disable vmstate-persistence and longhorn-static StorageClasses (#377)
* chore: disable vmstate-persistence and longhorn-static StorageClasses

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: allow internal storage class deletions in image and volumn

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: remove deletion tooltips in image and volume pages

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: rollback style changes of image and volume lists

Signed-off-by: Nick Chung <nick.chung@suse.com>

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-07-16 18:21:06 +08:00
Andy Lee 193d4536e3 chore: bump version to v1.6.0-rc2 (#403)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 18:09:34 +08:00
Andy Lee 9ca9fdb521 feat: support for subnets and VPCs from UI (#374)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 17:51:37 +08:00
Yiya Chen bcabefe9f3 fix: add default value to prevent exception (#397)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-16 15:16:40 +08:00
Andy Lee c541f81dc3 ci: add package version check (#396)
* ci: add package version check

Signed-off-by: Andy Lee <andy.lee@suse.com>

* ci: extract check version in a script

Signed-off-by: Andy Lee <andy.lee@suse.com>

* ci: update env variable

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 13:17:05 +08:00
Yiya Chen 4486f71c8f feat: add vm-migration-network setting (#395)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-16 10:13:07 +08:00
Yiya Chen 060105ead3 chore: upgrade rancher/shell to 3.0.5-rc6 (#393)
* chore: bump shell rc6
* chore: add rancher restriction

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-14 09:56:14 +08:00
Andy Lee 3ea73978ee fix: find the correct vmim based on creationTimeStamp (#389)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-10 11:16:45 +08:00
Yiya Chen f4e363396d feat: add csi-online-expand-validation setting (#378)
* feat: add csi-online-expand-validation setting
* feat: invalid json error message
* feat: handle API errors
* refactor: remove inStore()

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-09 15:26:56 +08:00
Andy Lee fa16e24983 chore: bump version to v1.6.0-rc1 (#385)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-09 14:34:36 +08:00
Andy Lee ce63ee6a19 fix: read volume accessMode even is RWO (#381)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-09 11:58:21 +08:00
Yiya Chen ee28697161 ci: ignore draft PR add add backport labels (#382)
* ci: skip draft PR

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* ci: fix auto assign

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* ci: add backport label

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* fix: remove token

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-09 11:34:30 +08:00
Yiya Chen 85a06feb91 feat: disable vm images (#376)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-08 15:23:32 +08:00
Yiya Chen 17dd46cee9 ci: add commands_restrictions (#375)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-03 17:46:11 +08:00
Nick Chung 0ef4ff65cc fix: can't migrate network config in Rancher integration (#371)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-07-03 16:52:07 +08:00
Yiya Chen cebb302730 ci: use pull_request_target (#370)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-03 14:00:31 +08:00
Andy Lee f12717a8f4 fix: add custom promptRemove for backup and snasphot deletion (#367) 2025-07-03 11:51:53 +08:00
Yiya Chen 03f54643fd ci: remove action-add-labels (#368)
* ci: remove action-add-labels
* ci: use gh cli
* ci: use pull_request_target
* ci: add ref

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-02 14:46:03 +08:00
Yiya Chen 7fb6d44208 ci: add backport label on PR creation (#365)
* ci: improve backport workflow
* chore: ignore tmp files
* ci: add exact match
* ci: target main branch
* ci: rewrite workflows

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-02 11:39:02 +08:00
Nick Chung dc683a50a4 feat: [ENHANCEMENT] Add setting upgrade-config to UI(#8369) (#347)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-07-01 11:14:56 +08:00
Yiya Chen be421054d8 feat: SB Enhancements (NS selection and timeout) (#345)
* feat: add namespace field
* feat: add optional inputs
* feat: refine code
* feat: add feature flag
* refactor: fix lint error
* feat: filter default namespaces
* refactor: hide tips with feature flag
* refactor: use UnitInput
* feat: load default value from settings
* refactor: fix API url
* refactor: no available namespaces
* chore: update subject-case rule
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-01 09:38:22 +08:00
Andy Lee fcef0391bb ci: replace tag with github.event.release.tag_name (#356)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-27 17:29:37 +08:00
Andy Lee 1d89aafeab fix: cloned secret should keep originl type (#354)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-27 15:55:47 +08:00
Nick Chung 7386a2deb6 chore: remove run-lint from build-standalone-on-release (#355)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-06-27 12:41:22 +08:00
Andy Lee 26f5ebc8e2 ci: auto open backport pr to v1.6 release branch (#353)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-26 17:01:00 +08:00
68 changed files with 2606 additions and 2239 deletions
+1
View File
@@ -14,3 +14,4 @@ dist-pkg
pkg/harvester/index.ts
pkg/harvester/store/*
scripts/pkgfile.js
tmp
+1 -2
View File
@@ -3,5 +3,4 @@ addReviewers: true
numberOfReviewers: 0
reviewers:
- a110605
- houhoucoop
- NickChungSUSE
- houhoucoop
+12 -31
View File
@@ -1,33 +1,14 @@
pull_request_rules:
- name: Automatically open backport PR to release-harvester-v1.5
conditions:
- base=main
- label="require backport 1.5"
actions:
backport:
branches:
- "release-harvester-v1.5"
assignees:
- "{{ author }}"
labels:
- "v1.5 backport PR"
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
- name: Automatically open backport PR to release-harvester-v1.0
conditions:
- base=main
- label="require backport 1.0"
actions:
backport:
branches:
- "release-harvester-v1.0"
assignees:
- "{{ author }}"
labels:
- "v1.0 backport PR"
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
commands_restrictions:
backport:
conditions:
- or:
- sender-permission>=write
- sender=github-actions[bot]
+40
View File
@@ -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
+6 -4
View File
@@ -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"],
+49
View File
@@ -0,0 +1,49 @@
name: "[PR Management] Request Backport via Mergify"
on:
pull_request_target:
types: [closed]
branches: [main]
permissions:
pull-requests: write
jobs:
comment-backport:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Post Mergify backport command
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
pr_number=${{ github.event.pull_request.number }}
repo="${{ github.repository }}"
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
labels=$(echo "$labels_json" | jq -r '.[] // empty')
echo "Labels on PR: $labels"
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
if [[ -z "$matches" ]]; then
echo "No back‑port labels found — skipping."
exit 0
fi
branches=$(echo "$matches" \
| sed -E 's/^require backport\/v/release-harvester-v/' \
| sort -u | tr '\n' ' ')
branches=${branches%% }
cmd="@Mergifyio backport $branches"
echo "Posting Mergify command: $cmd"
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
@@ -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:
+30
View File
@@ -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
View File
@@ -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: |
@@ -44,6 +50,7 @@ jobs:
echo "FROM=$FROM"
echo "TO=$TO"
npx commitlint --from "$FROM" --to "$TO" --verbose
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
-1
View File
@@ -138,7 +138,6 @@ To validate changes in a release series, switch to the appropriate `vX.Y-head` b
- Examples:
- Test `1.0.x` series → `v1.0-head`
- Test `1.5.x` series → `v1.5-head`
- Test `1.6.x` series → `v1.6-head`
**Steps:**
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
+4
View File
@@ -22,6 +22,10 @@ module.exports = {
'security', // Security fixes
]
],
// Allows the scope to be empty. [0] means the rule is disabled.
'scope-empty': [0, 'always'],
// Allows any string for the scope.
'scope-case': [2, 'always', 'kebab-case'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'subject-case': [0, 'never'],
+11 -10
View File
@@ -1,13 +1,13 @@
{
"name": "harvester-ui-extension",
"version": "1.6.0-rc3",
"version": "1.7.0-dev",
"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",
+5 -2
View File
@@ -11,7 +11,6 @@ const CATEGORY = {
ui: [
'branding',
'ui-source',
'ui-plugin-index',
'ui-index',
]
};
@@ -98,8 +97,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;
@@ -257,8 +257,12 @@ export default {
});
},
reconnect() {
this.$refs.novncConsole.reconnect();
},
softReboot() {
this.vmResource.softrebootVM();
this.vmResource.doSoftReboot();
},
showKeysModal() {
@@ -306,6 +310,13 @@ export default {
{{ t("harvester.action.softreboot") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<v-dropdown
v-if="!hideCustomKeysBar"
ref="customKeyPopover"
@@ -372,3 +383,15 @@ 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;
}
.vm-console .v-popper__inner{
overflow-y: visible;
}
</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));
+9 -7
View File
@@ -1,10 +1,12 @@
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
export const DOC = {
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`,
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`,
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
};
+9 -2
View File
@@ -39,13 +39,20 @@ const FEATURE_FLAGS = {
],
'v1.5.1': [],
'v1.6.0': [
'vmMachineTypes',
'customSupportBundle',
'csiOnlineExpandValidation',
'vmNetworkMigration',
'kubeovnVpcSubnet',
'rancherClusterSetting',
'cpuMemoryHotplug'
'cpuMemoryHotplug',
'cdiSettings',
'vmCloneRunStrategy',
],
'v1.6.1': [],
'v1.7.0': [
'vmMachineTypeAuto',
'lhV2VolExpansion',
'l2VlanTrunkMode'
]
};
+17
View File
@@ -10,6 +10,7 @@ import {
LOGGING,
STORAGE_CLASS,
SECRET,
NETWORK_POLICY
} from '@shell/config/types';
import { HCI, VOLUME_SNAPSHOT } from '../types';
import {
@@ -425,6 +426,7 @@ export function init($plugin, store) {
HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT,
HCI.VPC,
NETWORK_POLICY,
HCI.LB,
HCI.IP_POOL,
],
@@ -566,6 +568,21 @@ export function init($plugin, store) {
ifHaveType: HCI.VPC,
});
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.networkPolicy.label',
name: NETWORK_POLICY,
namespaced: true,
weight: 186,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: NETWORK_POLICY }
},
exact: false,
ifHaveType: NETWORK_POLICY,
});
configureType(HCI.SNAPSHOT, {
isCreatable: false,
location: {
+13
View File
@@ -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',
};
+4 -3
View File
@@ -4,7 +4,6 @@ export const HCI_SETTING = {
LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan',
@@ -84,7 +83,6 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
[HCI_SETTING.CONTAINERD_REGISTRY]: {
kind: 'json', from: 'import', canReset: true
},
@@ -113,7 +111,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 = {
+9 -3
View File
@@ -4,9 +4,10 @@ export const BACKUP_TYPE = {
};
export const NETWORK_TYPE = {
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork',
OVERLAY: 'OverlayNetwork',
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork',
OVERLAY: 'OverlayNetwork',
L2TRUNK_VLAN: 'L2VlanTrunkNetwork',
};
export const VOLUME_MODE = {
@@ -23,3 +24,8 @@ export const INTERNAL_STORAGE_CLASS = {
VMSTATE_PERSISTENCE: 'vmstate-persistence',
LONGHORN_STATIC: 'longhorn-static',
};
export const L2VLAN_MODE = {
ACCESS: 'access',
TRUNK: 'trunk',
};
@@ -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',
+20 -6
View File
@@ -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"
-149
View File
@@ -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>
@@ -8,7 +8,7 @@ import LabelValue from '@shell/components/LabelValue';
import { BadgeState } from '@components/BadgeState';
import { Banner } from '@components/Banner';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup, RadioButton } from '@components/Form/Radio';
import { RadioGroup } from '@components/Form/Radio';
import HarvesterDisk from '../../mixins/harvester-disk';
import Tags from '../../components/DiskTags';
import { HCI } from '../../types';
@@ -30,7 +30,6 @@ export default {
BadgeState,
Banner,
RadioGroup,
RadioButton,
ModalWithCard,
Tags,
},
@@ -184,12 +183,11 @@ export default {
},
forceFormattedDisabled() {
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
const systems = ['ext4', 'XFS'];
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
if (this.blockDevice?.childParts?.length > 0) {
return true;
} else if (systems.includes(fileSystem)) {
return false;
@@ -446,7 +444,7 @@ export default {
/>
</div>
<div
v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted"
v-if="(value.isNew && isLonghornV1) || isCorrupted"
class="col span-6"
>
<RadioGroup
@@ -459,15 +457,6 @@ export default {
:disabled="forceFormattedDisabled"
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
>
<template #1="{option, listeners}">
<RadioButton
:label="option.label"
:val="option.value"
:value="value.forceFormatted"
:disabled="forceFormattedDisabled && !value.forceFormatted"
v-on="listeners"
/>
</template>
</RadioGroup>
</div>
<div
@@ -7,6 +7,8 @@ import { Checkbox } from '@components/Form/Checkbox';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
import isEqual from 'lodash/isEqual';
import { clone } from '@shell/utils/object';
export const ksmtunedMode = [{
value: 'standard',
@@ -64,11 +66,13 @@ export default {
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
this.spec = this.ksmtuned.spec;
this.originSpec = clone(this.ksmtuned.spec);
},
data() {
return {
ksmtuned: {},
originSpec: {},
spec: {},
thresCoef: 30,
ksmtunedMode,
@@ -99,6 +103,11 @@ export default {
methods: {
async saveKsmtuned() {
// no ksmtuned spec change, skip save
if (isEqual(this.spec, this.originSpec)) {
return;
}
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
this.ksmtuned['spec'] = this.spec;
@@ -527,7 +527,6 @@ export default {
id: this.longhornNode.id,
opt: { force: true },
});
await new Promise((resolve) => setTimeout(resolve, '5000'));
await retrySave();
} else {
@@ -10,9 +10,13 @@ import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-anno
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
import { NETWORK_TYPE } from '../config/types';
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
import { removeObject } from '@shell/utils/array';
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
const {
L2VLAN, UNTAGGED, OVERLAY, L2TRUNK_VLAN
} = NETWORK_TYPE;
const { ACCESS, TRUNK } = L2VLAN_MODE;
const AUTO = 'auto';
const MANUAL = 'manual';
@@ -52,6 +56,8 @@ export default {
return {
config,
type,
l2VlanMode: this.value.vlanType === L2TRUNK_VLAN ? TRUNK : ACCESS,
vlanTrunk: this.parseVlanTrunk(config),
layer3Network: {
mode: layer3Network.mode || AUTO,
serverIPAddr: layer3Network.serverIPAddr || '',
@@ -108,6 +114,10 @@ export default {
}];
},
l2VlanTrunkModeFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('l2VlanTrunkMode');
},
kubeovnVpcSubnetSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
},
@@ -131,6 +141,16 @@ export default {
});
},
l2VlanModeOptions() {
return [{
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.access'),
value: ACCESS,
}, {
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.trunk'),
value: TRUNK,
}];
},
networkTypes() {
const types = [L2VLAN, UNTAGGED];
@@ -149,6 +169,22 @@ export default {
return this.type === L2VLAN;
},
isL2VlanTrunkMode() {
if (this.isView) {
return this.value.vlanType === L2TRUNK_VLAN && this.l2VlanMode === TRUNK;
}
return this.type === L2TRUNK_VLAN || (this.l2VlanMode === TRUNK && this.type === L2VLAN);
},
isL2VlanAccessMode() {
if (this.isView) {
return this.value.vlanType === L2VLAN && this.l2VlanMode === ACCESS;
}
return this.type === L2VLAN && this.l2VlanMode === ACCESS;
},
isOverlayNetwork() {
if (this.isView) {
return this.value.vlanType === OVERLAY;
@@ -168,11 +204,11 @@ export default {
watch: {
type(newType) {
if (newType === OVERLAY) {
if (newType === OVERLAY) { // overlay network configuration
this.config.type = 'kube-ovn';
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
} else {
} else { // l2vlan or untagged network configuration
this.config.type = 'bridge';
this.config.promiscMode = true;
this.config.ipam = {};
@@ -180,7 +216,20 @@ export default {
delete this.config.provider;
delete this.config.server_socket;
}
}
},
l2VlanMode(newAccessMode) {
if (this.type !== L2VLAN) {
return;
}
if (newAccessMode === TRUNK) { // trunk mode
this.config.vlan = 0;
this.config.vlanTrunk = this.vlanTrunk;
} else { // access mode
delete this.config.vlanTrunk;
this.config.vlan = '';
}
},
},
methods: {
@@ -188,7 +237,15 @@ export default {
const errors = [];
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
if (!this.config.vlan && !this.isUntaggedNetwork) {
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.minID === '')) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId') }));
}
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.maxID === '')) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId') }));
}
if (this.isL2VlanAccessMode && !this.config.vlan && !this.isUntaggedNetwork) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
}
@@ -217,6 +274,30 @@ export default {
await this.save(buttonCb);
},
parseVlanTrunk(config) {
if (config?.vlanTrunk && config?.vlanTrunk?.length > 0) {
return config.vlanTrunk;
}
return [{ minID: '', maxID: '' }];
},
removeVlanTrunk(trunk) {
removeObject(this.vlanTrunk, trunk);
},
addVlanTrunk() {
if (!this.config.vlanTrunk) {
this.config.vlanTrunk = [];
}
this.vlanTrunk.push({ minID: this.minId, maxID: this.maxId });
this.config.vlanTrunk = this.vlanTrunk;
},
vlanTrunkChange() {
this.config.vlanTrunk = this.vlanTrunk;
},
input(neu) {
if (neu === '') {
this.config.vlan = '';
@@ -292,8 +373,76 @@ export default {
required
/>
<LabeledSelect
v-if="isL2VlanNetwork && l2VlanTrunkModeFeatureEnabled"
v-model:value="l2VlanMode"
class="mb-20"
:options="l2VlanModeOptions"
:mode="mode"
:disabled="isEdit"
:label="t('harvester.vlanStatus.vlanConfig.l2VlanMode.label')"
required
/>
<div v-if="isL2VlanTrunkMode && l2VlanTrunkModeFeatureEnabled">
<div
v-for="(trunk, i) in vlanTrunk"
:key="i"
>
<div class="row mt-10">
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.minID"
class="mb-20"
required
placeholder="e.g. 1-4094"
:min="1"
:max="4094"
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.maxID"
class="mb-20"
:max="4094"
:min="1"
placeholder="e.g. 1-4094"
required
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col remove-btn mb-20">
<button
type="button"
:disabled="isView || vlanTrunk.length <= 1"
:aria-label="t('generic.ariaLabel.remove', {index: i+1})"
role="button"
class="btn role-link"
@click="removeVlanTrunk(trunk)"
>
{{ t('harvester.fields.remove') }}
</button>
</div>
</div>
</div>
<button
v-if="isL2VlanTrunkMode"
type="button"
class="btn btn-sm bg-primary mb-20"
:disabled="isView"
@click="addVlanTrunk"
>
{{ t('harvester.vlanStatus.vlanConfig.vlanTrunk.add') }}
</button>
</div>
<LabeledInput
v-if="isL2VlanNetwork"
v-if="isL2VlanAccessMode"
v-model:value.number="config.vlan"
class="mb-20"
required
@@ -316,7 +465,7 @@ export default {
/>
</Tab>
<Tab
v-if="isL2VlanNetwork"
v-if="isL2VlanAccessMode"
name="layer3Network"
:label="t('harvester.network.tabs.layer3Network')"
:weight="98"
@@ -375,3 +524,12 @@ export default {
</Tabbed>
</CruResource>
</template>
<style lang="scss" scoped>
.remove-btn {
align-self: center;
}
.trunk-span{
flex: 5;
}
</style>
@@ -15,6 +15,7 @@ import { HCI } from '../types';
import { sortBy } from '@shell/utils/sort';
import { BACKUP_TYPE } from '../config/types';
import { _EDIT, _CREATE } from '@shell/config/query-params';
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
export default {
name: 'CreateVMSchedule',
@@ -93,7 +94,7 @@ export default {
return this.settings.find( (O) => O.id === 'backup-target');
},
isEmptyValue() {
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
return isBackupTargetSettingEmpty(this.backupTargetResource);
},
canUpdate() {
return this?.backupTargetResource?.canUpdate;
@@ -109,7 +110,7 @@ export default {
!!this.value.spec.maxFailure;
},
isBackupTargetUnAvailable() {
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource);
},
vmOptions() {
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
@@ -166,20 +167,6 @@ export default {
onTypeChange(newType) {
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
},
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
},
validateFailure(count) {
if (this.value.spec.retain && count > this.value.spec.retain) {
this.value.spec['maxFailure'] = this.value.spec.retain;
@@ -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"
+26 -3
View File
@@ -7,6 +7,7 @@ import ResourceTabs from '@shell/components/form/ResourceTabs';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import Conditions from '@shell/components/form/Conditions';
import { Banner } from '@components/Banner';
import { allHash } from '@shell/utils/promise';
import { get } from '@shell/utils/object';
@@ -40,6 +41,7 @@ export default {
LabeledSelect,
LabeledInput,
NameNsDescription,
Conditions
},
mixins: [CreateEditView],
@@ -267,7 +269,19 @@ export default {
rebuildStatus() {
return this.value.longhornEngine?.status?.rebuildStatus;
}
},
isLHV2VolExpansionFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
},
isResizeDisabled() {
return (
!this.isLHV2VolExpansionFeatureEnabled &&
this.value?.isLonghornV2 &&
this.isEdit
);
},
},
watch: {
@@ -463,7 +477,7 @@ export default {
:output-modifier="true"
:increment="1024"
:mode="mode"
:disabled="value?.isLonghornV2 && isEdit"
:disabled="isResizeDisabled"
required
class="mb-20"
:suffix="GIBIBYTE"
@@ -471,7 +485,7 @@ export default {
/>
<Banner
v-if="value?.isLonghornV2 && isEdit"
v-if="isResizeDisabled"
color="warning"
>
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
@@ -581,6 +595,15 @@ export default {
:label="t('nameNsDescription.name.label')"
/>
</Tab>
<Tab
v-if="!isCreate"
name="conditions"
:label="t('harvester.volume.tabs.conditions')"
class="bordered-table"
:mode="mode"
>
<Conditions :value="value" />
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -0,0 +1,196 @@
<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { Banner } from '@components/Banner';
import InfoBox from '@shell/components/InfoBox';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'AccessControlList',
emits: ['update:value'],
components: {
Banner, InfoBox, LabeledSelect, LabeledInput
},
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
},
data() {
const rows = (this.value || []).map((row) => {
return {
action: row.action || '',
direction: row.direction || '',
priority: row.priority || 0,
match: row.match || '',
};
});
return { rows };
},
computed: {
isView() {
return this.mode === _VIEW;
},
actionOptions() {
return [
{ label: 'Allow', value: 'allow' },
{ label: 'Drop', value: 'drop' },
{ label: 'Pass', value: 'pass' },
{ label: 'Reject', value: 'reject' },
{ label: 'Allow-related', value: 'allow-related' },
{ label: 'Allow-stateless', value: 'allow-stateless' },
];
},
directionOptions() {
return [
{ label: 'To-lport', value: 'to-lport' },
{ label: 'From-lport', value: 'from-lport' },
];
}
},
created() {
this.queueUpdate = debounce(this.update, 100);
},
methods: {
add() {
this.rows.push({
action: '',
direction: '',
priority: this.rows.length,
match: '',
});
this.queueUpdate();
},
removeRule(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if (this.isView) {
return;
}
this.$emit('update:value', this.rows);
}
},
};
</script>
<template>
<div>
<Banner
v-if="rows.length > 0"
color="info"
>
<t
k="harvester.subnet.acl.banner"
:raw="true"
/>
</Banner>
<div
v-for="(row, idx) in rows"
:key="idx"
>
<InfoBox class="box">
<button
type="button"
class="role-link btn btn-sm removeBtn"
:disabled="isView"
@click="removeRule(idx)"
>
<i class="icon icon-x" />
</button>
<div class="row">
<div class="col span-4">
<LabeledSelect
v-model:value="row.action"
:mode="mode"
class="mt-5 ml-5"
:options="actionOptions"
:required="true"
:label="t('harvester.subnet.acl.action.label')"
:placeholder="t('harvester.subnet.acl.action.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="row.direction"
:mode="mode"
:options="directionOptions"
class="mt-5"
:required="true"
:label="t('harvester.subnet.acl.direction.label')"
:placeholder="t('harvester.subnet.acl.direction.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-3">
<LabeledInput
v-model:value.number="row.priority"
type="number"
class="mb-20 mt-5"
:max="32767"
:min="0"
:mode="mode"
required
label-key="harvester.subnet.acl.priority.label"
@update:value="update"
/>
</div>
</div>
<div class="row">
<div class="col span-11">
<LabeledInput
v-model:value="row.match"
class="mb-5 ml-5"
:mode="mode"
required
:placeholder="t('harvester.subnet.acl.match.placeholder')"
label-key="harvester.subnet.acl.match.label"
@update:value="update"
/>
</div>
</div>
</InfoBox>
</div>
<button
type="button"
class="btn role-tertiary add"
:disabled="isView"
@click="add()"
>
<t k="harvester.subnet.acl.addRule" />
</button>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
}
.removeBtn {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
</style>
+78 -2
View File
@@ -14,6 +14,8 @@ import ArrayList from '@shell/components/form/ArrayList';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types';
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
import { Banner } from '@components/Banner';
import AccessControlList from './AccessControlList';
export default {
name: 'EditSubnet',
@@ -21,6 +23,7 @@ export default {
emits: ['update:value'],
components: {
Banner,
CruResource,
LabeledInput,
LabeledSelect,
@@ -30,6 +33,7 @@ export default {
ArrayList,
ResourceTabs,
Loading,
AccessControlList
},
mixins: [CreateEditView],
@@ -38,7 +42,9 @@ export default {
created() {
const vpc = this.$route.query.vpc || '';
const enableDHCP = this.value?.spec?.enableDHCP || false;
set(this.value.spec, 'enableDHCP', enableDHCP);
set(this.value, 'spec', this.value.spec || {
cidrBlock: '',
protocol: NETWORK_PROTOCOL.IPv4,
@@ -46,7 +52,9 @@ export default {
vpc,
gatewayIP: '',
excludeIps: [],
private: false
private: false,
enableDHCP,
acls: []
});
},
@@ -81,7 +89,6 @@ export default {
protocolOptions() {
return Object.values(NETWORK_PROTOCOL);
},
provider: {
get() {
const raw = this.value.spec.provider;
@@ -125,10 +132,21 @@ export default {
}
},
watch: {
'value.spec.enableDHCP': {
handler(newValue) {
if (newValue === false) {
this.value.spec.dhcpV4Options = '';
this.value.spec.dhcpV6Options = '';
}
},
}
},
methods: {
async saveSubnet(buttonCb) {
const errors = [];
const name = this.value?.metadata?.name;
const hasEmptyAcls = this.value?.spec?.acls?.some((acl) => !acl.match || !acl.action || acl.priority === undefined || acl.priority === null);
try {
if (!name) {
@@ -139,6 +157,8 @@ export default {
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
} else if (this.value.spec.excludeIps.includes('')) {
errors.push(this.t('harvester.validation.subnet.excludeIps'));
} else if (hasEmptyAcls) {
errors.push(this.t('harvester.validation.subnet.aclEmptyError'));
}
if (errors.length > 0) {
@@ -245,6 +265,45 @@ export default {
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
v-model:value="value.spec.enableDHCP"
name="enabled"
:options="[true, false]"
:label="t('harvester.subnet.dhcp.label')"
:labels="[t('generic.enabled'), t('generic.disabled')]"
:mode="mode"
:tooltip="t('harvester.subnet.dhcp.tooltip')"
/>
<LabeledInput
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv4'"
v-model:value="value.spec.dhcpV4Options"
class="mb-20 mt-20"
:placeholder="t('harvester.subnet.dhcp.placeholder')"
:label="t('harvester.subnet.dhcp.v4Options')"
:mode="mode"
/>
<LabeledInput
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv6'"
v-model:value="value.spec.dhcpV6Options"
:placeholder="t('harvester.subnet.dhcp.placeholder')"
class="mb-20 mt-20"
:label="t('harvester.subnet.dhcp.v6Options')"
:mode="mode"
/>
<Banner
v-if="value.spec.enableDHCP"
color="info"
class="dhcpOption-banner"
>
<t
k="harvester.subnet.dhcp.dhcpOptionBanner"
:raw="true"
/>
</Banner>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
@@ -318,6 +377,23 @@ export default {
</template>
</ArrayList>
</Tab>
<Tab
name="ACL"
:label="t('harvester.subnet.acl.label')"
:weight="-2"
class="bordered-table"
>
<AccessControlList
v-model:value="value.spec.acls"
:mode="mode"
/>
</Tab>
</ResourceTabs>
</CruResource>
</template>
<style lang="scss" scoped>
.dhcpOption-banner {
width: max-content;
}
</style>
@@ -2,12 +2,17 @@
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { Banner } from '@components/Banner';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
export default {
name: 'StaticRoutes',
emits: ['update:value'],
components: { Banner },
props: {
value: {
type: Array,
@@ -45,6 +50,12 @@ export default {
nextHopIPTooltip() {
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
},
vpcPeeringExamplesLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.VPC_CONFIGURATION_EXAMPLES, version);
},
},
created() {
@@ -78,6 +89,13 @@ export default {
<template>
<div>
<Banner color="info">
<t
k="harvester.vpc.vpcPeerings.infoBanner"
:raw="true"
:url="vpcPeeringExamplesLink"
/>
</Banner>
<div
v-if="rows.length"
class="static-route-row"
@@ -240,7 +240,7 @@ export default {
</script>
<template>
<div class="mt-20">
<div>
<LabeledSelect
v-model:value="checkedSsh"
:label="t('harvester.virtualMachine.input.sshKey')"
@@ -136,6 +136,10 @@ export default {
pvcs() {
return this.$store.getters['harvester/all'](PVC) || [];
},
isLHV2VolExpansionFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
},
},
watch: {
@@ -270,8 +274,8 @@ export default {
},
isResizeDisabled(volume) {
if (this.isCreate) return false;
if (volume.newCreateId) return false;
if (this.isLHV2VolExpansionFeatureEnabled) return false;
if (this.isCreate || volume.newCreateId) return false;
const isStopped = this.vm.stateDisplay === OFF;
const isLonghornV2 = this.isLonghornV2(volume);
@@ -406,7 +410,7 @@ export default {
:label="ucFirst(value.volumeBackups.error.message)"
/>
<Banner
v-if="isLonghornV2(volume) && !isView"
v-if="!isLHV2VolExpansionFeatureEnabled && isLonghornV2(volume) && !isView"
color="warning"
:label="t('harvester.volume.longhorn.disableResize')"
/>
@@ -2,7 +2,7 @@
import { isEqual } from 'lodash';
import { mapGetters } from 'vuex';
import Tabbed from '@shell/components/Tabbed';
import { clone, set } from '@shell/utils/object';
import { clone } from '@shell/utils/object';
import Tab from '@shell/components/Tabbed/Tab';
import { Checkbox } from '@components/Form/Checkbox';
import CruResource from '@shell/components/CruResource';
@@ -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;
@@ -413,38 +434,44 @@ export default {
}
},
_compareDisksIgnoreStorage(disksA = [], disksB = []) {
if (disksA.length !== disksB.length) return false;
const sortedA = [...disksA].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
const sortedB = [...disksB].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
for (let i = 0; i < sortedA.length; i++) {
const cloneA = clone(sortedA[i]);
const cloneB = clone(sortedB[i]);
// remove the storage field before comparison
delete cloneA?.spec?.resources?.requests?.storage;
delete cloneB?.spec?.resources?.requests?.storage;
if (!isEqual(cloneA, cloneB)) return false;
}
return true;
},
_shouldPromptRestart(oldVM, newVM) {
const specChanged = !isEqual(oldVM?.spec, newVM?.spec);
// check if the disk defined in annotations has changed, ignoring storage size
const oldDisks = parseVolumeClaimTemplates(oldVM);
const newDisks = parseVolumeClaimTemplates(newVM);
const diskChanged = !this._compareDisksIgnoreStorage(oldDisks, newDisks);
return specChanged || diskChanged;
},
restartVM() {
if (this.mode !== 'edit') {
return;
}
if (!this.value.isRunning) {
return;
}
const cloneDeepNewVM = clone(this.value);
if (this.mode !== 'edit' || !this.value.isRunning) return;
// new VM
delete cloneDeepNewVM?.metadata;
delete cloneDeepNewVM?.__clone;
const needsRestart = this._shouldPromptRestart(this.cloneVM, this.value);
// old VM
delete this.cloneVM?.metadata;
delete this.cloneVM?.__clone;
// add empty hostDevices to old VM as CRD does not have it.
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
if (devicesObj && devicesObj.hostDevices === undefined) {
set(devicesObj, 'hostDevices', []);
}
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
// only check VM is changed in form page.
if (isEqual(oldVM, newVM)) {
return;
}
// if no restart is needed (either no changes or only size changed)
if (!needsRestart) return;
return new Promise((resolve) => {
this.isOpen = true;
@@ -466,9 +493,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 +630,7 @@ export default {
:label="t('harvester.virtualMachine.detail.tabs.basics')"
>
<CpuMemory
class="mb-20"
:cpu="cpu"
:max-cpu="maxCpu"
:memory="memory"
@@ -872,7 +898,10 @@ export default {
/>
</div>
<div class="col span-6">
<div
v-if="!value.vmMachineTypeAutoFeatureEnabled"
class="col span-6"
>
<LabeledSelect
v-model:value="machineType"
label-key="harvester.virtualMachine.input.MachineType"
@@ -0,0 +1,42 @@
<script>
import NetworkPolicy from '@shell/edit/networking.k8s.io.networkpolicy';
import { Banner } from '@components/Banner';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
export default {
components: {
NetworkPolicy,
Banner
},
computed: {
networkPolicyDocLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.NETWORK_POLICY, version);
}
}
};
</script>
<template>
<div class="network-policy-edit">
<Banner color="info">
<t
k="harvester.networkPolicy.banner"
:url="networkPolicyDocLink"
:raw="true"
/>
</Banner>
<NetworkPolicy v-bind="$attrs" />
</div>
</template>
<style lang="scss" scoped>
.network-policy-edit {
flex: auto;
display: flex;
flex-direction: column;
}
</style>
+68 -6
View File
@@ -236,6 +236,7 @@ harvester:
shutdown: Shut Down
powerOn: Power On
reboot: Reboot
reconnect: Reconnect
forceStop: Force Stop
formatters:
hardwareResourceGauge:
@@ -403,7 +404,7 @@ harvester:
sha512: 'Invalid SHA512 checksum.'
subnet:
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
aclEmptyError: The fields in subnet access control list rule can not be empty.
dashboard:
label: Dashboard
header: "Harvester Cluster: {cluster}"
@@ -836,6 +837,7 @@ harvester:
snapshots: Snapshots
datasource: Data Source
details: Details
conditions: Conditions
size: Size
volumeMode: Volume Mode
source: Source
@@ -1029,7 +1031,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
@@ -1044,6 +1046,13 @@ harvester:
gateway:
label: Gateway IP
placeholder: e.g. 172.20.0.1
dhcp:
label: Dynamic Host Configuration Protocol (DHCP)
v4Options: DHCPV4Options
v6Options: DHCPV6Options
placeholder: key1=value1, key2=value2
dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a>
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
private:
label: Private Subnet
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
@@ -1054,6 +1063,23 @@ harvester:
placeholder: e.g. 172.16.0.0/16
excludeIPs:
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
acl:
label: Access Control List
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
action:
label: Action
placeholder: Please select an action
direction:
label: Direction
placeholder: Please select a direction
addRule: Add Rule
priority:
label: Priority
placeholder: Please select a priority
match:
label: Match
placeholder: e.g. ip4.dst == 10.10.0.2
banner: The supported field in ACL match can refer to <a href="https://kubeovn.github.io/docs/v1.14.x/en/guide/subnet/#subnet-acl" target="_blank">KubeOvn Subnet ACL document</a>
vpc:
noAddonEnabled:
@@ -1080,8 +1106,10 @@ harvester:
placeholder: e.g. 169.254.0.1/16
remoteVpc:
label: Remote VPC
infoBanner: The static route destination CIDR must cover all subnets CIDR from remote VPC Peer. Read <a href="{url}" target="_blank">VPC Peering Configuration Examples</a> for more information.
networkPolicy:
label: Network Policies
banner: The network policies must be used for VMs attached to overlay networks. Please read the <a href="{url}" target="_blank">harvester document</a> how the network policy works.
network:
label: Virtual Machine Networks
tabs:
@@ -1388,6 +1416,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
@@ -1433,6 +1483,14 @@ harvester:
vlanStatus:
vlanConfig:
label: Network Configuration
l2VlanMode:
access: Access
trunk: Trunk
label: Mode
vlanTrunk:
minId: Minimum VLAN ID
maxId: Maximum VLAN ID
add: Add VLAN Trunk
clusterNetwork:
title: Cluster Network Configuration
@@ -1698,9 +1756,8 @@ advancedSettings:
'harv-server-version': Harvester server version.
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
'harv-ui-source': Configure how to load the UI source.
'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server.
'harv-ui-index': 'HTML index location for the UI.'
'harv-ui-plugin-index': 'JS index location for the Harvester plugin UI.'
'harv-cluster-registration-url': Registration URL for multi-cluster management.
'harv-http-proxy': 'HTTP proxy for Harvester to access external services.'
'harv-additional-ca': 'Custom CA root certificates for TLS validation.'
@@ -1787,6 +1844,11 @@ typeLabel:
one { Virtual Private Cloud }
other { Virtual Private Clouds }
}
networking.k8s.io.networkpolicy: |-
{count, plural,
one { Network Policy }
other { Network Policies }
}
harvesterhci.io.cloudtemplate: |-
{count, plural,
one { Cloud Configuration Template }
@@ -4,7 +4,7 @@ import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink';
import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable';
import { isBackupTargetSettingEmpty } from '../utils/setting';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import FilterVMSchedule from '../components/FilterVMSchedule';
import { HCI } from '../types';
@@ -39,7 +39,7 @@ export default {
this.settings = hash.settings;
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) {
const backupTargetResource = hash.settings.find( (O) => O.id === 'backup-target');
const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource);
const isEmpty = isBackupTargetSettingEmpty(backupTargetResource);
if (backupTargetResource && !isEmpty) {
this.testConnect();
@@ -78,20 +78,6 @@ export default {
}
},
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
},
getRow(row) {
return row.status && row.status.source;
},
@@ -183,11 +169,9 @@ export default {
backupTargetResource() {
return this.settings.find((O) => O.id === 'backup-target');
},
isEmptyValue() {
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
return isBackupTargetSettingEmpty(this.backupTargetResource);
},
canUpdate() {
return this?.backupTargetResource?.canUpdate;
},
@@ -74,7 +74,7 @@ export default {
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM, opt: { force: true } }),
pod: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }),
+66 -27
View File
@@ -273,7 +273,7 @@ export default {
needNewSecret() {
// When creating a template it is always necessary to create a new secret.
return this.showYaml ? false : this.resourceType === HCI.VM_VERSION || this.isCreate;
return this.isCreate ? true : this.showYaml ? false : this.resourceType === HCI.VM_VERSION;
},
defaultTerminationSetting() {
@@ -299,9 +299,11 @@ export default {
async created() {
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: SECRET });
if (this.value.vmMachineTypesFeatureEnabled) {
if (!this.value.vmMachineTypeAutoFeatureEnabled) {
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/clusters/local?link=machineTypes');
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](
'v1/harvester/clusters/local?link=machineTypes'
);
const machineTypes = await this.$store.dispatch('harvester/request', { url });
this.machineTypes = machineTypes;
@@ -687,21 +689,22 @@ 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;
}
}
},
parseDiskRows(disk) {
const disks = [];
const volumes = [];
const diskNameLables = [];
const diskNameLabels = [];
const volumeClaimTemplates = [];
disk.forEach( (R, index) => {
@@ -723,7 +726,7 @@ export default {
disks.push(_disk);
volumes.push(_volume);
diskNameLables.push(dataVolumeName);
diskNameLabels.push(dataVolumeName);
if (R.source !== SOURCE_TYPE.CONTAINER) {
volumeClaimTemplates.push(_dataVolumeTemplate);
@@ -764,6 +767,9 @@ export default {
}
}
const specDisks = this.spec?.template?.spec?.domain?.devices?.disks;
const mergedDisks = this.mergeDeviceList(specDisks, disks);
let spec = {
...this.spec,
runStrategy: this.runStrategy,
@@ -786,7 +792,7 @@ export default {
...this.spec.template?.spec?.domain,
devices: {
...this.spec.template?.spec?.domain?.devices,
disks,
disks: mergedDisks,
},
},
volumes,
@@ -885,13 +891,16 @@ export default {
interfaces.push(_interface);
});
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
const spec = {
...this.spec.template.spec,
domain: {
...this.spec.template.spec.domain,
devices: {
...this.spec.template.spec.domain.devices,
interfaces,
interfaces: mergedInterfaces,
},
},
networks
@@ -1034,7 +1043,9 @@ export default {
},
parseVolumeClaimTemplate(R, dataVolumeName) {
if (!String(R.size).includes('Gi') && R.size) {
const sizeString = String(R.size);
if (!(sizeString.includes('Gi') || sizeString.includes('Ti')) && R.size) {
R.size = `${ R.size }${ GIBIBYTE }`;
}
@@ -1358,21 +1369,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 = [];
@@ -1540,6 +1536,49 @@ export default {
this.refreshYamlEditor();
},
mergeInterfaceList(specInterfaceList, interfaceList) {
if (!specInterfaceList || specInterfaceList.length === 0) {
return interfaceList;
}
const specInterfaceMap = new Map(specInterfaceList.map((iface) => [iface.name, iface]));
return interfaceList.map((iface) => {
const specInterface = specInterfaceMap.get(iface.name);
if (specInterface) {
const merged = { ...specInterface, ...iface };
// currently we only have bridge and masquerade network type, they are mutually exclusive
if (iface['bridge']) {
delete merged['masquerade'];
} else {
delete merged['bridge'];
}
return merged;
}
return iface;
});
},
mergeDeviceList(specDeviceList, deviceList) {
if (!specDeviceList || specDeviceList.length === 0) {
return deviceList;
}
const specDeviceMap = new Map(specDeviceList.map((device) => [device.name, device]));
return deviceList.map((device) => {
const specDevice = specDeviceMap.get(device.name);
if (specDevice) {
return { ...specDevice, ...device };
}
return device;
});
},
refreshYamlEditor() {
this.$nextTick(() => {
this.$refs.yamlEditor?.updateValue();
+3 -1
View File
@@ -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);
+19
View File
@@ -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);
@@ -2,7 +2,7 @@ import SteveModel from '@shell/plugins/steve/steve-class';
import { HCI } from '@shell/config/labels-annotations';
import { NETWORK_TYPE } from '../config/types';
const { UNTAGGED } = NETWORK_TYPE;
const { UNTAGGED, OVERLAY, L2TRUNK_VLAN } = NETWORK_TYPE;
export default class NetworkAttachmentDef extends SteveModel {
applyDefaults() {
@@ -45,7 +45,7 @@ export default class NetworkAttachmentDef extends SteveModel {
}
get vlanId() {
return this.vlanType === UNTAGGED ? 'N/A' : this.parseConfig.vlan;
return this.vlanType === UNTAGGED || this.vlanType === OVERLAY || this.vlanType === L2TRUNK_VLAN ? 'N/A' : this.parseConfig.vlan;
}
get customValidationRules() {
@@ -68,7 +68,7 @@ export default class NetworkAttachmentDef extends SteveModel {
const route = annotations[HCI.NETWORK_ROUTE];
let config = {};
if (this.vlanType === UNTAGGED) {
if (this.vlanType === UNTAGGED || this.vlanType === OVERLAY || this.vlanType === L2TRUNK_VLAN) {
return 'N/A';
}
@@ -14,6 +14,7 @@ import { BACKUP_TYPE } from '../config/types';
import { HCI } from '../types';
import HarvesterResource from './harvester';
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
import { isBackupTargetSettingUnavailable } from '../utils/setting';
export const OFF = 'Off';
@@ -32,8 +33,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';
@@ -154,7 +153,7 @@ export default class VirtVm extends HarvesterResource {
},
{
action: 'backupVM',
enabled: !!this.actions?.backup,
enabled: !!this.actions?.backup && !this.isBackupTargetUnavailable,
icon: 'icon icon-backup',
label: this.t('harvester.action.backup')
},
@@ -329,6 +328,10 @@ export default class VirtVm extends HarvesterResource {
});
}
doSoftReboot() {
this.doActionGrowl('softreboot', {});
}
softrebootVM(resources = this) {
this.$dispatch('promptModal', {
resources,
@@ -742,6 +745,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 +1169,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;
}
@@ -1225,8 +1236,15 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
get vmMachineTypesFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypes');
get vmMachineTypeAutoFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto');
}
get isBackupTargetUnavailable() {
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
return isBackupTargetSettingUnavailable(backupTargetSetting);
}
setInstanceLabels(val) {
@@ -16,7 +16,10 @@ export default class HciVlanConfig extends HarvesterResource {
uplink: {
nics: [],
linkAttributes: {},
bondOptions: { mode: 'active-backup' },
bondOptions: {
mode: 'active-backup',
miimon: -1
},
},
};
+2 -2
View File
@@ -1,13 +1,13 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.6.0-rc3",
"version": "1.7.0-dev",
"private": false,
"rancher": {
"annotations": {
"catalog.cattle.io/display-name": "Harvester",
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
"catalog.cattle.io/rancher-version": ">= 2.12.0-0",
"catalog.cattle.io/rancher-version": ">= 2.13.0-0",
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
}
},
@@ -215,19 +215,18 @@ export default {
if (this.imageSource === IMAGE_METHOD.NEW) {
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
if (this.sourceType === UPLOAD && this.uploadImageId !== '') {
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
this.value.spec.image = this.uploadImageId;
} else if (this.sourceType === DOWNLOAD) {
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image
this.imageValue.spec.sourceType = DOWNLOAD;
if (!this.imageValue.spec.url) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
buttonCb(false);
return;
}
res = await this.imageValue.save();
this.value.spec.image = res.id;
}
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
@@ -247,7 +246,6 @@ export default {
if (this.skipSingleReplicaDetachedVolFeatureEnabled) {
this.value.metadata.annotations = { [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
}
await this.value.save();
this.done();
buttonCb(true);
@@ -334,6 +332,22 @@ export default {
},
watch: {
async sourceType(neu) {
if (neu === DOWNLOAD && this.imageValue && this.uploadController) {
if (this.uploadController) {
this.uploadController.abort();
}
try {
await this.imageValue.remove();
await this.initImageValue();
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error occurred while removing imageValue:', error);
}
}
},
imageSource(neu) {
if (neu !== IMAGE_METHOD.DELETE) {
this.deleteImageId = '';
+6 -7
View File
@@ -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;
}
+17 -13
View File
@@ -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);
+25
View File
@@ -0,0 +1,25 @@
export function isBackupTargetSettingEmpty(setting = {}) {
let isEmpty = true;
if (setting?.value) {
try {
const valueJson = JSON.parse(setting?.value);
isEmpty = !valueJson.type;
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to parse backup target value:', e);
}
}
return isEmpty;
}
export function isBackupTargetSettingUnavailable(setting = {}) {
const errorMessage = setting.errorMessage;
const isEmptyValue = isBackupTargetSettingEmpty(setting);
const canUpdate = setting.canUpdate;
return (errorMessage || isEmptyValue) && canUpdate;
}
+1 -1
View File
@@ -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
+16
View File
@@ -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);
+938 -1815
View File
File diff suppressed because it is too large Load Diff