Compare commits

..

43 Commits

Author SHA1 Message Date
renovate[bot]
4d4e3699a8
deps: update dependency @types/node to v25.6.2 (#874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 12:52:13 +08:00
renovate[bot]
9423cf4c11
deps: update dependency semver to v7.8.0 (#875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 12:38:50 +08:00
renovate[bot]
73b6193bf7
deps: update dependency eslint-plugin-promise to v7.3.0 (#862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-14 11:17:30 +08:00
renovate[bot]
21ff6b0307
deps: update dependency yaml to v2.8.4 (#861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-13 15:58:06 +08:00
mergify[bot]
b887e5ee01
ci: replace pull_request_target with two-step workflows (backport #841) (#854)
* ci: replace pull_request_target with two-step workflows (#841)

* ci: update PR auto assign workflows

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

* ci: update backport label workflow

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

* ci: update backport PR via mergify workflow

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

* ci: update add PR label workflow

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

* refactor: file name

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

* refactor: limit auto-assign-check for target branches

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 1a92265d039357ad0c1ae3641c6a9aa32a3ab146)

* ci: remove brackets in PR Management

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-05-04 15:23:15 +08:00
mergify[bot]
bf9b5866a7
docs: add AGENTS.md for AI agent guidance (#837) (#840)
* feat: add AGENNTS.md



* refactor: update based on copilot feedback



* refactor: update AGENTS.md



* refactor: update based on AI suggestion



* refactor: based on comments



* refactor: some files



* refactor: boundaries.md



---------


(cherry picked from commit 67bb6dfbd5d7f3632a138626ade5040188b76394)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-04-29 16:10:19 +08:00
Andy Lee
b97017e076
chore: bump to v1.8.1-dev (#836)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-28 15:49:20 +08:00
renovate[bot]
3740c9c424
deps: update minor dependencies (#834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 16:00:49 +08:00
renovate[bot]
af8e8b2eeb
deps: update patch dependencies (#833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 15:56:55 +08:00
mergify[bot]
7906033287
chore: bump @rancher/shell to 3.0.12-rc.1 (backport #810) (#821)
* chore: bump @rancher/shell to 3.0.12-rc.1 (#810)

* chore: bump @rancher/shell to 3.0.12-rc.1

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

* ci: update node version to 24

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

* ci: update build catalog yaml

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

* fix: nav items order

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

* refactor: remove unneeded weightType

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit afc0e0f5316aa99edbd03508b1d7dc87c835084c)

* fix: failed build-extension-charts CI (#822)

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-04-27 13:58:36 +08:00
mergify[bot]
5584c86789
fix: provisioner not update when deleting disk (#791) (#820)
* fix: provisioner not update when deleting disk



* refactor: update pkg/harvester/edit/harvesterhci.io.host/HarvesterDisk.vue




* refactor: based on feedback



---------



(cherry picked from commit c4d10183883b4e70e883733aae777c005978093e)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-27 13:51:11 +08:00
mergify[bot]
1dcb528717
chore: bump sha for upstream action pinning (#827) (#828)
(cherry picked from commit b7119d5c4c05ebc2180b09d5b6c29abe03454222)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-04-24 16:38:12 +08:00
Harvester Bot
36fc21ccbc
chore: bump version to 1.8.0 (#826)
Signed-off-by: Harvester Bot <94133267+harvesterhci-io-github-bot@users.noreply.github.com>
2026-04-24 15:52:17 +08:00
mergify[bot]
8803b79524
fix: change auth/V3user to auth/user (#824) (#825)
* fix: change auth/V3user to auth/user



* refactor: extract to utils/auth.js



---------


(cherry picked from commit 6fdd1e3954ca730a082c72e558fd79a54b65084f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-04-22 15:42:30 +08:00
Harvester Bot
3f80a71303
chore: bump version to 1.8.0-rc6 (#812)
Signed-off-by: Harvester Bot <94133267+harvesterhci-io-github-bot@users.noreply.github.com>
2026-04-16 13:43:38 +08:00
Andy Lee
13e4154335
Revert "deps: update patch digest dependencies (#796)" (#809)
This reverts commit 7788161d94b23917be580ef6676aef7fdb3c243e.
2026-04-13 16:34:30 +08:00
renovate[bot]
a80b87ce4f
deps: update fossas/fossa-action action to v1.9.0 (#797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 16:24:18 +08:00
mergify[bot]
809b9925fa
ci: add add_minrelaseday to delay dep update (#798) (#800)
(cherry picked from commit 6dd9b333365f172bf7a9993a9e3a82c9d49b27ad)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-04-07 15:30:12 +08:00
renovate[bot]
7788161d94
deps: update patch digest dependencies (#796)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-05 08:34:47 +00:00
mergify[bot]
90b8a821a1
chore: bump version to 1.8.0-rc5 (#789) (#790)
(cherry picked from commit 1f9e9b336ba94bc7ec423680666b4303c706e259)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-04-01 09:17:31 +08:00
mergify[bot]
87ff014bd8
refactor: add banner in PCI Devices page (#785) (#788)
* refactor: add banner in PCI Devices page



* refactor: based on copilot review



---------


(cherry picked from commit c5b4f6cd1ee6a23d14f251b6d45d13e7c2274b2b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-04-01 09:05:00 +08:00
mergify[bot]
20328b06e6
fix: the rancher-eio/read-vault-secret sha (#786) (#787)
(cherry picked from commit 4ce35ce075e7a1fd77fd82e9f20e932c05565216)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-31 14:09:22 +08:00
mergify[bot]
bbc415a2b4
chore: bump version to 1.8.0-rc4 (#778) (#779)
(cherry picked from commit 9d698b1230912a23515c2204220408bba122fb4c)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-27 16:42:53 +08:00
mergify[bot]
c12d4b5fba
feat: support advance option and creation from DataVolume (#776) (#777)
* feat: support advance option and creation from DataVolume

    - advance option would let user change the accessMode/volumeMode
    - creation from DataVolume could supports various scenario with
      3rd-party storage



* feat: add data migration action on volume page



* refactor: use show advanced options link instead of checkbox



* feat: add feature flag



* feat: add feature flag for dataMigration action



---------




(cherry picked from commit 566e79eda5c08e84e431d5e048e4bb581e7d9aaf)

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-26 17:33:01 +08:00
mergify[bot]
dc91be11f9
chore: bump version to 1.8.0-rc3 (#774) (#775)
(cherry picked from commit 42ddcfc1feea10ee3441758f8852f74c3ecc3057)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-26 12:35:12 +08:00
mergify[bot]
e5b72499b5
fix: use default value got undefined value in payload (#772) (#773)
(cherry picked from commit ad3decf71f82e5158ddf6a22965babd79c346a67)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-25 17:12:51 +08:00
mergify[bot]
e66037b007
ci: remove the single quota for use commit hash (#767) (#771)
(cherry picked from commit 8083a41df04ed5c82346cf3798dd85eedcb1e13a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-25 14:13:08 +08:00
mergify[bot]
c676cad057
chore: pin GH Actions to commit sha (#765) (#766)
(cherry picked from commit 62801b3b1371a221f0c485abe50f22b005155fe7)

Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2026-03-25 14:08:49 +08:00
mergify[bot]
254abd4648
ci: add CODEOWNERS file (#768) (#769)
(cherry picked from commit 46b860260a738bdce7ac13a08f7ceabec6fc922c)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-25 13:42:42 +08:00
renovate[bot]
1df1d3534a
deps: update dependency yaml to v2.8.3 (#757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-22 13:06:18 +00:00
mergify[bot]
ff1e6e6e7f
docs: update install instruction doc link (#753) (#754)
(cherry picked from commit 97e93dba0b056cbae2062cad099b96615f22a0cb)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-20 16:53:54 +08:00
mergify[bot]
622242e4a5
refactor: change rwxNetwork setting to kind json (#751) (#752)
(cherry picked from commit 9a8a709e56432b1d6ccf1f30e92b5a7dbefdb355)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-20 14:48:49 +08:00
mergify[bot]
d8ea9be174
feat: introduce rwxNetwork setting (#746) (#750)
* feat: add rwxNetwork setting



* fix: network payload



---------


(cherry picked from commit d1949641a7b6f5055a0eba210196d5e83db08f1a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-19 16:19:44 +08:00
mergify[bot]
f8a479cfcf
feat: add storage migration operation (#724) (#749)
- add storage migration
    - add cancel storage migration


(cherry picked from commit 9c9f59c939706edac551692694ea4ab88ec192d0)

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2026-03-19 11:53:24 +08:00
mergify[bot]
6d5e584113
chore: bump to v1.8.0-rc2 (#747) (#748)
(cherry picked from commit ccc14c7fb99b430fc7f24e580fae7f0d8a606f09)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-19 11:31:07 +08:00
mergify[bot]
a9eb04195e
feat: introduce instance-manager-resources setting (#744) (#745)
(cherry picked from commit 2ba471907e6b869352fb78bbadb76eb3c1f26bb2)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-18 17:21:28 +08:00
mergify[bot]
0441c1f6e3
refactor: remove using resourceName to determine is vGPU device (#741) (#743)
(cherry picked from commit 5aea476f645d201afb26e39b7be7f7a8049c9925)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-18 17:16:28 +08:00
mergify[bot]
dde58269d5
fix: typo (#740) (#742)
(cherry picked from commit 519c7d9f1fc0d59817c62d476e20599e07643c0e)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-17 16:33:37 +08:00
mergify[bot]
b8111f0ad7
fix: reword the error message to focus on bootable volume (#736) (#738)
(cherry picked from commit a9c392c13feb3cfe4100843eb41e0501e6708aaf)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:46:08 +08:00
mergify[bot]
6d627f82e9
fix: container disks don't need volumeClaimTemplates but volumes (#735) (#737)
(cherry picked from commit 888ec7a50fdc2e8ffc7d5210ae0e39d30c3e43cc)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:45:40 +08:00
mergify[bot]
cfc7a76fe7
feat: add vGPU filter button and hide the enable/disable passthrough in PCIDevice page (#729) (#732)
* feat: add another filter button



* feat: add feature falg to hide vGPU enable/disable actions in PCIDevices page



* refactor: update with conditionally rendering



---------


(cherry picked from commit 23344e0c0759e970bbdc88bc463774053448e0a1)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:39:41 +08:00
mergify[bot]
71d3067354
feat: ensure the state is pending when perform cloning the efi (#730) (#734)
* feat: ensure the state is pending when perform cloning the efi



* feat: define harvesterhci.io/clone-backend-storage-status in labels-annotations.js



---------


(cherry picked from commit a2486a7d389ff86760f8456a73ef2202ed06ca02)

Signed-off-by: pohanhuang <pohan.huang@suse.com>
Co-authored-by: Po Han Huang <hhcs9527@gmail.com>
2026-03-11 15:39:01 +08:00
mergify[bot]
9ecc372009
chore: bump to v1.8.0-rc1 (#731) (#733)
(cherry picked from commit df3d249923a51ea10d9080fecea6c5b942f47f8a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:34:55 +08:00
82 changed files with 2678 additions and 5297 deletions

16
.github/renovate.json vendored
View File

@ -14,17 +14,10 @@
"semanticCommits": "enabled",
"semanticCommitType": "deps",
"prHourlyLimit": 12,
"digest": {
"enabled": false
},
"timezone": "Asia/Taipei",
"schedule": ["after 10am on sunday"],
"postUpdateOptions": ["yarnDedupeFewer"],
"packageRules": [
{
"matchUpdateTypes": ["digest"],
"enabled": false
},
{
"matchUpdateTypes": ["major"],
"enabled": false
@ -46,12 +39,11 @@
"reviewers": ["a110605", "houhoucoop"]
},
{
"matchUpdateTypes": ["patch"],
"automerge": false,
"matchUpdateTypes": ["patch", "digest"],
"automerge": true,
"minimumReleaseAge": "7 days",
"groupName": "patch dependencies",
"labels": ["patch-update"],
"reviewers": ["a110605", "houhoucoop"]
"groupName": "patch digest dependencies",
"labels": ["patch-update", "automerge"]
}
]
}

View File

@ -5,19 +5,6 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
> **Note:**
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
## Table of Contents
- [Installation](#installation)
- [Development Setup](#development-setup)
- [Commit Message Guidelines](#commit-message-guidelines)
- [Branch Structure](#branch-structure)
- [Testing Guidelines](#testing-guidelines)
- [Release](#release)
- [Contributing](#contributing)
- [License](#license)
## Installation
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
@ -170,13 +157,6 @@ To test the standalone UI, configure Harvester to load the UI from an external s
2. Set **ui-source** to `External`
3. Set **ui-index** to the desired URL
## Release
The Harvester UI Extension follows the [Harvester](https://github.com/harvester/harvester) release cycle. After RC1 is cut for a new Harvester version, we usually create and work from the corresponding release branch (for example, `release-harvester-v1.8`). The remaining RC builds and the final official release are published from that branch.
After Harvester releases a new version, update the Harvester entry in rancher/ui-plugin-charts [manifest.json](https://github.com/rancher/ui-plugin-charts/blob/aafd215debbc6cb3100e7ba4b0a542c932397acd/manifest.json#L133-L151). This ensures air-gapped users can pull the new Harvester UI Extension image.
## Contributing
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.

View File

@ -33,7 +33,7 @@ module.exports = {
'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [0, 'never'],
'body-leading-blank': [2, 'always'],
'body-max-line-length': [0, 'always', 100],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [2, 'always'],
'footer-max-line-length': [2, 'always', 100],
},

View File

@ -1,13 +1,13 @@
{
"name": "harvester-ui-extension",
"version": "1.9.0-dev",
"version": "1.8.1-dev",
"private": false,
"engines": {
"node": ">=24.0.0"
},
"dependencies": {
"@babel/plugin-transform-class-static-block": "7.29.7",
"@rancher/shell": "3.0.12-rc.3",
"@babel/plugin-transform-class-static-block": "7.28.6",
"@rancher/shell": "3.0.12-rc.1",
"@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5",
@ -21,7 +21,7 @@
"yaml": "^2.5.1"
},
"resolutions": {
"@types/node": "25.9.4",
"@types/node": "25.6.2",
"cronstrue": "2.59.0",
"d3-color": "3.1.0",
"ejs": "3.1.10",
@ -33,9 +33,9 @@
"merge": "2.1.1",
"node-forge": "1.4.0",
"nth-check": "2.1.1",
"qs": "6.15.2",
"roarr": "7.21.6",
"semver": "7.8.5",
"qs": "6.15.1",
"roarr": "7.21.4",
"semver": "7.8.0",
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
},
"scripts": {

View File

@ -1,186 +0,0 @@
<script>
import { Banner } from '@components/Banner';
import MatchExpressions from '@shell/components/form/MatchExpressions';
import ResourceTable from '@shell/components/ResourceTable';
import { _EDIT } from '@shell/config/query-params';
import { convert, simplify, matching as selectorMatching } from '@shell/utils/selector';
import throttle from 'lodash/throttle';
import { NODE } from '@shell/config/types';
import { NAME, AGE } from '@shell/config/table-headers';
export default {
name: 'HarvesterNodeSelector',
components: {
Banner,
MatchExpressions,
ResourceTable,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
required: true,
},
},
async fetch() {
this.updateMatchingResources();
},
data() {
const inStore = this.$store.getters['currentProduct'].inStore;
return {
matchingResources: {
matched: 0,
matches: [],
none: true,
sample: null,
total: 0,
},
tableHeaders: [
NAME,
{
name: 'host-ip',
labelKey: 'tableHeaders.hostIp',
search: ['internalIp'],
value: 'internalIp',
sort: ['internalIp'],
align: 'center',
},
{
name: 'cpuManager',
labelKey: 'harvester.tableHeaders.cpuManager',
value: 'id',
formatter: 'HarvesterCPUPinning',
width: 150,
align: 'center',
},
{
name: 'diskState',
labelKey: 'tableHeaders.diskState',
value: 'diskState',
formatter: 'HarvesterDiskState',
width: 130,
},
AGE,
],
inStore,
};
},
watch: {
value: {
handler: 'updateMatchingResources',
deep: true,
},
allResourcesInScope() {
this.updateMatchingResources();
},
},
computed: {
schema() {
return this.$store.getters[`${ this.inStore }/schemaFor`](NODE);
},
selectorExpressions: {
get() {
return convert(
this.value.matchLabels || {},
this.value.matchExpressions || []
);
},
set(selectorExpressions) {
const { matchLabels, matchExpressions } = simplify(selectorExpressions);
this.value['matchLabels'] = matchLabels;
this.value['matchExpressions'] = matchExpressions;
this.updateMatchingResources();
},
},
allNodes() {
return this.$store.getters[`${ this.inStore }/all`](NODE) || [];
},
allResourcesInScope() {
return this.allNodes.length;
},
},
methods: {
updateMatchingResources: throttle(function() {
const expressions = this.selectorExpressions;
const allNodes = this.allNodes;
// Empty expressions with no key = no match
const hasValidExpression = expressions.length > 0 && expressions.every((e) => !!e.key);
if (!hasValidExpression) {
this.matchingResources = {
matched: 0,
matches: [],
none: true,
sample: null,
total: allNodes.length,
};
return;
}
const matches = selectorMatching(allNodes, expressions, 'metadata.labels');
this.matchingResources = {
matched: matches.length,
matches,
none: matches.length === 0,
sample: matches[0]?.nameDisplay || null,
total: allNodes.length,
};
}, 100, { trailing: true })
},
};
</script>
<template>
<div>
<div class="row">
<div class="col span-12">
<MatchExpressions
v-model:value="selectorExpressions"
:mode="mode"
:show-remove="false"
:type="'node'"
:target-resources="allResourcesInScope"
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<Banner :color="(matchingResources.none ? 'warning' : 'success')">
<span v-clean-html="t('generic.selectors.matchingResources.matchesSome', matchingResources)" />
</Banner>
</div>
</div>
<div class="row">
<div class="col span-12">
<ResourceTable
:rows="matchingResources.matches"
:headers="tableHeaders"
key-field="id"
:table-actions="false"
:row-actions="false"
:schema="schema"
:groupable="false"
:search="false"
/>
</div>
</div>
</div>
</template>

View File

@ -122,7 +122,7 @@ export default {
return this.$store.getters['currentCluster'].isLocal;
},
canEditClusterMembers() {
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
},
},
};

View File

@ -58,14 +58,11 @@ export default {
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
// Defer so v-select can finish closing the dropdown before the popup steals focus
this.$nextTick(() => {
window.open(
url,
'_blank',
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
);
});
},
isEmpty(o) {

View File

@ -1,196 +0,0 @@
<script>
import { RadioGroup } from '@components/Form/Radio';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view';
export default {
name: 'HarvesterClusterPodSecurityStandard',
components: {
RadioGroup,
LabeledSelect,
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => ({}),
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
},
data() {
let enabled = false;
let whitelistedNamespaces = [];
let privilegedNamespaces = [];
let restrictedNamespaces = [];
try {
const parsed = JSON.parse(this.value.value || this.value.default || '{}');
enabled = !!parsed.enabled;
whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
} catch (e) {
enabled = false;
whitelistedNamespaces = [];
privilegedNamespaces = [];
restrictedNamespaces = [];
}
return {
enabled,
whitelistedNamespaces,
privilegedNamespaces,
restrictedNamespaces,
};
},
computed: {
enabledOptions() {
return [
{ label: this.t('generic.enabled'), value: true },
{ label: this.t('generic.disabled'), value: false },
];
},
allNamespaces() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](NAMESPACE).filter((ns) => !ns.isSystem).map((ns) => ns.id);
},
whitelistedOptions() {
const excluded = new Set([...this.privilegedNamespaces, ...this.restrictedNamespaces]);
return this.allNamespaces.filter((ns) => !excluded.has(ns));
},
privilegedOptions() {
const excluded = new Set([...this.whitelistedNamespaces, ...this.restrictedNamespaces]);
return this.allNamespaces.filter((ns) => !excluded.has(ns));
},
restrictedOptions() {
const excluded = new Set([...this.whitelistedNamespaces, ...this.privilegedNamespaces]);
return this.allNamespaces.filter((ns) => !excluded.has(ns));
},
},
methods: {
useDefault() {
try {
const parsed = JSON.parse(this.value.default || '{}');
this.enabled = !!parsed.enabled;
this.whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
this.privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
this.restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
} catch (e) {
this.enabled = false;
this.whitelistedNamespaces = [];
this.privilegedNamespaces = [];
this.restrictedNamespaces = [];
}
this.save();
},
onUpdateEnabled() {
if (!this.enabled) {
this.whitelistedNamespaces = [];
this.privilegedNamespaces = [];
this.restrictedNamespaces = [];
}
this.save();
},
updateWhitelisted(selected) {
this.whitelistedNamespaces = selected;
this.save();
},
updatePrivileged(selected) {
this.privilegedNamespaces = selected;
this.save();
},
updateRestricted(selected) {
this.restrictedNamespaces = selected;
this.save();
},
save() {
this.value.value = JSON.stringify({
enabled: this.enabled,
whitelistedNamespacesList: this.whitelistedNamespaces.join(','),
privilegedNamespacesList: this.privilegedNamespaces.join(','),
restrictedNamespacesList: this.restrictedNamespaces.join(','),
});
},
},
};
</script>
<template>
<div>
<div class="row mb-20">
<div class="col span-6">
<RadioGroup
v-model:value="enabled"
name="enabled"
:options="enabledOptions"
@update:value="onUpdateEnabled"
/>
</div>
</div>
<template v-if="enabled">
<div class="row mb-20">
<div class="col span-12">
<LabeledSelect
v-model:value="whitelistedNamespaces"
:label="t('harvester.setting.clusterPodSecurityStandard.whitelistedNamespaces.label')"
:options="whitelistedOptions"
:multiple="true"
:mode="mode"
@update:value="updateWhitelisted"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<LabeledSelect
v-model:value="privilegedNamespaces"
:label="t('harvester.setting.clusterPodSecurityStandard.privilegedNamespaces.label')"
:options="privilegedOptions"
:multiple="true"
:mode="mode"
@update:value="updatePrivileged"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<LabeledSelect
v-model:value="restrictedNamespaces"
:label="t('harvester.setting.clusterPodSecurityStandard.restrictedNamespaces.label')"
:options="restrictedOptions"
:multiple="true"
:mode="mode"
@update:value="updateRestricted"
/>
</div>
</div>
</template>
</div>
</template>

View File

@ -64,9 +64,6 @@ export default {
try {
parsedDefaultValue = JSON.parse(this.value.value);
if (typeof parsedDefaultValue.exclusiveVlan !== 'boolean') {
parsedDefaultValue.exclusiveVlan = false;
}
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
openVlan = true;
} catch (error) {
@ -75,8 +72,7 @@ export default {
vlan: '',
clusterNetwork: '',
range: '',
exclude: [],
exclusiveVlan: false
exclude: []
};
}
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
@ -98,10 +94,6 @@ export default {
},
computed: {
showExclusiveVlan() {
return this.networkType === L2VLAN &&
Number(this.parsedDefaultValue.vlan) !== 1;
},
showVlan() {
return this.networkType === L2VLAN;
},
@ -140,18 +132,6 @@ export default {
};
});
},
exclusiveVlanOptions() {
return [
{
label: this.t('generic.enabled'),
value: true
},
{
label: this.t('generic.disabled'),
value: false
}
];
},
},
watch: {
@ -194,8 +174,7 @@ export default {
vlan: '',
clusterNetwork: '',
range: '',
exclude: [],
exclusiveVlan: false
exclude: []
};
},
@ -301,15 +280,7 @@ export default {
label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan"
/>
<LabeledSelect
v-if="showExclusiveVlan"
v-model:value="parsedDefaultValue.exclusiveVlan"
class="mb-20"
:options="exclusiveVlanOptions"
:mode="mode"
label-key="harvester.setting.storageNetwork.exclusiveVlan"
@update:value="update"
/>
<LabeledSelect
v-model:value="parsedDefaultValue.clusterNetwork"
label-key="harvester.setting.storageNetwork.clusterNetwork"

View File

@ -65,15 +65,7 @@ const FEATURE_FLAGS = {
'vGPUAsPCIDevice',
'instanceManagerResourcesSetting',
'rwxNetworkSetting',
'createPVCWithDataVolume',
'clusterPodSecurityStandardSetting',
],
'v1.8.1': [],
'v1.9.0': [
'supportFilesystem',
'disableResourcePooling',
'expandOnlineEncryptedVolume',
'longhornV2HugepageSettings'
'createPVCWithDataVolume'
],
};

View File

@ -54,14 +54,6 @@ import { registerAddonSideNav } from '../utils/dynamic-nav';
const TEMPLATE = HCI.VM_VERSION;
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
const LOGGING_GROUP = 'Monitoring & Logging::Logging';
const OVERLAY_NETWORKS_GROUP = 'Overlay Networks';
const UNDERLAY_NETWORKS_GROUP = 'Underlay Networks';
const NAT_INTERNET_GROUP = `${ OVERLAY_NETWORKS_GROUP }::NAT & Internet`;
const GATEWAYS_GROUP = `${ NAT_INTERNET_GROUP }::Gateways`;
const EXTERNAL_IPS_GROUP = `${ NAT_INTERNET_GROUP }::External IPs`;
const RULES_GROUP = `${ NAT_INTERNET_GROUP }::Rules`;
const SOURCE_RULES_GROUP = `${ RULES_GROUP }::Source Rules`;
const DESTINATION_RULES_GROUP = `${ RULES_GROUP }::Destination Rules`;
export const PRODUCT_NAME = 'harvester';
@ -245,7 +237,6 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimport',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT }
@ -275,7 +266,6 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_V,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V }
@ -305,7 +295,6 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_O,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O }
@ -334,7 +323,6 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
@ -496,7 +484,6 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.CLUSTER_FLOW,
labelKey: 'harvester.logging.clusterFlow.label',
name: HCI.CLUSTER_FLOW,
namespaced: true,
@ -520,7 +507,6 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.CLUSTER_OUTPUT,
labelKey: 'harvester.logging.clusterOutput.label',
name: HCI.CLUSTER_OUTPUT,
namespaced: true,
@ -544,7 +530,6 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.FLOW,
labelKey: 'harvester.logging.flow.label',
name: HCI.FLOW,
namespaced: true,
@ -568,7 +553,6 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.OUTPUT,
labelKey: 'harvester.logging.output.label',
name: HCI.OUTPUT,
namespaced: true,
@ -589,53 +573,14 @@ export function init($plugin, store) {
[
HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT,
HCI.HOST_NETWORK_CONFIG,
HCI.VPC,
NETWORK_POLICY,
HCI.LB,
HCI.IP_POOL,
],
'networks'
);
basicType(
[HCI.VPC],
OVERLAY_NETWORKS_GROUP
);
basicType(
[NETWORK_POLICY],
OVERLAY_NETWORKS_GROUP
);
basicType(
[HCI.VPC_NAT_GATEWAY],
GATEWAYS_GROUP
);
basicType(
[HCI.IPTABLES_EIP],
EXTERNAL_IPS_GROUP
);
basicType(
[HCI.IPTABLES_SNAT_RULE],
SOURCE_RULES_GROUP
);
basicType(
[HCI.IPTABLES_DNAT_RULE],
DESTINATION_RULES_GROUP
);
basicType(
[HCI.PROVIDER_NETWORK],
UNDERLAY_NETWORKS_GROUP
);
basicType(
[HCI.VLAN],
UNDERLAY_NETWORKS_GROUP
);
basicType(
[
HCI.SCHEDULE_VM_BACKUP,
@ -647,11 +592,7 @@ export function init($plugin, store) {
);
weightGroup('networks', 494, true);
weightGroup('Overlay Networks', 493, true);
weightGroup('NAT & Internet', 492, true);
weightGroup('Rules', 491, true);
weightGroup('Underlay Networks', 490, true);
weightGroup('backupAndSnapshot', 489, true);
weightGroup('backupAndSnapshot', 493, true);
basicType(
[
@ -730,7 +671,7 @@ export function init($plugin, store) {
name: HCI.CLUSTER_NETWORK,
ifHaveType: HCI.CLUSTER_NETWORK,
namespaced: false,
weight: 484,
weight: 189,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.CLUSTER_NETWORK }
@ -745,14 +686,14 @@ export function init($plugin, store) {
},
resource: NETWORK_ATTACHMENT,
resourceDetail: HCI.NETWORK_ATTACHMENT,
resourceEdit: HCI.NETWORK_ATTACHMENT,
resourceEdit: HCI.NETWORK_ATTACHMENT
});
virtualType({
labelKey: 'harvester.network.label',
name: HCI.NETWORK_ATTACHMENT,
namespaced: true,
weight: 485,
weight: 188,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.NETWORK_ATTACHMENT }
@ -766,7 +707,7 @@ export function init($plugin, store) {
labelKey: 'harvester.vpc.label',
name: HCI.VPC,
namespaced: true,
weight: 195,
weight: 187,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VPC }
@ -775,73 +716,13 @@ export function init($plugin, store) {
ifHaveType: HCI.VPC,
});
configureType(HCI.VPC_NAT_GATEWAY, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.natGateway.label',
name: HCI.VPC_NAT_GATEWAY,
namespaced: false,
weight: 193,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VPC_NAT_GATEWAY }
},
exact: false,
ifHaveType: HCI.VPC_NAT_GATEWAY,
});
configureType(HCI.IPTABLES_EIP, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.externalIP.label',
name: HCI.IPTABLES_EIP,
namespaced: false,
weight: 192,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IPTABLES_EIP }
},
exact: false,
ifHaveType: HCI.IPTABLES_EIP,
});
configureType(HCI.IPTABLES_SNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.snat.label',
name: HCI.IPTABLES_SNAT_RULE,
namespaced: false,
weight: 191,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IPTABLES_SNAT_RULE }
},
exact: false,
ifHaveType: HCI.IPTABLES_SNAT_RULE,
});
configureType(HCI.IPTABLES_DNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.dnat.label',
name: HCI.IPTABLES_DNAT_RULE,
namespaced: false,
weight: 190,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IPTABLES_DNAT_RULE }
},
exact: false,
ifHaveType: HCI.IPTABLES_DNAT_RULE,
});
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.networkPolicy.label',
name: NETWORK_POLICY,
namespaced: true,
weight: 194,
weight: 186,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: NETWORK_POLICY }
@ -850,53 +731,6 @@ export function init($plugin, store) {
ifHaveType: NETWORK_POLICY,
});
configureType(HCI.PROVIDER_NETWORK, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.providerNetwork.label',
name: HCI.PROVIDER_NETWORK,
namespaced: false,
weight: 189,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.PROVIDER_NETWORK }
},
exact: false,
ifHaveType: HCI.PROVIDER_NETWORK,
});
configureType(HCI.VLAN, { hiddenNamespaceGroupButton: true, canYaml: false });
headers(HCI.VLAN, [
STATE,
NAME_COL,
{
name: 'id',
label: 'ID',
value: 'spec.id',
sort: 'spec.id'
},
{
name: 'provider',
labelKey: 'harvester.subnet.provider.label',
value: 'spec.provider',
sort: 'spec.provider'
}
]);
virtualType({
labelKey: 'harvester.vlanNetwork.label',
name: HCI.VLAN,
namespaced: false,
weight: 188,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VLAN }
},
exact: false,
ifHaveType: HCI.VLAN,
});
configureType(HCI.SNAPSHOT, {
isCreatable: false,
location: {
@ -1251,7 +1085,7 @@ export function init($plugin, store) {
labelKey: 'harvester.loadBalancer.label',
name: HCI.LB,
namespaced: true,
weight: 483,
weight: 185,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.LB }
@ -1290,7 +1124,7 @@ export function init($plugin, store) {
labelKey: 'harvester.ipPool.label',
name: HCI.IP_POOL,
namespaced: false,
weight: 482,
weight: 184,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IP_POOL }
@ -1299,24 +1133,4 @@ export function init($plugin, store) {
ifHaveType: HCI.IP_POOL,
});
headers(HCI.IP_POOL, IP_POOL_HEADERS);
configureType(HCI.HOST_NETWORK_CONFIG, {
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.HOST_NETWORK_CONFIG }
},
canYaml: false,
});
virtualType({
labelKey: 'harvester.hostNetworkConfig.label',
name: HCI.HOST_NETWORK_CONFIG,
namespaced: false,
weight: 481,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.HOST_NETWORK_CONFIG }
},
exact: false,
ifHaveType: HCI.HOST_NETWORK_CONFIG,
});
}

View File

@ -88,8 +88,6 @@ export const CSI_SECRETS = {
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
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',
CSI_NODE_EXPAND_SECRET_NAME: 'csi.storage.k8s.io/node-expand-secret-name',
CSI_NODE_EXPAND_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-expand-secret-namespace'
};
// Some harvester CRD type is not equal to model file name, define the mapping here

View File

@ -19,7 +19,6 @@ export const HCI = {
NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
@ -82,5 +81,4 @@ export const HCI = {
MAC_ADDRESS: 'harvesterhci.io/mac-address',
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks',
};

View File

@ -35,16 +35,13 @@ export const HCI_SETTING = {
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED: 'longhorn-v2-data-engine-hugepage-enabled',
LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE: 'longhorn-v2-data-engine-memory-size',
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
KUBEVIRT_MIGRATION: 'kubevirt-migration',
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
};
export const HCI_ALLOWED_SETTINGS = {
@ -114,14 +111,6 @@ export const HCI_ALLOWED_SETTINGS = {
experimental: true,
featureFlag: 'longhornV2LVMSupport'
},
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
kind: 'boolean',
featureFlag: 'longhornV2HugepageSettings'
},
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
kind: 'number',
featureFlag: 'longhornV2HugepageSettings'
},
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
[HCI_SETTING.UPGRADE_CONFIG]: {
kind: 'json',
@ -141,9 +130,6 @@ export const HCI_ALLOWED_SETTINGS = {
},
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
},
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
}
};

View File

@ -46,9 +46,3 @@ export const CDI_POPULATOR_KIND = {
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
};
export const FILESYSTEM_SOURCE_TYPE = {
CONFIGMAP: 'configmap',
SECRET: 'secret',
SERVICEACCOUNT: 'serviceaccount',
};

View File

@ -27,11 +27,15 @@ export default {
await allHash({
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
vmims: this.$store.dispatch('harvester/findAll', { type: HCI.VMIM }),
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
});
},
computed: {
allClusterNetwork() {
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
},
rows() {
const vms = this.$store.getters['harvester/all'](HCI.VM);
@ -104,6 +108,7 @@ export default {
<HarvesterVmState
class="vmstate"
:row="scope.row"
:all-cluster-network="allClusterNetwork"
/>
</div>
</template>

View File

@ -98,9 +98,9 @@ export default {
methods: {
escapeHtml,
close(data) {
close() {
this.errors = [];
this.$emit('close', data);
this.$emit('close');
},
async apply(buttonDone) {
@ -109,7 +109,7 @@ export default {
await resource.doActionGrowl(this.modalData.action, {});
}
buttonDone(true);
this.close({ performCallback: true, clearTableSelection: true });
this.close();
} catch (e) {
this.errors = exceptionToErrorsArray(e);
buttonDone(false);

View File

@ -1,8 +1,6 @@
<script>
import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import { Card } from '@components/Card';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types';
@ -15,9 +13,7 @@ export default {
components: {
AsyncButton,
Banner,
Card,
Checkbox,
},
props: {
@ -28,16 +24,10 @@ export default {
},
data() {
return { disableResourcePooling: false };
return {};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
disableResourcePoolingEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('disableResourcePooling');
},
},
computed: { ...mapGetters({ t: 'i18n/t' }) },
methods: {
close() {
@ -64,8 +54,7 @@ export default {
spec: {
address: actionResource.status.address,
nodeName: actionResource.status.nodeName,
userName,
disableResourcePooling: this.disableResourcePooling,
userName
}
} );
@ -96,19 +85,7 @@ export default {
</template>
<template #body>
<p class="mb-20">
{{ t('harvester.pci.enablePassthroughWarning') }}
</p>
<template v-if="disableResourcePoolingEnabled">
<Checkbox
v-model:value="disableResourcePooling"
label-key="harvester.pci.disableResourcePooling"
/>
<Banner
color="info"
:label="t('harvester.pci.disableResourcePoolingDescription')"
/>
</template>
</template>
<template #actions>

View File

@ -1,154 +0,0 @@
<script>
import merge from 'lodash/merge';
import jsyaml from 'js-yaml';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { LabeledInput } from '@components/Form/LabeledInput';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
export default {
name: 'HarvesterEnableNvidiaDriverToolkit',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
const addon = this.resources[0];
let valuesContentJson;
try {
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(addon.spec.valuesContent));
} catch (e) {
valuesContentJson = { ...DEFAULT_VALUE };
}
return { valuesContentJson };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
buttonDisabled() {
const { image, driverLocation } = this.valuesContentJson;
return !(image?.repository || '').trim() || !(image?.tag || '').trim() || !(driverLocation || '').trim();
}
},
methods: {
close() {
this.$emit('close');
},
async enable(buttonCb) {
const addon = this.resources[0];
try {
addon.spec.valuesContent = jsyaml.dump(this.valuesContentJson);
addon.spec.enabled = true;
await addon.save();
buttonCb(true);
this.close();
} catch (err) {
addon.spec.enabled = false;
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(addon.metadata.name) }),
err,
}, { root: true });
buttonCb(false);
}
}
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
<h4
v-clean-html="t('harvester.addons.nvidiaDriverToolkit.enable.title')"
class="text-default-text"
/>
</template>
<template #body>
<div class="body">
<div class="row mb-15">
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.image.repository"
:required="true"
:label="t('harvester.addons.nvidiaDriverToolkit.image.repository')"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.image.tag"
:required="true"
:label="t('harvester.addons.nvidiaDriverToolkit.image.tag')"
/>
</div>
</div>
<div class="row mb-15">
<div class="col span-12">
<LabeledInput
v-model:value="valuesContentJson.driverLocation"
:required="true"
:label="t('harvester.addons.nvidiaDriverToolkit.driver.location')"
/>
</div>
</div>
</div>
</template>
<template #actions>
<div class="buttons actions">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="enable"
:disabled="buttonDisabled"
@click="enable"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.body {
display: flex;
flex-direction: column;
min-width: 400px;
}
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>

View File

@ -1,12 +1,15 @@
<script>
import { mapGetters } from 'vuex';
import { NODE } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI } from '../types';
export default {
emits: ['close'],
@ -59,46 +62,28 @@ export default {
return this.resources[0];
},
anyCpuPinning() {
return this.resources.some((r) => r.isCpuPinning);
},
vmi() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
vmsByNode() {
const groups = {};
for (const r of this.resources) {
const node = r.nodeName || '';
const name = r.nameDisplay || r.name || r.id;
if (!groups[node]) {
groups[node] = [];
}
groups[node].push(name);
}
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
return resource;
},
cpuPinningAlertMessage() {
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
},
allVmsOnTargetNode() {
if (!this.nodeName) {
return false;
}
return this.resources.every((r) => r.nodeName === this.nodeName);
},
nodeNameList() {
const nodes = this.$store.getters['harvester/all'](NODE);
return nodes.filter((n) => {
const isNotSelfNode = !!this.availableNodes.includes(n.id);
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
const isCpuPinning = this.actionResource?.isCpuPinning;
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
return isNotWitnessNode && matchingCpuManagerConfig;
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
}).map((n) => {
let label = n?.metadata?.name;
const value = n?.metadata?.name;
@ -117,10 +102,10 @@ export default {
},
methods: {
close(data) {
close() {
this.nodeName = '';
this.errors = [];
this.$emit('close', data);
this.$emit('close');
},
async apply(buttonDone) {
@ -141,32 +126,10 @@ export default {
}
try {
// Filter out VMs already running on the selected node
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
const failedMigrations = results
.map((result, index) => ({ resource: toMigrate[index], result }))
.filter(({ result }) => result.status === 'rejected');
if (failedMigrations.length) {
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
const error = result.reason?.data || result.reason;
const messages = exceptionToErrorsArray(error);
return messages.map((message) => `${ vmName }: ${ message }`);
});
buttonDone(false);
return;
}
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
buttonDone(true);
this.close({ performCallback: true, clearTableSelection: true });
this.close();
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
@ -183,35 +146,17 @@ export default {
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
{{ t('harvester.modal.migration.title') }}
</template>
<template #body>
<Banner
v-if="anyCpuPinning"
v-if="actionResource?.isCpuPinning"
color="warning"
:label="cpuPinningAlertMessage"
/>
<p>
{{ t('harvester.modal.migration.selectedVMs') }}
</p>
<ul class="vm-list">
<li
v-for="group in vmsByNode"
:key="group.node"
>
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
<span
v-if="nodeName && group.node === nodeName"
class="already-on-target"
>
({{ t('harvester.modal.migration.alreadyOnTarget') }})
</span>
</li>
</ul>
<LabeledSelect
v-model:value="nodeName"
class="mt-15"
:label="t('harvester.modal.migration.fields.nodeName.label')"
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
:options="nodeNameList"
@ -238,7 +183,7 @@ export default {
<AsyncButton
mode="apply"
:disabled="!nodeName || allVmsOnTargetNode"
:disabled="!nodeName"
@click="apply"
/>
</div>
@ -256,16 +201,4 @@ export default {
justify-content: flex-end;
width: 100%;
}
.already-on-target {
color: var(--warning);
font-style: italic;
}
.vm-list {
list-style: disc;
padding-left: 1.5em;
margin-bottom: 10px;
margin-top: 10px;
}
</style>

View File

@ -108,7 +108,6 @@ export default {
<YamlEditor
ref="yamlUser"
v-model:value="config"
:mode="mode"
class="yaml-editor"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
@onChanges="update"

View File

@ -8,7 +8,6 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
import { randomStr } from '@shell/utils/string';
import CreateEditView from '@shell/mixins/create-edit-view';
import { getLoginAwareErrors } from '../utils/error';
export default {
name: 'HarvesterEditKeypair',
@ -64,14 +63,6 @@ export default {
}
},
computed: {
normalizedErrors() {
const message = this.t('harvester.virtualMachine.genericLoginError');
return getLoginAwareErrors(this.errors, message);
}
},
methods: { onKeySelected: createOnSelected('publicKey') },
};
</script>
@ -81,9 +72,10 @@ export default {
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="normalizedErrors"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<div class="header mb-20">
<FileSelector

View File

@ -9,7 +9,6 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { NAMESPACE, NODE } from '@shell/config/types';
import { HCI } from '../types';
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
import { removeObject } from '@shell/utils/array';
@ -21,7 +20,6 @@ const { ACCESS, TRUNK } = L2VLAN_MODE;
const AUTO = 'auto';
const MANUAL = 'manual';
const KUBE_SYSTEM = 'kube-system';
export default {
emits: ['update:value'],
@ -72,12 +70,7 @@ export default {
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
});
await allHash({ clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }) });
},
created() {
@ -206,80 +199,6 @@ export default {
}
return this.type === UNTAGGED;
},
showNicsTab() {
return this.isOverlayNetwork && this.value.metadata.namespace === KUBE_SYSTEM;
},
namespaceOptions() {
const ns = this.$store.getters['harvester/all'](NAMESPACE) || [];
// Allow users to select the "kube-system" namespace as the external subnet from Kube-OVN.
// This expects the provider network to be in the "kube-system" namespace for VPC NAT gateway functionality.
return ns
.filter((ns) => !ns.isSystem || ns.id === KUBE_SYSTEM)
.map((ns) => ({ name: ns.id }))
.sort((a, b) => a.name.localeCompare(b.name));
},
nodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
return nodes.filter((n) => n.isEtcd !== 'true');
},
nics() {
const inStore = this.$store.getters['currentProduct'].inStore;
const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
const linkStatus = linkMonitor?.status?.linkStatus || {};
const nodes = this.nodes.map((n) => n.id);
const out = [];
// Collect all nics from all nodes (for overlay, we select from all nodes)
Object.keys(linkStatus).map((nodeName) => {
if (nodes.includes(nodeName)) {
const nics = linkStatus[nodeName] || [];
nics.map((nic) => {
out.push({
...nic,
nodeName,
});
});
}
});
return out;
},
nicOptions() {
const out = [];
const seen = new Set();
(this.nics || []).forEach((nic) => {
if (!seen.has(nic.name)) {
seen.add(nic.name);
out.push({
label: nic.name,
value: nic.name,
});
}
});
return out.sort((a, b) => a.label.localeCompare(b.label));
},
master: {
get() {
return this.config?.master || '';
},
set(value) {
this.config.master = value;
}
}
},
@ -295,7 +214,6 @@ export default {
this.config.ipam = {};
this.config.bridge = '';
delete this.config.provider;
delete this.config.master;
delete this.config.server_socket;
}
},
@ -312,13 +230,6 @@ export default {
this.config.vlan = '';
}
},
'value.metadata.namespace'(newNamespace) {
// NIC selection is only valid for overlay in kube-system namespace.
if (newNamespace !== KUBE_SYSTEM) {
delete this.config.master;
this.value.spec.config = JSON.stringify({ ...this.config });
}
},
},
methods: {
@ -413,10 +324,6 @@ export default {
delete this.config.promiscMode;
delete this.config.vlan;
delete this.config.ipam;
if (this.value.metadata.namespace !== KUBE_SYSTEM) {
delete this.config.master;
}
}
if (this.isUntaggedNetwork) {
@ -443,7 +350,6 @@ export default {
ref="nd"
:value="value"
:mode="mode"
:namespace-options="namespaceOptions"
@update:value="$emit('update:value', $event)"
/>
<Tabbed
@ -615,25 +521,6 @@ export default {
</div>
</div>
</Tab>
<Tab
v-if="showNicsTab"
name="nics"
:label="t('harvester.network.tabs.nic')"
:weight="97"
class="bordered-table"
>
<div class="row mt-10">
<div class="col span-12">
<LabeledSelect
v-model:value="master"
:label="t('harvester.vlanConfig.uplink.nics.label')"
:placeholder="t('harvester.vlanConfig.uplink.nics.overlayWarning')"
:mode="mode"
:options="nicOptions"
/>
</div>
</div>
</Tab>
</Tabbed>
</CruResource>
</template>

View File

@ -3,9 +3,8 @@ import KeyValue from '@shell/components/form/KeyValue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import RadioGroup from '@components/Form/Radio/RadioGroup';
import Checkbox from '@components/Form/Checkbox/Checkbox';
import { SECRET, LONGHORN } from '@shell/config/types';
import { _CREATE, _VIEW, _EDIT } from '@shell/config/query-params';
import { _CREATE, _VIEW } from '@shell/config/query-params';
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
import { clone } from '@shell/utils/object';
import { uniq } from '@shell/utils/array';
@ -17,9 +16,7 @@ const {
CSI_NODE_PUBLISH_SECRET_NAME,
CSI_NODE_PUBLISH_SECRET_NAMESPACE,
CSI_NODE_STAGE_SECRET_NAME,
CSI_NODE_STAGE_SECRET_NAMESPACE,
CSI_NODE_EXPAND_SECRET_NAME,
CSI_NODE_EXPAND_SECRET_NAMESPACE
CSI_NODE_STAGE_SECRET_NAMESPACE
} = CSI_SECRETS;
export default {
@ -30,7 +27,6 @@ export default {
LabeledSelect,
LabeledInput,
RadioGroup,
Checkbox,
},
props: {
@ -61,10 +57,7 @@ export default {
};
}
const hasExpandSecret = !!(this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAME] && this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAMESPACE]);
const volumeExpansionCheckBoxEnabled = this.realMode === _CREATE ? true : hasExpandSecret;
return { volumeExpansionCheckBoxEnabled };
return { };
},
computed: {
@ -105,11 +98,8 @@ export default {
}, []);
},
isEdit() {
return this.realMode === _EDIT;
},
isView() {
return this.realMode === _VIEW;
return this.mode === _VIEW;
},
migratableOptions() {
@ -162,10 +152,6 @@ export default {
}
},
enableOnlineExpansionVolumeEncryption() {
return this.value.expandOnlineEncryptedVolumeFeatureEnabled;
},
volumeEncryption: {
set(neu) {
this.value['parameters'] = {
@ -194,11 +180,6 @@ export default {
set(selectedSecret) {
const [namespace, name] = selectedSecret.split('/');
const expandSecretParams = (this.enableOnlineExpansionVolumeEncryption && this.volumeExpansionCheckBoxEnabled) ? {
[CSI_NODE_EXPAND_SECRET_NAME]: name,
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
} : {};
this.value['parameters'] = {
...this.value.parameters,
[CSI_PROVISIONER_SECRET_NAME]: name,
@ -206,8 +187,7 @@ export default {
[CSI_NODE_STAGE_SECRET_NAME]: name,
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace,
...expandSecretParams,
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace
};
}
},
@ -260,32 +240,6 @@ export default {
}
},
},
watch: {
volumeExpansionCheckBoxEnabled(enabled) {
const currentSecret = this.secret;
if (!currentSecret) {
return;
}
const [namespace, name] = currentSecret.split('/');
if (enabled && this.enableOnlineExpansionVolumeEncryption) {
this.value['parameters'] = {
...this.value.parameters,
[CSI_NODE_EXPAND_SECRET_NAME]: name,
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
};
} else {
const params = { ...this.value.parameters };
delete params[CSI_NODE_EXPAND_SECRET_NAME];
delete params[CSI_NODE_EXPAND_SECRET_NAMESPACE];
this.value['parameters'] = params;
}
},
},
};
</script>
<template>
@ -383,16 +337,6 @@ export default {
:mode="mode"
/>
</div>
<div
v-if="enableOnlineExpansionVolumeEncryption"
class="col span-6 flex items-center mt-20"
>
<Checkbox
v-model:value="volumeExpansionCheckBoxEnabled"
:label="t('harvester.storage.volumeExpansionCheckbox')"
:disabled="isEdit || isView"
/>
</div>
</div>
</template>
<KeyValue

View File

@ -254,24 +254,15 @@ export default {
v-if="restoreNewVm"
v-model:value="restoreResource.spec.keepMacAddress"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.keepMacAddress')"
/>
<LabeledSelect
v-if="!restoreNewVm"
v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption"
/>
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div>
<Footer

View File

@ -26,7 +26,6 @@ import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
import Filesystem from './kubevirt.io.virtualmachine/VirtualMachineFilesystem';
export default {
name: 'HarvesterEditVMTemplate',
@ -52,7 +51,6 @@ export default {
UnitInput,
Banner,
KeyValue,
Filesystem,
},
mixins: [CreateEditView, VM_MIXIN],
@ -97,10 +95,6 @@ export default {
secretNamePrefix() {
return this.templateValue?.metadata?.name;
},
filesystemEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
},
},
watch: {
@ -160,7 +154,6 @@ export default {
mounted() {
this.imageId = this.diskRows[0]?.image || '';
this['filesystemRows'] = this.getFilesystemRows(this.value.spec.vm);
},
methods: {
@ -356,19 +349,6 @@ export default {
</template>
</Tab>
<Tab
v-if="filesystemEnabled"
name="filesystem"
:label="t('harvester.tab.filesystem')"
:weight="-8"
>
<Filesystem
v-model:value="filesystemRows"
:mode="mode"
:namespace="templateValue.metadata.namespace"
/>
</Tab>
<Tab
name="labels"
:label="t('generic.labels')"

View File

@ -2,7 +2,6 @@
import Footer from '@shell/components/form/Footer';
import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput';
import Checkbox from '@components/Form/Checkbox/Checkbox';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
@ -33,7 +32,6 @@ const createObject = {
export default {
name: 'CreateRestore',
components: {
Checkbox,
Footer,
RadioGroup,
LabeledInput,
@ -251,17 +249,9 @@ export default {
<LabeledSelect
v-if="!restoreNewVm"
v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption"
/>
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div>
<Footer

View File

@ -1,190 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
eip: this.value?.spec?.eip || '',
externalPort: this.value?.spec?.externalPort || '',
internalIp: this.value?.spec?.internalIp || '',
internalPort: this.value?.spec?.internalPort || '',
protocol: this.value?.spec?.protocol || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
eipOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
return eips.map((eip) => ({
label: eip.id,
value: eip.id,
}));
},
protocolOptions() {
return [
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
];
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.eip = this.eip;
this.value.spec.externalPort = this.externalPort;
this.value.spec.internalIp = this.internalIp;
this.value.spec.internalPort = this.internalPort;
this.value.spec.protocol = this.protocol;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="eip"
class="mb-20"
:options="eipOptions"
:mode="mode"
:label="t('harvester.dnat.eip.label')"
:placeholder="t('harvester.dnat.eip.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="externalPort"
class="mb-20"
:mode="mode"
:label="t('harvester.dnat.externalPort.label')"
:placeholder="t('harvester.dnat.externalPort.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="internalIp"
class="mb-20"
:mode="mode"
:label="t('harvester.dnat.internalIp.label')"
:placeholder="t('harvester.dnat.internalIp.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="internalPort"
class="mb-20"
:mode="mode"
:label="t('harvester.dnat.internalPort.label')"
:placeholder="t('harvester.dnat.internalPort.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="protocol"
class="mb-20"
:options="protocolOptions"
:mode="mode"
:label="t('harvester.dnat.protocol.label')"
:placeholder="t('harvester.dnat.protocol.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -1,168 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
natGwDp: this.value?.spec?.natGwDp || '',
externalSubnet: this.value?.spec?.externalSubnet || '',
v4ip: this.value?.spec?.v4ip || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
natGateways: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC_NAT_GATEWAY }),
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
});
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
natGatewayOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const natGateways = this.$store.getters[`${ inStore }/all`](HCI.VPC_NAT_GATEWAY) || [];
return natGateways.map((gw) => ({
label: gw.id,
value: gw.id,
}));
},
subnetOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
return subnets.map((subnet) => ({
label: subnet.id,
value: subnet.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.natGwDp = this.natGwDp;
this.value.spec.externalSubnet = this.externalSubnet;
this.value.spec.v4ip = this.v4ip;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="natGwDp"
class="mb-20"
:options="natGatewayOptions"
:mode="mode"
:label="t('harvester.externalIP.natGateway.label')"
:placeholder="t('harvester.externalIP.natGateway.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="externalSubnet"
class="mb-20"
:options="subnetOptions"
:label="t('harvester.externalIP.externalSubnet.label')"
:placeholder="t('harvester.externalIP.externalSubnet.placeholder')"
:mode="mode"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="v4ip"
class="mb-20"
:label="t('harvester.externalIP.v4ip.label')"
:placeholder="t('harvester.externalIP.v4ip.placeholder')"
:mode="mode"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -1,140 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
eip: this.value?.spec?.eip || '',
internalCIDR: this.value?.spec?.internalCIDR || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
eipOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
return eips.map((eip) => ({
label: eip.id,
value: eip.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.eip = this.eip;
this.value.spec.internalCIDR = this.internalCIDR;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="eip"
class="mb-20"
:options="eipOptions"
:mode="mode"
:label="t('harvester.snat.eip.label')"
:placeholder="t('harvester.snat.eip.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="internalCIDR"
class="mb-20"
:mode="mode"
:label="t('harvester.snat.internalCIDR.label')"
:placeholder="t('harvester.snat.internalCIDR.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -1,384 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import ArrayList from '@shell/components/form/ArrayList';
import InfoBox from '@shell/components/InfoBox';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { NODE } from '@shell/config/types';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
ArrayList,
InfoBox,
NameNsDescription,
ResourceTabs,
Tab,
LabeledSelect,
ArrayListSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
defaultInterface: this.value?.spec?.defaultInterface || '',
excludedNodes: this.value?.spec?.excludeNodes || [],
customInterfaces: this.value?.spec?.customInterfaces || [],
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
});
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
nodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
return nodes.filter((n) => n.isEtcd !== 'true');
},
nics() {
const inStore = this.$store.getters['currentProduct'].inStore;
const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
const linkStatus = linkMonitor?.status?.linkStatus || {};
const nodes = this.nodes.map((n) => n.id);
const out = [];
// Collect all nics from all nodes
Object.keys(linkStatus).map((nodeName) => {
if (nodes.includes(nodeName)) {
const nics = linkStatus[nodeName] || [];
nics.map((nic) => {
out.push({
...nic,
nodeName,
});
});
}
});
return out;
},
nicOptions() {
const out = [];
const seen = new Set();
(this.nics || []).forEach((nic) => {
if (!seen.has(nic.name)) {
seen.add(nic.name);
out.push({
label: nic.name,
value: nic.name,
});
}
});
return out.sort((a, b) => a.label.localeCompare(b.label));
},
nodeOptions() {
return this.nodes.map((node) => ({
label: node.id,
value: node.id,
}));
},
},
methods: {
removeCustomInterface(index) {
this.customInterfaces.splice(index, 1);
},
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.defaultInterface = this.defaultInterface;
this.value.spec.excludeNodes = this.excludedNodes;
this.value.spec.customInterfaces = (this.customInterfaces || [])
.filter((item) => item?.interface || (item?.nodes || []).length)
.map((item) => ({
interface: item.interface || '',
nodes: (item.nodes || []).filter((node) => !!node),
}))
.filter((item) => item.interface && item.nodes.length > 0);
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="Interfaces"
label="Interfaces"
:weight="99"
>
<LabeledSelect
v-model:value="defaultInterface"
class="mb-20"
required
:options="nicOptions"
:mode="mode"
:label="t('harvester.providerNetwork.defaultInterface.label')"
:placeholder="t('harvester.providerNetwork.defaultInterface.placeholder')"
/>
<hr class="section-divider" />
<ArrayList
v-model:value="customInterfaces"
class="mb-20 custom-interface-list"
:mode="mode"
:title="t('harvester.providerNetwork.customInterfaces.label')"
:protip="false"
:remove-allowed="false"
:initial-empty-row="true"
:default-add-value="{ interface: '', nodes: [] }"
>
<template #add="{ add }">
<div class="custom-interface-primary-add">
<button
type="button"
class="btn role-primary"
:disabled="mode === 'view'"
@click="add"
>
{{ t('harvester.providerNetwork.customInterfaces.addLabel') }}
</button>
</div>
</template>
<template #column-headers>
<div class="row custom-interface-header">
<div class="col span-6">
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
</div>
<div class="col span-6">
{{ t('harvester.providerNetwork.customInterfaces.nodes.label') }}
</div>
</div>
</template>
<template #columns="scope">
<InfoBox class="custom-interface-box">
<button
v-if="mode !== 'view'"
type="button"
class="role-link btn btn-sm remove"
@click="removeCustomInterface(scope.i)"
>
<i class="icon icon-x" />
</button>
<div class="custom-interface-content">
<div class="row custom-interface-row interface-row">
<div class="col span-12 interface-col">
<h3 class="mb-10">
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
</h3>
<LabeledSelect
v-model:value="scope.row.value.interface"
class="mb-20"
:label="''"
:options="nicOptions"
:mode="mode"
:placeholder="t('harvester.providerNetwork.customInterfaces.interface.placeholder')"
/>
</div>
</div>
<div class="row custom-interface-row nodes-row">
<div class="col span-12">
<ArrayListSelect
v-model:value="scope.row.value.nodes"
:options="nodeOptions"
:mode="mode"
:disabled="mode === 'view'"
:enable-default-add-value="false"
:array-list-props="{
addLabel: t('harvester.providerNetwork.customInterfaces.nodes.addLabel'),
initialEmptyRow: true,
title: t('harvester.providerNetwork.customInterfaces.nodes.label'),
required: false,
protip: false,
}"
:select-props="{
placeholder: t('harvester.providerNetwork.customInterfaces.nodes.placeholder'),
disabled: mode === 'view',
}"
>
<template #add="{ add }">
<div class="custom-interface-add">
<button
type="button"
class="btn role-tertiary add"
:disabled="mode === 'view'"
@click="add"
>
{{ t('harvester.providerNetwork.customInterfaces.nodes.addLabel') }}
</button>
</div>
</template>
</ArrayListSelect>
</div>
</div>
</div>
</InfoBox>
</template>
</ArrayList>
</Tab>
<Tab
name="excludedNodes"
:label="t('harvester.providerNetwork.excludedNodes.label')"
:weight="98"
>
<div class="row">
<div class="col span-12">
<ArrayListSelect
v-model:value="excludedNodes"
:options="nodeOptions"
:disabled="mode === 'view'"
:mode="mode"
:enable-default-add-value="false"
:array-list-props="{
addLabel: t('harvester.providerNetwork.excludedNodes.addLabel'),
initialEmptyRow: true,
required: false,
protip: false,
}"
:select-props="{
placeholder: t('harvester.providerNetwork.excludedNodes.placeholder'),
disabled: mode === 'view',
}"
/>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>
<style lang="scss" scoped>
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 10px 0 20px;
}
.custom-interface-header {
margin-bottom: 10px;
font-weight: 600;
}
.custom-interface-row {
align-items: flex-start;
}
.interface-row {
width: calc(100% - 90px);
margin-bottom: 10px;
}
.nodes-row {
align-items: flex-start;
}
.custom-interface-add {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
:deep(.nodes-row .array-list-select .box) {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
}
:deep(.nodes-row .array-list-select .box .remove) {
align-self: center;
}
.custom-interface-primary-add {
max-width: 100%;
}
.custom-interface-box {
position: relative;
width: 100%;
padding: 20px;
margin-bottom: 5px;
}
:deep(.custom-interface-list .box) {
grid-template-columns: 1fr;
}
.remove {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
padding: 0;
}
</style>

View File

@ -43,7 +43,6 @@ export default {
created() {
const vpc = this.$route.query.vpc || '';
const enableDHCP = this.value?.spec?.enableDHCP || false;
const natOutgoing = this.value?.spec?.natOutgoing || false;
set(this.value.spec, 'enableDHCP', enableDHCP);
set(this.value, 'spec', this.value.spec || {
@ -51,11 +50,10 @@ export default {
protocol: NETWORK_PROTOCOL.IPv4,
provider: '',
vpc,
gateway: '',
gatewayIP: '',
excludeIps: [],
private: false,
enableDHCP,
natOutgoing,
acls: []
});
},
@ -66,7 +64,6 @@ export default {
const hash = {
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
vlans: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN }),
};
await allHash(hash);
@ -132,20 +129,6 @@ export default {
label: n.id,
value: n.id,
}));
},
natOutgoingDisabled() {
// Disable the NAT Outgoing option when the subnet belongs to the ovn-cluster VPC and its name is join or ovn-default.
return this.value?.spec?.vpc === 'ovn-cluster' && ['join', 'ovn-default'].includes(this.value?.metadata?.name);
},
vlanOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
return vlans.map((vlan) => ({
label: vlan.id,
value: vlan.id,
}));
}
},
@ -281,16 +264,6 @@ export default {
:mode="mode"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.vlan"
class="mb-20"
:options="vlanOptions"
:placeholder="t('harvester.subnet.vlan.placeholder')"
:label="t('harvester.subnet.vlan.label')"
:mode="mode"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
@ -331,20 +304,6 @@ export default {
</Banner>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
v-model:value="value.spec.natOutgoing"
name="enableExternalConnectivity"
:disabled="natOutgoingDisabled"
:options="[true, false]"
:label="t('harvester.subnet.externalConnectivity.label')"
:labels="[t('generic.enabled'), t('generic.disabled')]"
:mode="mode"
:tooltip="t('harvester.subnet.externalConnectivity.tooltip')"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup

View File

@ -1,146 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
vlanId: this.value?.spec?.id || '',
provider: this.value?.spec?.provider || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ providerNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PROVIDER_NETWORK }) });
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
providerNetworks() {
const inStore = this.$store.getters['currentProduct'].inStore;
const providerNetworks = this.$store.getters[`${ inStore }/all`](HCI.PROVIDER_NETWORK) || [];
return providerNetworks.map((pn) => ({
label: pn.id,
value: pn.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
if (this.vlanId !== '') {
this.value.spec.id = Number(this.vlanId);
}
this.value.spec.provider = this.provider;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value.number="vlanId"
class="mb-20"
type="number"
:min="1"
:max="4094"
:label="t('harvester.vlan.id.label')"
:placeholder="t('harvester.vlan.id.placeholder')"
:mode="mode"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="provider"
class="mb-20"
:options="providerNetworks"
:mode="mode"
:label="t('harvester.vlan.provider.label')"
:placeholder="t('harvester.vlan.provider.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -1,246 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI as HCI_ANNOTATIONS } from '@pkg/config/labels-annotations';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
ArrayListSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
const internalTenantNetwork = this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.CNI_NETWORKS] || '';
return {
internalTenantNetwork,
vpc: this.value?.spec?.vpc || '',
subnet: this.value?.spec?.subnet || '',
lanIp: this.value?.spec?.lanIp || '',
externalSubnets: this.value?.spec?.externalSubnets || [],
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
vmNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NETWORK_ATTACHMENT }),
});
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
vpcOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
return vpcs.map((vpc) => ({
label: vpc.id,
value: vpc.id,
}));
},
subnetOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
return subnets.map((subnet) => ({
label: subnet.id,
value: subnet.id,
}));
},
vmNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmNetworks = this.$store.getters[`${ inStore }/all`](HCI.NETWORK_ATTACHMENT) || [];
return vmNetworks.map((network) => ({
label: network.id,
value: network.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
if (!this.value.metadata) {
this.value.metadata = {};
}
if (!this.value.metadata.annotations) {
this.value.metadata.annotations = {};
}
this.value.spec.vpc = this.vpc;
this.value.spec.subnet = this.subnet;
this.value.spec.lanIp = this.lanIp;
this.value.spec.externalSubnets = (this.externalSubnets || []).filter((subnet) => !!subnet);
if (this.internalTenantNetwork) {
this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS] = this.internalTenantNetwork;
} else {
delete this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS];
}
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="internalTenantNetwork"
class="mb-20"
required
:options="vmNetworkOptions"
:mode="mode"
:label="t('harvester.natGateway.internalTenantNetwork.label')"
:placeholder="t('harvester.natGateway.internalTenantNetwork.placeholder')"
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="vpc"
class="mb-20"
:options="vpcOptions"
:mode="mode"
:label="t('harvester.natGateway.vpc.label')"
:placeholder="t('harvester.natGateway.vpc.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="subnet"
class="mb-20"
:options="subnetOptions"
:mode="mode"
:label="t('harvester.natGateway.subnet.label')"
:placeholder="t('harvester.natGateway.subnet.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="lanIp"
class="mb-20"
:mode="mode"
:label="t('harvester.natGateway.lanIp.label')"
:placeholder="t('harvester.natGateway.lanIp.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
<Tab
name="externalSubnets"
:label="t('harvester.natGateway.externalSubnets.label')"
:weight="98"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<ArrayListSelect
v-model:value="externalSubnets"
:mode="mode"
:disabled="mode === 'view'"
required
:options="subnetOptions"
:enable-default-add-value="false"
:array-list-props="{
addLabel: t('harvester.natGateway.externalSubnets.addLabel'),
title: t('harvester.natGateway.subnet.label'),
initialEmptyRow: true,
required: true,
protip: false,
}"
:select-props="{
placeholder: t('harvester.natGateway.externalSubnets.placeholder'),
disabled: mode === 'view',
}"
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -143,7 +143,6 @@ export default {
<YamlEditor
ref="yaml"
v-model:value="yamlScript"
:mode="mode"
class="yaml-editor"
:editor-mode="editorMode"
/>

View File

@ -1,310 +0,0 @@
<script>
import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import { CONFIG_MAP, SECRET, SERVICE_ACCOUNT } from '@shell/config/types';
import { _VIEW } from '@shell/config/query-params';
import CopyToClipboard from '@shell/components/CopyToClipboard';
import MessageLink from '@shell/components/MessageLink';
import { FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
const MAX_FILESYSTEMS = 3;
const { CONFIGMAP: FS_TYPE_CONFIGMAP, SECRET: FS_TYPE_SECRET, SERVICEACCOUNT: FS_TYPE_SERVICEACCOUNT } = FILESYSTEM_SOURCE_TYPE;
const DEFAULT_VOLUME_NAMES = {
[FS_TYPE_CONFIGMAP]: 'appconfigfs',
[FS_TYPE_SECRET]: 'appsecretfs',
[FS_TYPE_SERVICEACCOUNT]: 'appserviceaccountfs',
};
const FS_TYPE_OPTIONS = [
{ label: 'ConfigMap', value: FS_TYPE_CONFIGMAP },
{ label: 'Secret', value: FS_TYPE_SECRET },
{ label: 'ServiceAccount', value: FS_TYPE_SERVICEACCOUNT },
];
function emptyRow() {
return {
fsType: FS_TYPE_CONFIGMAP,
volumeName: DEFAULT_VOLUME_NAMES[FS_TYPE_CONFIGMAP],
resourceName: '',
};
}
export default {
name: 'VirtualMachineFilesystem',
emits: ['update:value'],
components: {
Banner,
LabeledSelect,
LabeledInput,
CopyToClipboard,
MessageLink,
},
props: {
mode: {
type: String,
default: 'create',
},
namespace: {
type: String,
default: '',
},
value: {
type: Array,
default: () => [],
},
},
data() {
return { rows: this.value.length > 0 ? this.value.map((r) => ({ ...r })) : [emptyRow()] };
},
watch: {
value(newVal) {
if (newVal) {
const incoming = JSON.stringify(newVal);
const current = JSON.stringify(this.rows);
if (incoming !== current) {
this.rows = newVal.map((r) => ({ ...r }));
}
}
},
rows: {
deep: true,
handler(val) {
this.$emit('update:value', val.map((r) => ({ ...r })));
},
},
},
computed: {
...mapGetters({ t: 'i18n/t' }),
inStore() {
return this.$store.getters['currentProduct'].inStore;
},
configMaps() {
return this.$store.getters[`${ this.inStore }/all`](CONFIG_MAP)
.filter((cm) => !this.namespace || cm.metadata.namespace === this.namespace)
.map((cm) => ({ label: cm.metadata.name, value: cm.metadata.name }));
},
secrets() {
return this.$store.getters[`${ this.inStore }/all`](SECRET)
.filter((s) => !this.namespace || s.metadata.namespace === this.namespace)
.map((s) => ({ label: s.metadata.name, value: s.metadata.name }));
},
serviceAccounts() {
return this.$store.getters[`${ this.inStore }/all`](SERVICE_ACCOUNT)
.filter((sa) => !this.namespace || sa.metadata.namespace === this.namespace)
.map((sa) => ({ label: sa.metadata.name, value: sa.metadata.name }));
},
canAddRow() {
return this.rows.length < MAX_FILESYSTEMS;
},
isView() {
return this.mode === _VIEW;
},
completedRows() {
return this.rows.filter((r) => r.fsType && r.volumeName && r.resourceName);
},
allMountCommands() {
return this.completedRows.map((r) => this.mountCommands(r)).join('\n');
},
},
methods: {
fsTypeOptions(currentIndex) {
const usedTypes = this.rows
.filter((_, i) => i !== currentIndex)
.map((r) => r.fsType);
return FS_TYPE_OPTIONS.filter((opt) => !usedTypes.includes(opt.value));
},
resourceOptions(fsType) {
if (fsType === FS_TYPE_CONFIGMAP) return this.configMaps;
if (fsType === FS_TYPE_SECRET) return this.secrets;
if (fsType === FS_TYPE_SERVICEACCOUNT) return this.serviceAccounts;
return [];
},
onFsTypeChange(row, newType) {
row.fsType = newType;
row.volumeName = DEFAULT_VOLUME_NAMES[newType] || '';
row.resourceName = '';
},
addRow() {
if (this.canAddRow) {
const usedTypes = this.rows.map((r) => r.fsType);
const nextType = FS_TYPE_OPTIONS.find((opt) => !usedTypes.includes(opt.value))?.value || FS_TYPE_CONFIGMAP;
this.rows.push({
fsType: nextType,
volumeName: DEFAULT_VOLUME_NAMES[nextType] || '',
resourceName: '',
});
}
},
removeRow(index) {
this.rows.splice(index, 1);
},
mountCommands(row) {
const vol = row.volumeName || '<volume-name>';
return `- mkdir -p /mnt/${ vol }\n- mount -t virtiofs ${ vol } /mnt/${ vol }`;
},
},
};
</script>
<template>
<div class="vm-filesystem">
<p class="mb-20">
{{ t('harvester.virtualMachine.filesystem.description') }}
</p>
<div
v-for="(row, index) in rows"
:key="index"
class="filesystem-row mb-15"
>
<div class="row">
<div class="col span-3">
<LabeledSelect
:value="row.fsType"
:label="t('harvester.virtualMachine.filesystem.type')"
:options="fsTypeOptions(index)"
:mode="mode"
required
@update:value="onFsTypeChange(row, $event)"
/>
</div>
<div class="col span-3">
<LabeledInput
v-model:value="row.volumeName"
:label="t('harvester.virtualMachine.filesystem.volume')"
:mode="mode"
required
/>
</div>
<div class="col span-5">
<LabeledSelect
v-model:value="row.resourceName"
:label="t('harvester.virtualMachine.filesystem.resource')"
:options="resourceOptions(row.fsType)"
:mode="mode"
required
/>
</div>
<div
v-if="!isView"
class="col span-1 remove-col"
>
<button
type="button"
class="btn role-link remove-btn"
@click="removeRow(index)"
>
{{ t('generic.remove') }}
</button>
</div>
</div>
</div>
<Banner
v-if="completedRows.length > 0 && mode === 'create'"
color="warning"
class="mt-10"
>
<div>
<MessageLink
:to="{ hash: '#advanced' }"
prefix-label="harvester.virtualMachine.filesystem.mountBannerHint"
middle-label="harvester.virtualMachine.filesystem.mountBannerHintLink"
suffix-label="harvester.virtualMachine.filesystem.mountBannerHintSuffix"
/>
<div class="pre-wrapper mt-10">
<pre class="mt-5 mb-0">{{ allMountCommands }}</pre>
<CopyToClipboard
:text="allMountCommands"
:show-label="false"
class="icon-btn"
action-color="bg-transparent"
/>
</div>
</div>
</Banner>
<button
v-if="!isView && canAddRow"
type="button"
class="btn role-tertiary add"
@click="addRow"
>
{{ t('harvester.virtualMachine.filesystem.add') }}
</button>
</div>
</template>
<style lang="scss" scoped>
.vm-filesystem {
padding: 10px 0;
}
.filesystem-row {
border-bottom: 1px solid var(--border);
padding-bottom: 15px;
}
.remove-col {
display: flex;
align-items: center;
justify-content: center;
}
.remove-btn {
padding: 0;
}
.pre-wrapper {
position: relative;
pre {
padding-right: 36px;
}
.icon-btn {
position: absolute;
top: 0px;
right: 5px;
padding: 2px 4px;
line-height: 1;
&:active {
background-color: var(--success) !important;
}
}
}
</style>

View File

@ -152,10 +152,6 @@ export default {
return !rows.find((device) => !device.passthroughClaim);
},
canManageGroup(rows = []) {
return rows.length > 0 && rows.every((row) => row.canUpdate === true);
},
changeRows(filterRows, parentSriov) {
this['filterRows'] = filterRows;
this['parentSriov'] = parentSriov;
@ -188,10 +184,6 @@ export default {
:ref="group.key"
v-trim-whitespace
class="group-tab"
>
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
>
<button
v-if="groupIsAllEnabled(group.rows)"
@ -209,7 +201,6 @@ export default {
>
{{ t('harvester.pci.enableGroup') }}
</button>
</div>
<span v-clean-html="group.key" />
</div>
</template>
@ -241,9 +232,3 @@ export default {
</template>
</ResourceTable>
</template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>

View File

@ -52,21 +52,31 @@ export default {
}
const selectedDevices = [];
const oldFormatDevices = [];
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
const vmDeviceNames = vmDevices.map(({ name }) => name);
this.pciDevices.forEach((row) => {
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
});
vmDevices.forEach(({ name }) => {
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
vmDevices.forEach(({ name, deviceName }) => {
const checkName = (deviceName || '').split('/')?.[1];
if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
oldFormatDevices.push(name);
} else if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
selectedDevices.push(name);
}
});
if (oldFormatDevices.length > 0) {
this.oldFormatDevices = oldFormatDevices;
} else {
this.selectedDevices = selectedDevices;
}
},
data() {
@ -77,6 +87,7 @@ export default {
selectedDevices: [],
pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE),
showMatrix: false,
oldFormatDevices: [],
};
},
@ -188,6 +199,11 @@ export default {
});
},
oldFormatDevicesHTML() {
return this.oldFormatDevices.map((device) => {
return `<li>${ device }</li>`;
}).join('');
},
},
methods: {
@ -211,6 +227,17 @@ export default {
<template>
<div>
<div
v-if="oldFormatDevices.length > 0"
class="row"
>
<div class="col span-12">
<Banner color="warning">
<p v-clean-html="t('harvester.pci.oldFormatDevices.help', {oldFormatDevicesHTML}, true)" />
</Banner>
</div>
</div>
<div v-else>
<div class="row">
<div class="col span-12">
<Banner color="info">
@ -296,4 +323,5 @@ export default {
</div>
</div>
</div>
</div>
</template>

View File

@ -10,7 +10,6 @@ import { _VIEW } from '@shell/config/query-params';
import { NAMESPACE } from '@shell/config/types';
import { HCI } from '../../types';
import { getLoginAwareErrors } from '../../utils/error';
const _NEW = '_NEW';
@ -215,9 +214,7 @@ export default {
buttonCb(true);
this.cancel();
} catch (err) {
const message = this.t('harvester.virtualMachine.genericLoginError');
this.errors = getLoginAwareErrors(err, message);
this.errors = [err.message];
buttonCb(false);
}
},

View File

@ -64,12 +64,6 @@ export default {
value: 'status.productID',
sort: ['status.productID', 'status.vendorID']
},
{
name: 'classType',
labelKey: 'harvester.usb.classType',
value: 'status.classType',
sort: ['status.classType']
},
];
if (!isSingleProduct) {
@ -113,11 +107,6 @@ export default {
}
});
},
canManageGroup(rows = []) {
return rows.every((row) => row.canUpdate === true);
},
groupIsAllEnabled(rows = []) {
return !rows.find((device) => !device.passthroughClaim);
},
@ -157,10 +146,6 @@ export default {
:ref="group.key"
v-trim-whitespace
class="group-tab"
>
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
>
<button
v-if="groupIsAllEnabled(group.rows)"
@ -178,7 +163,6 @@ export default {
>
{{ t('harvester.usb.enableGroup') }}
</button>
</div>
<span v-clean-html="group.key" />
</div>
</template>
@ -191,9 +175,3 @@ export default {
</template>
</ResourceTable>
</template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>

View File

@ -475,6 +475,7 @@ export default {
<button
type="button"
class="btn btn-sm bg-primary mr-15 mb-10"
:disabled="rows.length === 0"
@click="addVolume(SOURCE_TYPE.NEW)"
>
{{ t('harvester.virtualMachine.volume.addVolume') }}

View File

@ -21,7 +21,9 @@ import { saferDump } from '@shell/utils/create-yaml';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
import CreateEditView from '@shell/mixins/create-edit-view';
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
import VM_MIXIN from '../../mixins/harvester-vm';
import { HCI } from '../../types';
@ -35,7 +37,6 @@ import Network from './VirtualMachineNetwork';
import Volume from './VirtualMachineVolume';
import SSHKey from './VirtualMachineSSHKey';
import Reserved from './VirtualMachineReserved';
import Filesystem from './VirtualMachineFilesystem';
import { Banner } from '@components/Banner';
import MessageLink from '@shell/components/MessageLink';
@ -71,7 +72,6 @@ export default {
Banner,
MessageLink,
UsbDevices,
Filesystem,
},
mixins: [CreateEditView, VM_MIXIN],
@ -91,8 +91,6 @@ export default {
const hostname = this.value.spec.template.spec.hostname || '';
const customizeDisplayName = !!(this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME]);
return {
cloneVM,
count: 2,
@ -103,24 +101,12 @@ export default {
isOpen: false,
hostname,
isRestartImmediately,
customizeDisplayName,
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
// VM display name is stored as an annotation; bind a dedicated input to it
// so we don't have to mutate metadata.name (which would break the k8s PUT).
displayName: {
get() {
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || '';
},
set(val) {
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, val);
},
},
to() {
return {
name: 'harvester-c-cluster-resource',
@ -232,9 +218,6 @@ export default {
usbPassthroughEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
},
filesystemEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
},
},
watch: {
@ -306,12 +289,6 @@ export default {
this.getInitConfig({ value: this.value, init: this.isCreate });
}
},
customizeDisplayName(neu) {
if (!neu) {
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, '');
}
},
},
created() {
@ -344,7 +321,6 @@ export default {
const diskRows = this.getDiskRows(this.value);
this['diskRows'] = diskRows;
this['filesystemRows'] = this.getFilesystemRows(this.value);
const templateId = this.$route.query.templateId;
const templateVersionId = this.$route.query.versionId;
@ -634,33 +610,6 @@ export default {
</template>
</NameNsDescription>
<div v-if="isSingle">
<div class="row mb-20">
<div class="col span-12">
<Checkbox
v-model:value="customizeDisplayName"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.input.customizeDisplayName')"
:mode="mode"
/>
</div>
</div>
<div
v-if="customizeDisplayName"
class="row mb-20"
>
<div class="col span-6">
<LabeledInput
v-model:value="displayName"
:mode="mode"
:label="t('harvester.virtualMachine.input.displayName')"
:placeholder="t('harvester.virtualMachine.input.displayNamePlaceholder')"
/>
</div>
</div>
</div>
<Checkbox
v-if="isCreate"
v-model:value="useTemplate"
@ -834,23 +783,10 @@ export default {
/>
</Tab>
<Tab
v-if="filesystemEnabled"
name="filesystem"
:label="t('harvester.tab.filesystem')"
:weight="-9"
>
<Filesystem
v-model:value="filesystemRows"
:mode="isCreate ? mode : 'view'"
:namespace="value.metadata.namespace"
/>
</Tab>
<Tab
name="labels"
:label="t('generic.labels')"
:weight="-10"
:weight="-9"
>
<Banner color="info">
<t k="harvester.virtualMachine.labels.banner" />
@ -869,7 +805,7 @@ export default {
<Tab
name="instanceLabel"
:label="t('harvester.tab.instanceLabel')"
:weight="-11"
:weight="-10"
>
<Banner color="info">
<t k="harvester.virtualMachine.instanceLabels.banner" />
@ -890,7 +826,7 @@ export default {
<Tab
name="annotations"
:label="t('harvester.tab.annotations')"
:weight="-12"
:weight="-11"
>
<Banner color="info">
<t k="harvester.virtualMachine.annotations.banner" />
@ -911,7 +847,7 @@ export default {
<Tab
name="advanced"
:label="t('harvester.tab.advanced')"
:weight="-13"
:weight="-12"
>
<div class="row mb-20">
<div class="col span-6">

View File

@ -1,419 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import InfoBox from '@shell/components/InfoBox';
import MessageLink from '@shell/components/MessageLink';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
import { Checkbox } from '@components/Form/Checkbox';
import HarvesterNodeSelector from '../components/HarvesterNodeSelector';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { set } from '@shell/utils/object';
import { NODE } from '@shell/config/types';
import { HCI } from '../types';
import { ADD_ONS } from '../config/harvester-map';
const MODE_DHCP = 'dhcp';
const MODE_STATIC = 'static';
export default {
name: 'HarvesterHostNetworkConfigEditPage',
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
InfoBox,
MessageLink,
LabeledSelect,
LabeledInput,
RadioGroup,
Checkbox,
HarvesterNodeSelector,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
hostNetworkConfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
});
},
data() {
const networkMode = this.value?.spec?.mode || MODE_DHCP;
const ips = { ...(this.value?.spec?.ips || {}) };
if (!this.value.spec) {
set(this.value, 'spec', {});
}
return {
networkMode,
ips,
hasNodeSelector: !!this.value?.spec?.nodeSelector,
};
},
computed: {
modeOptions() {
return [
{ label: 'DHCP', value: MODE_DHCP },
{ label: 'Static', value: MODE_STATIC },
];
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
return clusterNetworks.map((n) => {
const disabled = !n.isReady;
return {
label: disabled ? `${ n.id } (${ this.t('generic.notReady') })` : n.id,
value: n.id,
disabled,
};
});
},
nodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](NODE) || [];
},
isStaticMode() {
return this.networkMode === MODE_STATIC;
},
underlay: {
get() {
return !!this.value?.spec?.underlay;
},
set(val) {
set(this.value, 'spec.underlay', val);
},
},
vlanID: {
get() {
return this.value?.spec?.vlanID;
},
set(val) {
set(this.value, 'spec.vlanID', val);
},
},
clusterNetwork: {
get() {
return this.value?.spec?.clusterNetwork;
},
set(val) {
set(this.value, 'spec.clusterNetwork', val);
},
},
underlayConflict() {
const inStore = this.$store.getters['currentProduct'].inStore;
const all = this.$store.getters[`${ inStore }/all`](HCI.HOST_NETWORK_CONFIG) || [];
const currentId = this.value?.id;
return all.find((c) => c.id !== currentId && c.spec?.underlay === true) || null;
},
kubeovnEnabled() {
const inStore = this.$store.getters['currentProduct'].inStore;
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS) || [];
return addons.find((a) => a.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
},
underlayDisabled() {
return !this.kubeovnEnabled || !!this.underlayConflict;
},
kubeovnAddonTo() {
return {
name: 'c-cluster-product-resource-namespace-id',
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
resource: HCI.ADD_ONS,
namespace: 'kube-system',
id: ADD_ONS.KUBEOVN_OPERATOR,
},
query: { mode: 'edit' },
hash: '#basic',
};
},
},
watch: {
networkMode(neu) {
set(this.value, 'spec.mode', neu);
if (neu !== MODE_STATIC) {
if (this.value?.spec?.ips !== undefined) {
delete this.value.spec.ips;
}
this.ips = {};
}
},
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
methods: {
updateBeforeSave() {
set(this.value, 'spec.mode', this.networkMode);
if (this.isStaticMode) {
set(this.value, 'spec.ips', { ...this.ips });
}
},
updateIp(nodeName, val) {
this.ips = { ...this.ips, [nodeName]: val };
},
addNodeSelector() {
set(this.value.spec, 'nodeSelector', {
matchExpressions: [{
key: '', operator: 'In', values: []
}]
});
this.hasNodeSelector = true;
},
removeNodeSelector() {
delete this.value.spec.nodeSelector;
this.hasNodeSelector = false;
},
},
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:mode="mode"
:resource="value"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@cancel="done"
@error="e => errors = e"
>
<NameNsDescription
:value="value"
:mode="mode"
:namespaced="false"
description-key="spec.description"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('harvester.hostNetworkConfig.tabs.mode')"
:weight="99"
class="bordered-table"
>
<div class="row mb-20">
<div class="col span-6">
<RadioGroup
v-model:value="networkMode"
name="hostNetworkConfigMode"
:options="modeOptions"
:mode="mode"
:row="true"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="clusterNetwork"
:label="t('harvester.network.clusterNetwork.label')"
:options="clusterNetworkOptions"
:mode="mode"
required
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value.number="vlanID"
type="number"
required
:min="2"
:max="4094"
placeholder="e.g. 2 ~ 4094"
:label="t('harvester.hostNetworkConfig.vlanID.label')"
:mode="mode"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<Checkbox
v-model:value="underlay"
:label="t('harvester.hostNetworkConfig.underlay.label')"
:tooltip="t('harvester.hostNetworkConfig.underlay.tooltip')"
:mode="mode"
:disabled="underlayDisabled"
/>
<span
v-if="!kubeovnEnabled"
class="underlay-conflict-warning"
>
<i class="icon icon-warning" />
<MessageLink
:to="kubeovnAddonTo"
prefix-label="harvester.hostNetworkConfig.underlay.noKubeovn.prefix"
middle-label="harvester.hostNetworkConfig.underlay.noKubeovn.middle"
suffix-label="harvester.hostNetworkConfig.underlay.noKubeovn.suffix"
/>
</span>
<span
v-else-if="underlayConflict"
class="underlay-conflict-warning"
>
<i class="icon icon-warning" />
{{ t('harvester.hostNetworkConfig.underlay.conflict', { name: underlayConflict.nameDisplay || underlayConflict.id }) }}
</span>
</div>
</div>
<template v-if="isStaticMode">
<hr class="section-divider" />
<div
v-for="node in nodes"
:key="node.id"
class="row mb-10 ips-row"
>
<div class="col span-3">
<LabeledInput
:value="node.nameDisplay || node.id"
:label="t('harvester.hostNetworkConfig.ips.nodeLabel')"
mode="view"
:disabled="true"
/>
</div>
<div class="col span-5">
<LabeledInput
:value="ips[node.id]"
:label="t('harvester.hostNetworkConfig.ips.label')"
:placeholder="t('harvester.hostNetworkConfig.ips.placeholder')"
:mode="mode"
required
@update:value="updateIp(node.id, $event)"
/>
</div>
</div>
</template>
</Tab>
<Tab
name="nodeSelector"
:label="t('harvester.hostNetworkConfig.tabs.nodeSelector')"
:weight="98"
>
<template v-if="hasNodeSelector">
<InfoBox class="node-selector-box">
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
:aria-label="t('generic.remove')"
@click="removeNodeSelector"
>
<i class="icon icon-x" />
</button>
<HarvesterNodeSelector
class="mt-20"
:value="value.spec.nodeSelector"
:mode="mode"
/>
</InfoBox>
</template>
<template v-else>
<button
type="button"
class="btn role-secondary"
:disabled="isView"
@click="addNodeSelector"
>
{{ t('harvester.hostNetworkConfig.nodeSelector.addButton') }}
</button>
</template>
</Tab>
</ResourceTabs>
</CruResource>
</template>
<style lang="scss" scoped>
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 10px 0 20px;
}
.node-selector-box {
position: relative;
.remove {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
padding: 0;
}
}
.underlay-conflict-warning {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 4px;
color: var(--warning);
font-size: 13px;
}
</style>

View File

@ -31,10 +31,6 @@ export default {
to() {
return this.vm?.detailLocation;
},
attachVMName() {
return this.vm?.nameDisplay || this.vm?.metadata?.name || this.value;
}
}
};
</script>
@ -44,10 +40,10 @@ export default {
v-if="to"
:to="to"
>
{{ attachVMName }}
{{ value }}
</router-link>
<span v-else>
{{ attachVMName }}
{{ value }}
</span>
</template>

View File

@ -1,23 +0,0 @@
<script>
export default {
name: 'HarvesterBooleanFormatter',
props: {
value: {
type: Boolean,
default: false,
},
},
};
</script>
<template>
<span v-if="value">
<i class="icon icon-checkmark" />
</span>
<span
v-else
class="text-muted"
>
&mdash;
</span>
</template>

View File

@ -1,29 +0,0 @@
<script>
export default {
name: 'HarvesterHostNetworkConfigModeFormatter',
props: {
value: {
type: String,
default: '',
},
},
computed: {
displayMode() {
if (this.value?.toLowerCase() === 'dhcp') {
return 'DHCP';
}
if (this.value?.toLowerCase() === 'static') {
return 'Static';
}
return this.value;
},
},
};
</script>
<template>
<span>{{ displayMode }}</span>
</template>

View File

@ -20,7 +20,12 @@ export default {
computed: {
vmiResource() {
return this.$store.getters['harvester/byId'](HCI.VMI, this.vmResource?.id) || null;
const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || [];
const vmi = vmiList.find( (VMI) => {
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.vmResource?.metadata?.uid;
});
return vmi;
},
migrationState() {
return this.vmiResource?.migrationState?.status || '';

View File

@ -1,24 +0,0 @@
<script>
export default {
name: 'HarvesterVlanFormatter',
props: {
value: {
type: String,
default: '',
},
},
};
</script>
<template>
<span v-if="value">
{{ value }}
</span>
<span
v-else
class="text-muted"
>
&mdash;
</span>
</template>

View File

@ -14,6 +14,20 @@ export default {
type: Object,
required: true
},
allNodeNetwork: {
type: Array,
default: () => {
return [];
}
},
allClusterNetwork: {
type: Array,
default: () => {
return [];
}
}
},
data() {

View File

@ -144,10 +144,6 @@ harvester:
migration:
failedMessage: Latest migration failed!
title: Migration
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
unknownNode: (unknown node)
alreadyOnTarget: Already on Target
fields:
nodeName:
label: Target Node
@ -252,7 +248,6 @@ harvester:
suspendSchedule: Suspend
restoreExistingVM: Replace Existing
migrate: Migrate
vmMigrate: Virtual Machine Migration
cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration
storageMigration: Storage Migration
@ -295,12 +290,6 @@ harvester:
harvesterIpAddress:
customIpTooltip: "Custom IP (set via annotation)"
tableHeaders:
hostNetworkConfig:
underlay: Underlay
underlayTooltip: Allow this interface to act as the underlay for VM overlay networks.
vlanID: VLAN ID
mode: Mode
clusterNetwork: Cluster Network
imageEncryption: Encryption
size: Size
virtualSize: Virtual Size
@ -309,7 +298,6 @@ harvester:
phase: Phase
attachedVM: Attached Virtual Machine
cpuManager: CPU Manager
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
fingerprint: Fingerprint
value: Value
actions: Actions
@ -348,9 +336,6 @@ harvester:
vmImportSourceOClusterStatus: Cluster Status
vmImportSourceOVAUrl: URL
vmImportSourceOVAStatus: Status
v4ip: V4 IP
v6ip: V6 IP
eipName: EIP Name
tab:
volume: Volumes
network: Networks
@ -364,7 +349,6 @@ harvester:
snapshots: Snapshots
instanceLabel: Instance Labels
annotations: Annotations
filesystem: Filesystem Volume
fields:
version: Version
name: Name
@ -401,6 +385,21 @@ harvester:
middle: vGPU Devices
suffix: page.
deviceInTheSameHost: 'You can only select devices on the same host.'
oldFormatDevices:
help: |-
<p>
The following PCI devices are using the old naming convention and need to be updated in the YAML file:
</p>
<ul>
{oldFormatDevicesHTML}
</ul>
<p>
Please use the following instructions to update the virtual machine:
</p>
<ol>
<li>Stop the virtual machine, edit the virtual machine YAML, and remove the <Code>hostDevices</Code> section, and save virtual machine the changes to the YAML file.</li>
<li>Edit the virtual machine, and add the already enabled PCI Device from the list of available PCIDevices, and save and start VM.</li>
</ol>
showCompatibility: Show device compatibility matrix
hideCompatibility: Hide device compatibility matrix
claimError: Error enabling passthrough on {name}
@ -418,8 +417,6 @@ harvester:
suffix: to enable the add-on to successfully manage your PCI devices.
noPCIPermission: Please contact your system administrator to enable the PCI devices first.
enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure.
disableResourcePooling: Disable Resource Pooling
disableResourcePoolingDescription: Assigns this device a unique resource name so it can be pinned to a specific VM, instead of sharing the pool with other identical devices.
devices:
matrixHostName: Host Name
@ -729,7 +726,6 @@ harvester:
other {Start}
} Now
createSSHKey: Create a New...
genericLoginError: Authentication failed. Please re-log in and try again.
installAgent: Install guest agent
enableUsb: Enable USB Tablet
advancedOptions:
@ -784,7 +780,7 @@ harvester:
addPort: Add Port
cloudConfig:
title: Cloud Configuration
createTemplateTitle: 'Create {name}'
createTemplateTitle: 'Create {name}.'
createNew: Create new...
cloudInit:
label: Cloud Init
@ -827,18 +823,6 @@ harvester:
username: Username
password: Password
reservedMemory: Reserved Memory
customizeDisplayName: Customize virtual machine display name
displayNamePlaceholder: Virtual machine alias name
displayName: Display Name
filesystem:
description: Harvester supports filesystem volumes for VM via virtiofs.
type: Filesystem Type
volume: Volume
resource: Resource
add: Add
mountBannerHint: "Please update the mount path (e.g. /mnt/appconfigfs) to your preferred location, then add the corresponding commands to the"
mountBannerHintLink: "runcmd"
mountBannerHintSuffix: "in Advanced tab User Data."
machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?'
detail:
tabs:
@ -1127,7 +1111,6 @@ harvester:
replaceExisting: Replace existing
virtualMachineName: Virtual Machine Name
keepMacAddress: Keep MAC Address
haltAfterRestore: Keep powered off after restore
matchTarget: The current backup target does not match the existing one.
progress:
details: Volume details
@ -1158,9 +1141,6 @@ harvester:
gateway:
label: Gateway IP
placeholder: e.g. 172.20.0.1
vlan:
label: VLAN
placeholder: Select a VLAN
dhcp:
label: Dynamic Host Configuration Protocol (DHCP)
v4Options: DHCPV4Options
@ -1168,9 +1148,6 @@ harvester:
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.
externalConnectivity:
label: NAT Outgoing
tooltip: Enable NAT for VMs using this subnet to access external networks.
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.
@ -1246,97 +1223,14 @@ harvester:
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.
natGateway:
label: Gateways
internalTenantNetwork:
label: Internal Tenant Network
placeholder: Select a Virtual Machine Network
vpc:
label: VPC
placeholder: Select a VPC
subnet:
label: Subnet
placeholder: Select a subnet
lanIp:
label: LAN IP
placeholder: Enter LAN IP
externalSubnets:
label: External Subnets
addLabel: Add
placeholder: Select a subnet
externalIP:
label: External IPs
natGateway:
label: VpcNatGateway
placeholder: Select a VpcNatGateway
externalSubnet:
label: External Subnet
placeholder: Select an external subnet
v4ip:
label: V4 IP
placeholder: public ip from external subnet
snat:
label: Source Rules
eip:
label: EIP
placeholder: Select an external IP
internalCIDR:
label: Internal CIDR
placeholder: internal subnet CIDR
dnat:
label: Destination Rules
eip:
label: EIP
placeholder: Select an external IP
externalPort:
label: External Port
placeholder: port number
internalIp:
label: Internal IP
placeholder: internal IP address
internalPort:
label: Internal Port
placeholder: port number
protocol:
label: Protocol
placeholder: Select protocol (tcp or udp)
providerNetwork:
label: Provider Networks
defaultInterface:
label: Default Interface
placeholder: Select the interface the same as master interface of external overlay network
customInterfaces:
label: Custom Interfaces
addLabel: Add Custom Interface
interface:
label: Network Interface
placeholder: e.g. eth2
nodes:
label: Nodes
addLabel: Add Node
placeholder: Select a node
excludedNodes:
label: Excluded Nodes
addLabel: Add Excluded Node
placeholder: Select node to exclude from this provider network
vlanNetwork:
label: VLANs
vlan:
id:
label: VLAN ID
placeholder: "e.g. 1-4094"
provider:
label: Provider Network
placeholder: Select a provider network
networkPolicy:
label: Policies
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:
basics: Basics
layer3Network: Route
nic: Network Interface Card
clusterNetwork:
label: Cluster Network
create: Create a new cluster network
@ -1391,13 +1285,6 @@ harvester:
rancherCluster:
kubeConfig: Rancher KubeConfig
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
clusterPodSecurityStandard:
whitelistedNamespaces:
label: 'Whitelisted Namespaces'
privilegedNamespaces:
label: 'Privileged Namespaces'
restrictedNamespaces:
label: 'Restricted Namespaces'
storageNetwork:
range:
placeholder: e.g. 172.16.0.0/24
@ -1405,7 +1292,6 @@ harvester:
invalid: '"Range" is invalid.'
clusterNetwork: Cluster Network
vlan: VLAN ID
exclusiveVlan: Exclusive VLAN
exclude:
label: Exclude IPs
placeholder: CIDR format, e.g. 172.16.0.10/32
@ -1622,7 +1508,6 @@ harvester:
vmSnapshot:
label: Virtual Machine Snapshots
createText: Restore Snapshot
title: Restore Virtual Machine
snapshot: Snapshot
storage:
@ -1630,7 +1515,6 @@ harvester:
useDefault: Use the default storage
volumeEncryption: Volume Encryption
secret: Secret
volumeExpansionCheckbox: Enable Expansion
migratable:
label: Migratable
numberOfReplicas:
@ -1723,9 +1607,8 @@ harvester:
schedulingRules: Select node(s) matching rules
uplink:
nics:
label: NIC
label: NICs
addLabel: Add NIC
overlayWarning: The NIC selected here must match the NIC provided in the provider network.
placeholder: Select a NIC that is available on all the selected nodes
validate:
available: NIC "{nic}" is not available on the selected nodes
@ -1881,8 +1764,6 @@ harvester:
repository: Image Repository
driver:
location: Driver Location
enable:
title: Enable NVIDIA Driver Toolkit
parsingSpecError:
The field 'spec.valuesContent' has invalid format.
usbController:
@ -1967,32 +1848,6 @@ harvester:
addLabel: Add CIDR
range:
addLabel: Add Range
hostNetworkConfig:
label: Host Networks
mode:
label: Mode
tabs:
mode: Mode
nodeSelector: Node Selector
nodeSelector:
addButton: Add Node Selector
underlay:
label: Underlay
tooltip: Allow this interface to act as the underlay for VM overlay networks.
conflict: '`{name}` host network config already has underlay enabled. Only one underlay is allowed in the cluster.'
noKubeovn:
prefix: The kubeovn-operator add-on is not enabled. Click
middle: here
suffix: to enable the add-on for overlay networking.
vlanID:
label: VLAN ID
ipRange:
label: IP Range ({node})
placeholder: e.g. 192.168.1.10/24
ips:
nodeLabel: Node
label: IP
placeholder: 'e.g. 192.168.1.10/24'
service:
healthCheckPort:
@ -2038,8 +1893,6 @@ harvester:
migconfiguration:
label: vGPU MIG Configurations
status:
outOfSync: Out of Sync
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
profileSpec: Profile Specs
profileStatus: Profile Status
@ -2102,7 +1955,6 @@ harvester:
title: Cannot Disable Passthrough
message: Please detach the device from the VM and save it first before disabling passthrough.
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/>&nbsp1) Re-plugging the USB device.<br/>&nbsp2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
classType: Class Type
harvesterVlanConfigMigrateDialog:
targetClusterNetwork:
@ -2180,8 +2032,6 @@ advancedSettings:
'harv-auto-rotate-rke2-certs': The certificate rotation mechanism relies on Rancher. Harvester will automatically update certificates generation to trigger rotation.
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
'harv-longhorn-v2-data-engine-hugepage-enabled': 'Enable hugepages when using the Longhorn V2 data engine. Default is true. Disabling hugepages reduces memory pressure on low-spec nodes and increases deployment flexibility. However, performance may be lower compared to running with hugepages.'
'harv-longhorn-v2-data-engine-memory-size': 'Configure the amount of memory allocated to the SPDK target daemon when using the Longhorn V2 data engine. Default is 2048 MiB.'
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
@ -2189,7 +2039,6 @@ advancedSettings:
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
typeLabel:
kubevirt.io.virtualmachine: |-
@ -2252,36 +2101,6 @@ typeLabel:
one { Virtual Private Cloud }
other { Virtual Private Clouds }
}
kubeovn.io.vlan: |-
{count, plural,
one { VLAN Network }
other { VLAN Networks }
}
kubeovn.io.providernetwork: |-
{count, plural,
one { Provider Network }
other { Provider Networks }
}
kubeovn.io.vpcnatgateway: |-
{count, plural,
one { NAT Gateway }
other { NAT Gateways }
}
kubeovn.io.iptablessnatrule: |-
{count, plural,
one { Source Rule }
other { Source Rules }
}
kubeovn.io.iptablesdnatrule: |-
{count, plural,
one { Destination Rule }
other { Destination Rules }
}
kubeovn.io.iptableseip: |-
{count, plural,
one { External IP }
other { External IPs }
}
networking.k8s.io.networkpolicy: |-
{count, plural,
one { Network Policy }
@ -2342,17 +2161,16 @@ typeLabel:
one { PCI Device }
other { PCI Devices }
}
persistentvolumeclaim: |-
{count, plural,
one { Volume }
other { Volumes }
}
network.harvesterhci.io.clusternetwork: |-
{count, plural,
one { Cluster Network }
other { Cluster Networks }
}
network.harvesterhci.io.hostnetworkconfig: |-
{count, plural,
one { Host Network }
other { Host Networks }
}
harvesterhci.io.addon: |-
{count, plural,
one { Add-on }

View File

@ -3,6 +3,7 @@ import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
@ -112,7 +113,6 @@ export default {
value: 'connectivity',
labelKey: 'tableHeaders.routeConnectivity',
formatter: 'NetworkRouteConnectivity',
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
formatterOpts: { arbitrary: true },
width: 130,
},
@ -166,7 +166,6 @@ export default {
:schema="schema"
:groupable="true"
:rows="filterRows"
:ignore-filter="true"
key-field="_key"
>
<template #cell:state="{row}">

View File

@ -106,8 +106,8 @@ export default {
name: 'AttachedVM',
labelKey: 'tableHeaders.attachedVM',
type: 'attached',
value: 'attachVMName',
sort: 'attachVMName',
value: 'spec.claimRef',
sort: 'name',
},
{
name: 'VolumeSnapshotCounts',
@ -134,8 +134,8 @@ export default {
return row?.attachVM?.detailLocation;
},
getAttachedVMName(row) {
return row.attachVMName || '';
getVMName(row) {
return row.attachVM?.metadata?.name || '';
},
isInternalStorageClass(storageClassName) {
@ -173,10 +173,10 @@ export default {
<template #cell:AttachedVM="{row}">
<div>
<router-link
v-if="getAttachedVMName(row)"
v-if="getVMName(row)"
:to="goTo(row)"
>
{{ getAttachedVMName(row) }}
{{ getVMName(row) }}
</router-link>
</div>
</template>

View File

@ -67,13 +67,6 @@ export default {
NAMESPACE,
CIDR_BLOCK,
PROTOCOL,
{
name: 'vlan',
labelKey: 'harvester.subnet.vlan.label',
value: 'spec.vlan',
sort: 'spec.vlan',
formatter: 'HarvesterVlan',
},
PROVIDER,
AGE
];

View File

@ -1,5 +1,4 @@
<script>
import { mapGetters } from 'vuex';
import ResourceTable from '@shell/components/ResourceTable';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import {
@ -13,18 +12,11 @@ import { HCI } from '../types';
import HarvesterVmState from '../formatters/HarvesterVmState';
import ConsoleBar from '../components/VMConsoleBar';
const ENCRYPTED_VOLUME_TOOLTIP_KEYS = {
all: 'harvester.virtualMachine.volume.lockTooltip.all',
partial: 'harvester.virtualMachine.volume.lockTooltip.partial',
};
export const VM_HEADERS = [
STATE,
{
...NAME,
width: 350,
value: 'nameDisplay',
sort: ['nameDisplay'],
},
NAMESPACE,
{
@ -101,9 +93,19 @@ export default {
this.hasNode = true;
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK)) {
_hash.nodeNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NODE_NETWORK });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.CLUSTER_NETWORK)) {
_hash.clusterNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK });
}
const hash = await allHash(_hash);
this.allVMs = hash.vms;
this.allNodeNetworks = hash.nodeNetworks || [];
this.allClusterNetworks = hash.clusterNetworks || [];
},
data() {
@ -111,14 +113,14 @@ export default {
hasNode: false,
allVMs: [],
allVMIs: [],
allNodeNetworks: [],
allClusterNetworks: [],
restartNotificationDisplayed: false,
HCI
};
},
computed: {
...mapGetters({ actionCb: 'action-menu/performCallbackData' }),
headers() {
const restoreCol = {
name: 'restoreProgress',
@ -161,12 +163,6 @@ export default {
*/
hasBackUpRestoreInProgress() {
return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
},
vmRestartRequiredNames() {
return this.allVMs
.filter((vm) => vm.isRestartRequired)
.map((vm) => vm.metadata.name);
}
},
@ -185,17 +181,18 @@ export default {
},
watch: {
actionCb(neu) {
if (neu?.clearTableSelection) {
this.$refs.resourceTable.clearSelection();
this.$store.dispatch('action-menu/clearCallbackData');
}
},
allVMs: {
handler(neu) {
const vmNames = [];
vmRestartRequiredNames(vmNames) {
neu.forEach((vm) => {
if (vm.isRestartRequired) {
vmNames.push(vm.metadata.name);
}
});
const count = vmNames.length;
if (count === 0 && this.restartNotificationDisplayed) {
if ( count === 0 && this.restartNotificationDisplayed) {
this.restartNotificationDisplayed = false;
return;
@ -206,7 +203,9 @@ export default {
if (this.restartNotificationDisplayed) {
this.$store.dispatch('growl/clear');
}
}
if (count > 0 && vmNames.length > 0) {
this.$store.dispatch('growl/warning', {
title: this.t('harvester.notification.restartRequired.title', { count }),
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
@ -214,13 +213,21 @@ export default {
}, { root: true });
this.restartNotificationDisplayed = true;
}
},
deep: true,
}
},
methods: {
lockIconTooltipMessage(row) {
const key = ENCRYPTED_VOLUME_TOOLTIP_KEYS[row.encryptedVolumeType];
const message = '';
return key ? this.t(key) : '';
if (row.encryptedVolumeType === 'all') {
return this.t('harvester.virtualMachine.volume.lockTooltip.all');
} else if (row.encryptedVolumeType === 'partial') {
return this.t('harvester.virtualMachine.volume.lockTooltip.partial');
}
return message;
}
}
};
@ -230,7 +237,6 @@ export default {
<Loading v-if="$fetchState.pending" />
<div v-else>
<ResourceTable
ref="resourceTable"
v-bind="$attrs"
:headers="headers"
default-sort-by="age"
@ -247,6 +253,8 @@ export default {
<HarvesterVmState
class="vmstate"
:row="scope.row"
:all-node-network="allNodeNetworks"
:all-cluster-network="allClusterNetworks"
/>
</div>
</template>
@ -257,16 +265,16 @@ export default {
v-if="scope.row.type !== HCI.VMI"
:to="scope.row.detailLocation"
>
{{ scope.row.nameDisplay }}
{{ scope.row.metadata.name }}
<i
v-if="scope.row.encryptedVolumeType !== 'none'"
v-if="lockIconTooltipMessage(scope.row)"
v-tooltip="lockIconTooltipMessage(scope.row)"
class="icon icon-lock"
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
/>
</router-link>
<span v-else>
{{ scope.row.nameDisplay }}
{{ scope.row.metadata.name }}
</span>
<ConsoleBar
:resource-type="scope.row"

View File

@ -1,86 +0,0 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers';
import { HCI } from '../types';
const UNDERLAY = {
name: 'underlay',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.underlay',
tooltip: 'harvester.tableHeaders.hostNetworkConfig.underlayTooltip',
value: 'spec.underlay',
sort: 'spec.underlay',
formatter: 'HarvesterBoolean',
};
const VLAN_ID = {
name: 'vlanID',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.vlanID',
value: 'spec.vlanID',
sort: 'spec.vlanID',
};
const MODE = {
name: 'mode',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.mode',
value: 'spec.mode',
sort: 'spec.mode',
formatter: 'HarvesterHostNetworkConfigMode',
};
const CLUSTER_NETWORK = {
name: 'clusterNetwork',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.clusterNetwork',
value: 'spec.clusterNetwork',
sort: 'spec.clusterNetwork',
align: 'center',
};
export default {
name: 'HarvesterListHostNetworkConfig',
components: { ResourceTable, Loading },
inheritAttrs: false,
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG });
},
data() {
return { rows: [] };
},
computed: {
schema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/schemaFor`](HCI.HOST_NETWORK_CONFIG);
},
headers() {
return [
STATE,
NAME_COL,
UNDERLAY,
VLAN_ID,
MODE,
CLUSTER_NETWORK,
AGE,
];
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:headers="headers"
:rows="rows"
:schema="schema"
key-field="_key"
/>
</template>

View File

@ -12,7 +12,7 @@ import { base64Decode } from '@shell/utils/crypto';
import { formatSi, parseSi } from '@shell/utils/units';
import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params';
import {
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, SERVICE_ACCOUNT, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
} from '@shell/config/types';
import { HOSTNAME } from '@shell/config/labels-annotations';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
@ -25,7 +25,7 @@ import { HCI } from '../../types';
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
import impl, { QGA_JSON, USB_TABLET } from './impl';
import { GIBIBYTE } from '../../utils/unit';
import { VOLUME_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
@ -102,8 +102,6 @@ export default {
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
configMaps: this.$store.dispatch(`${ inStore }/findAll`, { type: CONFIG_MAP }),
serviceAccounts: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE_ACCOUNT }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
};
@ -158,7 +156,6 @@ export default {
imageId: '',
diskRows: [],
networkRows: [],
filesystemRows: [],
machineType: '',
machineTypes: [],
secretName: '',
@ -443,7 +440,6 @@ export default {
this['imageId'] = imageId;
this['diskRows'] = diskRows;
this['filesystemRows'] = this.getFilesystemRows(vm);
this.refreshYamlEditor();
},
@ -645,80 +641,6 @@ export default {
this.parseAccessCredentials();
this.parseNetworkRows(this.networkRows);
this.parseDiskRows(this.diskRows);
this.parseFilesystemRows();
},
getFilesystemRows(vm) {
const _filesystems = vm.spec.template.spec.domain.devices?.filesystems || [];
const _volumes = vm.spec.template.spec.volumes || [];
return _filesystems.map((fs) => {
const volume = _volumes.find((v) => v.name === fs.name);
let fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
let resourceName = '';
if (volume?.configMap) {
fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
resourceName = volume.configMap.name;
} else if (volume?.secret) {
fsType = FILESYSTEM_SOURCE_TYPE.SECRET;
resourceName = volume.secret.secretName;
} else if (volume?.serviceAccount) {
fsType = FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT;
resourceName = volume.serviceAccount.serviceAccountName;
}
return {
fsType,
volumeName: fs.name,
resourceName,
};
});
},
parseFilesystemRows() {
const completedRows = this.filesystemRows.filter(
(r) => r.fsType && r.volumeName && r.resourceName
);
const filesystems = completedRows.map((r) => ({
name: r.volumeName,
virtiofs: {},
}));
const fsVolumes = completedRows.map((r) => {
if (r.fsType === FILESYSTEM_SOURCE_TYPE.CONFIGMAP) {
return {
name: r.volumeName,
configMap: { name: r.resourceName },
};
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SECRET) {
return {
name: r.volumeName,
secret: { secretName: r.resourceName },
};
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT) {
return {
name: r.volumeName,
serviceAccount: { serviceAccountName: r.resourceName },
};
}
return null;
}).filter(Boolean);
if (filesystems.length > 0) {
this.spec.template.spec.domain.devices['filesystems'] = filesystems;
} else {
delete this.spec.template.spec.domain.devices['filesystems'];
}
if (fsVolumes.length > 0) {
if (!this.spec.template.spec.volumes) {
this.spec.template.spec['volumes'] = [];
}
this.spec.template.spec.volumes.push(...fsVolumes);
}
},
parseOther() {
@ -985,22 +907,14 @@ export default {
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
const devices = {
...this.spec.template.spec.domain.devices,
interfaces: mergedInterfaces,
};
if (this.isEdit && networkRow.length === 0) {
devices.autoattachPodInterface = false;
} else {
delete devices.autoattachPodInterface;
}
const spec = {
...this.spec.template.spec,
domain: {
...this.spec.template.spec.domain,
devices,
devices: {
...this.spec.template.spec.domain.devices,
interfaces: mergedInterfaces,
},
},
networks
};
@ -1742,7 +1656,7 @@ export default {
const oldImageId = old[0]?.image;
if (this.isCreate && oldImageId !== imageId && imageId && osType) {
if (this.isCreate && oldImageId === imageId && imageId) {
this.osType = osType;
}
}

View File

@ -9,7 +9,6 @@ import HarvesterResource from './harvester';
export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() {
let out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out = out.map((action) => {
if (action.action === 'showConfiguration') {
@ -27,13 +26,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
out.push(
{
action: 'enableConfig',
enabled: !this.isEnabled && canUpdate,
enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableConfig',
enabled: this.isEnabled && canUpdate,
enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
@ -63,27 +62,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
}
get stateDisplay() {
if (this.configStatus === 'out-of-sync') {
return this.t('harvester.migconfiguration.status.outOfSync');
}
return this.actualState;
}
get stateDescription() {
if (this.status?.message) {
return this.status.message;
}
return super.stateDescription;
}
get stateColor() {
if (this.configStatus === 'out-of-sync') {
return 'text-warning';
}
const state = this.actualState;
return colorForState(this.actualState);
return colorForState(state);
}
get isEnabled() {

View File

@ -34,7 +34,7 @@ export default class PCIDevice extends SteveModel {
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.isEnabling && !this.isvGPUDevice && this.canUpdate,
enabled: !this.isEnabling && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
@ -43,7 +43,7 @@ export default class PCIDevice extends SteveModel {
},
{
action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && this.canUpdate,
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough',
bulkable: true,
@ -54,10 +54,6 @@ export default class PCIDevice extends SteveModel {
return out;
}
get canUpdate() {
return !!this.linkFor('update');
}
get isvGPUDevice() {
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
return false;

View File

@ -16,13 +16,13 @@ export default class SRIOVDevice extends SteveModel {
out.push(
{
action: 'enableDevice',
enabled: !this.isEnabled && this.canUpdate,
enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableDevice',
enabled: this.isEnabled && this.canUpdate,
enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
@ -31,10 +31,6 @@ export default class SRIOVDevice extends SteveModel {
return out;
}
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() {
return false;
}

View File

@ -33,7 +33,7 @@ export default class USBDevice extends SteveModel {
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.passthroughClaim && !this.status.enabled && this.canUpdate,
enabled: !this.passthroughClaim && !this.status.enabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
@ -42,7 +42,7 @@ export default class USBDevice extends SteveModel {
},
{
action: 'disablePassthrough',
enabled: this.status.enabled && this.canUpdate,
enabled: this.status.enabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough',
bulkable: true,
@ -53,10 +53,6 @@ export default class USBDevice extends SteveModel {
return out;
}
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() {
return false;
}

View File

@ -27,18 +27,17 @@ const STATUS_DISPLAY = {
export default class VGpuDevice extends SteveModel {
get _availableActions() {
const out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out.push(
{
action: 'enableVGpu',
enabled: !this.isEnabled && canUpdate,
enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableVGpu',
enabled: this.isEnabled && canUpdate,
enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
bulkable: true,

View File

@ -24,7 +24,7 @@ const OBSCURE_NAMESPACE_PREFIX = [
export default class HciNamespace extends namespace {
get _availableActions() {
let out = super._availableActions;
const out = super._availableActions;
const remove = out.findIndex((a) => a.action === 'promptRemove');
const promptRemove = {
@ -53,16 +53,6 @@ export default class HciNamespace extends namespace {
insertAt(out, out.length - 1, promptRemove);
insertAt(out, out.length - 5, editQuotaAction);
const canUpdate = !!this.linkFor('update');
out = out.map((action) => {
if (['move'].includes(action.action)) {
return { ...action, enabled: action.enabled && canUpdate };
}
return action;
});
return out;
}

View File

@ -235,10 +235,6 @@ export default class HciPv extends HarvesterResource {
return allVMs.find(findAttachVM);
}
get attachVMName() {
return this.attachVM?.nameDisplay || this.attachVM?.metadata?.name || '';
}
get isAvailable() {
const unAvailable = ['Resizing', 'Not Ready'];

View File

@ -5,20 +5,6 @@ import Secret from '@shell/models/secret';
import { NAMESPACE } from '@shell/config/types';
export default class HciSecret extends Secret {
get _availableActions() {
let out = super._availableActions;
out = out.map((action) => {
if (['download'].includes(action.action)) {
return { ...action, enabled: !!this.linkFor('update') };
}
return action;
});
return out;
}
// prevent harvester secret detail page be overridden.
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
get fullDetailPageOverride() {

View File

@ -96,10 +96,6 @@ export default class HciStorageClass extends StorageClass {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
}
get expandOnlineEncryptedVolumeFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('expandOnlineEncryptedVolume');
}
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
@ -110,15 +106,6 @@ export default class HciStorageClass extends StorageClass {
get availableActions() {
let out = super.availableActions || [];
const canUpdate = !!this.linkFor('update');
out = out.map((action) => {
if (['setDefault', 'setAsDefault', 'resetDefault'].includes(action.action)) {
return { ...action, enabled: canUpdate };
}
return action;
});
if (this.isInternalStorageClass()) {
out = out.filter((action) => {

View File

@ -4,8 +4,6 @@ import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
import HarvesterResource from './harvester';
import { HCI } from '../types';
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
export default class HciAddonConfig extends HarvesterResource {
get availableActions() {
const out = super._availableActions;
@ -21,10 +19,9 @@ export default class HciAddonConfig extends HarvesterResource {
out.push(rancherDashboard);
}
const canUpdate = !!this.linkFor('update');
const toggleAddon = {
action: 'toggleAddon',
enabled: canUpdate,
enabled: true,
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
};
@ -48,15 +45,6 @@ export default class HciAddonConfig extends HarvesterResource {
}
}
if (!this.spec.enabled && this.id === HARVESTER_NVIDIA_DRIVER_TOOLKIT) {
this.$dispatch('promptModal', {
resources: [this],
component: 'HarvesterEnableNvidiaDriverToolkit',
});
return;
}
this.spec.enabled = !this.spec.enabled;
await this.save();
} catch (err) {

View File

@ -3,20 +3,6 @@ import { findBy } from '@shell/utils/array';
import HarvesterResource from './harvester';
export default class HciKeypair extends HarvesterResource {
get _availableActions() {
let out = super._availableActions;
out = out.map((action) => {
if (['download'].includes(action.action)) {
return { ...action, enabled: !!this.linkFor('update') };
}
return action;
});
return out;
}
get stateDisplay() {
const conditions = get(this, 'status.conditions');
const status = (findBy(conditions, 'type', 'validated') || {}).status ;

View File

@ -19,18 +19,16 @@ export default class ScheduleVmBackup extends HarvesterResource {
}
});
const canUpdate = !!this.linkFor('update');
return [
{
action: 'resumeSchedule',
enabled: canUpdate && ucFirst(this.state) === STATES.suspended.label,
enabled: ucFirst(this.state) === STATES.suspended.label,
icon: 'icons icon-play',
label: this.t('harvester.action.resumeSchedule'),
},
{
action: 'suspendSchedule',
enabled: canUpdate && ucFirst(this.state) === STATES.active.label,
enabled: ucFirst(this.state) === STATES.active.label,
icon: 'icons icon-pause',
label: this.t('harvester.action.suspendSchedule'),
},

View File

@ -39,6 +39,9 @@ function isReady() {
export default class HciVmImage extends HarvesterResource {
get availableActions() {
let out = super._availableActions;
const toFilter = ['goToEditYaml'];
out = out.filter( (A) => !toFilter.includes(A.action));
// show `Clone` only when imageSource is `download`
if (this.imageSource !== 'download') {
@ -52,7 +55,6 @@ export default class HciVmImage extends HarvesterResource {
canCreateVM = false;
}
const canCreateImage = !!this.$getters?.['schemaFor']?.(HCI.IMAGE)?.collectionMethods?.some((method) => method.toLowerCase() === 'post');
const customActions = this.isReady ? [
{
action: 'createFromImage',
@ -62,13 +64,13 @@ export default class HciVmImage extends HarvesterResource {
},
{
action: 'encryptImage',
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted && canCreateImage,
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
icon: 'icon icon-lock',
label: this.t('harvester.action.encryptImage'),
},
{
action: 'decryptImage',
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted && canCreateImage,
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
icon: 'icon icon-unlock',
label: this.t('harvester.action.decryptImage'),
},

View File

@ -23,17 +23,17 @@ export default class HciVmTemplateVersion extends HarvesterResource {
});
const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = false;
let canCreateVM = true;
if (schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = true;
if ( schema && !schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = false;
}
return [
{
action: 'launchFromTemplate',
icon: 'icon icon-spinner',
enabled: canCreateVM && this.isReady,
disabled: !canCreateVM || !this.isReady,
label: this.t('harvester.action.launchFormTemplate'),
},
{

View File

@ -83,60 +83,17 @@ const VMIPhase = {
let productInStore;
let _podOwnerMap = null;
let _podOwnerMapSource = null;
function getPodByOwnerName(rootGetters, inStore, ownerName) {
const podList = rootGetters[`${ inStore }/all`](POD);
if (!Array.isArray(podList)) {
return undefined;
}
// if not equals (usually means the pod list has been updated), we need to rebuild the map, otherwise we can reuse the map for better performance
if (_podOwnerMapSource !== podList) {
_podOwnerMap = new Map(); // use Map to store ownerReference name and pod mapping
for (const pod of podList) {
const refName = pod.metadata?.ownerReferences?.[0]?.name;
if (refName) {
_podOwnerMap.set(refName, pod);
}
}
_podOwnerMapSource = podList;
}
return _podOwnerMap.get(ownerName);
}
function getPvcsByNames(rootGetters, inStore, names) {
const pvcList = rootGetters[`${ inStore }/all`](PVC);
if (!Array.isArray(pvcList)) {
return [];
}
const uniqueNames = new Set(names);
return pvcList.filter((pvc) => uniqueNames.has(pvc.metadata?.name));
}
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
export default class VirtVm extends HarvesterResource {
get availableActions() {
let out = super._availableActions;
if (this.isCloneBackendStorageCloning || this.isCloneBackendStorageFailed) {
out = out.filter(({ action }) => action !== 'goToClone');
}
const out = super._availableActions;
const clone = out.find((action) => action.action === 'goToClone');
if (clone) {
clone.action = 'goToCloneVM';
}
const canCreateVMSSchedule = !!this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
return [
{
action: 'stopVM',
@ -169,7 +126,6 @@ export default class VirtVm extends HarvesterResource {
},
{
action: 'restartVM',
altAction: 'altRestartVM',
enabled: !!this.actions?.restart,
icon: 'icon icon-refresh',
label: this.t('harvester.action.restart'),
@ -178,7 +134,6 @@ export default class VirtVm extends HarvesterResource {
},
{
action: 'softrebootVM',
altAction: 'doSoftReboot',
enabled: !!this.actions?.softreboot,
icon: 'icon icon-pipeline',
label: this.t('harvester.action.softreboot')
@ -188,8 +143,7 @@ export default class VirtVm extends HarvesterResource {
enabled: !!this.actions?.start,
icon: 'icon icon-play',
label: this.t('harvester.action.start'),
bulkable: true,
bulkAction: 'startVM'
bulkable: true
},
{
action: 'backupVM',
@ -217,7 +171,7 @@ export default class VirtVm extends HarvesterResource {
},
{
action: 'createSchedule',
enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled,
enabled: this.schedulingVMBackupFeatureEnabled,
icon: 'icon icon-history',
label: this.t('harvester.action.createSchedule')
},
@ -237,9 +191,7 @@ export default class VirtVm extends HarvesterResource {
action: 'migrateVM',
enabled: !!this.actions?.migrate,
icon: 'icon icon-copy',
label: this.t('harvester.action.vmMigrate'),
bulkable: true,
bulkAction: 'migrateVM'
label: this.t('harvester.action.migrate')
},
{
action: 'abortMigrationVM',
@ -550,38 +502,16 @@ export default class VirtVm extends HarvesterResource {
});
}
async altRestartVM() {
await this.doActionGrowl('restart', {});
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
altStopVM() {
this.doActionGrowl('stop', {});
}
async altStopVM() {
await this.doActionGrowl('stop', {});
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
forceStop() {
this.doActionGrowl('forceStop', {});
}
async forceStop() {
await this.doActionGrowl('forceStop', {});
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
}
async startVM(resources = this) {
const list = Array.isArray(resources) ? resources : [resources];
for (const r of list) {
await r.doActionGrowl('start', {});
}
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
}
async download() {
await super.download();
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
}
async downloadBulk(items) {
await super.downloadBulk(items);
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
startVM() {
this.doActionGrowl('start', {});
}
migrateVM(resources = this) {
@ -730,13 +660,16 @@ export default class VirtVm extends HarvesterResource {
get podResource() {
const inStore = this.productInStore;
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
const podList = this.$rootGetters[`${ inStore }/all`](POD);
if (!vmiResource?.metadata?.name) {
return undefined;
}
return getPodByOwnerName(this.$rootGetters, inStore, vmiResource.metadata.name);
return podList.find((P) => {
return (
vmiResource?.metadata?.name &&
vmiResource?.metadata?.name === P.metadata?.ownerReferences?.[0].name
);
});
}
get isPaused() {
@ -777,13 +710,17 @@ export default class VirtVm extends HarvesterResource {
get vmi() {
const inStore = this.productInStore;
return this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
const vmis = this.$rootGetters[`${ inStore }/all`](HCI.VMI);
return vmis.find((VMI) => VMI.id === this.id);
}
get volumes() {
const pvcs = this.$rootGetters[`${ this.productInStore }/all`](PVC);
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
return getPvcsByNames(this.$rootGetters, this.productInStore, volumeClaimNames);
return pvcs.filter((pvc) => volumeClaimNames.includes(pvc.metadata.name));
}
get lvmVolumes() {
@ -794,18 +731,6 @@ export default class VirtVm extends HarvesterResource {
return this.volumes.filter((volume) => volume?.isLonghornV2);
}
get cloneBackendStorageStatus() {
return this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS]?.toLowerCase() || '';
}
get isCloneBackendStorageCloning() {
return this.cloneBackendStorageStatus === 'cloning';
}
get isCloneBackendStorageFailed() {
return this.cloneBackendStorageStatus === 'failed';
}
get encryptedVolumeType() {
if (!this.volumes || this.volumes.length === 0) {
return 'none';
@ -828,6 +753,17 @@ export default class VirtVm extends HarvesterResource {
return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
}
if ((this.vmi || this.isVMCreated) && this.podResource) {
// const podStatus = this.podResource.getPodStatus;
// if (POD_STATUS_ALL_ERROR.includes(podStatus?.status)) {
// return {
// ...podStatus,
// status: 'LAUNCHER_POD_ERROR',
// pod: this.podResource,
// };
// }
}
return this?.vmi?.status?.phase;
}
@ -861,21 +797,13 @@ export default class VirtVm extends HarvesterResource {
!this.isVMExpectedRunning &&
this.isVMCreated &&
this.vmi?.status?.phase === VMIPhase.Pending
) || this.isCloneBackendStorageCloning) {
) || (this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS] === 'cloning')) {
return { status: VMIPhase.Pending };
}
return null;
}
get isCloneFailed() {
if (this.isCloneBackendStorageFailed) {
return { status: VMIPhase.Failed };
}
return null;
}
get isStopping() {
if (this &&
!this.isVMExpectedRunning &&
@ -973,7 +901,9 @@ export default class VirtVm extends HarvesterResource {
const inStore = this.productInStore;
const res = this.$rootGetters[`${ inStore }/byId`](HCI.RESTORE, id);
const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE);
const res = allRestore.find((O) => O.id === id);
if (res) {
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
@ -1032,7 +962,6 @@ export default class VirtVm extends HarvesterResource {
this.isUnschedulable?.status ||
this.isPaused?.status ||
this.isVMError?.status ||
this.isCloneFailed?.status ||
this.isPending?.status ||
this.isStopping?.status ||
this.isOff?.status ||
@ -1040,7 +969,7 @@ export default class VirtVm extends HarvesterResource {
this.isRunning?.status ||
this.isNotReady?.status ||
this.isStarting?.status ||
this.isWaitingForVMI?.status ||
this.isWaitingForVMI?.state ||
this.otherState?.status;
return state;
@ -1144,6 +1073,42 @@ export default class VirtVm extends HarvesterResource {
return out;
}
get warningCount() {
return this.resourcesStatus.warningCount;
}
get errorCount() {
return this.resourcesStatus.errorCount;
}
get resourcesStatus() {
const inStore = this.productInStore;
const vmList = this.$rootGetters[`${ inStore }/all`](HCI.VM);
let warningCount = 0;
let errorCount = 0;
vmList.forEach((vm) => {
const status = vm.actualState;
if (status === VM_ERROR) {
errorCount += 1;
} else if (
status === 'Stopping' ||
status === 'Waiting' ||
status === 'Pending' ||
status === 'Starting' ||
status === 'Terminating'
) {
warningCount += 1;
}
});
return {
warningCount,
errorCount
};
}
get volumeClaimTemplates() {
return parseVolumeClaimTemplates(this);
}
@ -1161,6 +1126,7 @@ export default class VirtVm extends HarvesterResource {
get rootImageId() {
let imageId = '';
const inStore = this.productInStore;
const pvcs = this.$rootGetters[`${ inStore }/all`](PVC) || [];
const volumes = this.spec.template.spec.volumes || [];
@ -1170,7 +1136,9 @@ export default class VirtVm extends HarvesterResource {
});
if (!isNoExistingVolume) {
const existingVolume = this.$rootGetters[`${ inStore }/byId`](PVC, `${ this.metadata.namespace }/${ firstVolumeName }`);
const existingVolume = pvcs.find(
(P) => P.id === `${ this.metadata.namespace }/${ firstVolumeName }`
);
if (existingVolume) {
return existingVolume?.metadata?.annotations?.[
@ -1319,10 +1287,6 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
}
get nameDisplay() {
return this.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || this.metadata?.name || this.id;
}
get volumeEncryptionFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
}
@ -1352,7 +1316,8 @@ export default class VirtVm extends HarvesterResource {
}
get isBackupTargetUnavailable() {
const backupTargetSetting = this.$rootGetters['harvester/byId'](HCI.SETTING, 'backup-target');
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
return isBackupTargetSettingUnavailable(backupTargetSetting);
}

View File

@ -1,19 +0,0 @@
import shellProject from '@shell/models/management.cattle.io.project';
// This model controls `Project / Namespace` page in rancher integration mode
// Extend management.cattle.io.project model from shell
export default class Project extends shellProject {
get _availableActions() {
const canUpdate = !!this.linkFor('update');
// disable `Edit Config` action if user does not have update permission.
return super._availableActions.map((action) => {
if (action.action === 'goToEdit') {
return { ...action, enabled: canUpdate };
}
return action;
});
}
}

View File

@ -57,11 +57,7 @@ export default class HciVlanConfig extends HarvesterResource {
get _availableActions() {
const out = super._availableActions;
const canMigrate = !!this.$getters?.['schemaFor']?.(HCI.VLAN_CONFIG)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
if (canMigrate) {
insertAt(out, 0, this.migrateAction);
}
return out;
}

View File

@ -1,7 +1,7 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.9.0-dev",
"version": "1.8.1-dev",
"private": false,
"rancher": {
"annotations": {

View File

@ -123,7 +123,6 @@ export default {
this.value?.[0]?.currentRouter().push(goTo);
}
this.close();
this.$store.commit('action-menu/togglePromptModal', { performCallback: true, clearTableSelection: true });
}).catch((err) => {
this.$emit('errors', err);
});

View File

@ -3,8 +3,7 @@ import { ClusterNotFoundError } from '@shell/utils/error';
import { SETTING } from '@shell/config/settings';
import { COUNT, NAMESPACE, MANAGEMENT } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { DEV, NAMESPACE_FILTERS } from '@shell/store/prefs';
import { createNamespaceFilterKeyWithId } from '@shell/utils/namespace-filter';
import { DEV } from '@shell/store/prefs';
import { HCI } from '../../types';
export default {
@ -122,11 +121,8 @@ export default {
await dispatch('cleanNamespaces', null, { root: true });
const namespaceFilterKey = createNamespaceFilterKeyWithId(id, 'harvester');
const savedFilters = rootGetters['prefs/get'](NAMESPACE_FILTERS)?.[namespaceFilterKey];
commit('updateNamespaces', {
filters: savedFilters || [],
filters: [],
all: getters.filterNamespace(),
getters
}, { root: true });

View File

@ -123,26 +123,5 @@ export default {
const clusterId = currentCluster.id;
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
},
// Few harvester resources name and REAL resource are different. E.g. HCI.NETWORK_ATTACHMENT page resource is NETWORK_ATTACHMENT.
// Check in config/harvester-cluster.js for more details.
// We need to look up the schema by resource name, and fallback to find using real resource name
schemaFor: (state, getters, rootState, rootGetters) => (type, _fuzzy = false, _allowThrow = true) => {
// follow the same logic as type-map/schemaFor in /dashboard/shell/plugins/dashboard-store/getters.js
const normalizedType = getters.normalizeType(type);
const schemas = state.types['schema'];
const out = schemas?.map?.get(normalizedType);
if (out) return out;
// if not found, use the resource mapping in configureType for a second try
const resourceType = rootGetters['type-map/optionsFor'](type)?.resource;
if (resourceType && resourceType !== type) {
const normalizedResource = getters.normalizeType(resourceType);
return schemas?.map?.get(normalizedResource) || null;
}
return null;
},
};

View File

@ -1,9 +1,3 @@
// To find the CRD name, you can run `kubectl api-resources` and look for the `NAME` column.
// The CRD name is usually in the format of `<plural>.<group>`, where `<plural>` is the plural form of the resource and `<group>` is the API group it belongs to.
// e.g
// 1. `virtualmachines.kubevirt.io` -> kubevirt.io.virtualmachine, the CRD name for the `VirtualMachine` resource in the `kubevirt.io` API group
// 2. `vpc-nat-gateways.kubeovn.io` -> kubeovn.io.vpcnatgateway, the CRD name for the `VpcNatGateway` resource in the `kubeovn.io` API group.
export const HCI = {
VM: 'kubevirt.io.virtualmachine',
VMI: 'kubevirt.io.virtualmachineinstance',
@ -22,16 +16,9 @@ export const HCI = {
RESTORE: 'harvesterhci.io.virtualmachinerestore',
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
HOST_NETWORK_CONFIG: 'network.harvesterhci.io.hostnetworkconfig',
SUBNET: 'kubeovn.io.subnet',
VPC: 'kubeovn.io.vpc',
IP: 'kubeovn.io.ip',
VLAN: 'kubeovn.io.vlan',
IPTABLES_EIP: 'kubeovn.io.iptableseip',
IPTABLES_SNAT_RULE: 'kubeovn.io.iptablessnatrule',
IPTABLES_DNAT_RULE: 'kubeovn.io.iptablesdnatrule',
PROVIDER_NETWORK: 'kubeovn.io.providernetwork',
VPC_NAT_GATEWAY: 'kubeovn.io.vpcnatgateway',
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
@ -75,7 +62,6 @@ export const HCI = {
VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource',
VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport',
MIGRATION: 'migration.harvesterhci.io',
};
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';

View File

@ -31,42 +31,22 @@ export function registerAddonSideNav(store, productName, {
}, 600);
};
const hasAccessibleSchema = (t) => {
try {
return !!store.getters[`${ productName }/schemaFor`]?.(t);
} catch (e) {
return false;
}
};
const showTypes = (visibleTypes) => {
// Adds or removes the resource IDs from the product visibility whitelist.
const setMenuVisibility = (visible) => {
if (visible) {
store.commit('type-map/basicType', {
product: productName,
group: navGroup,
types: visibleTypes
types
});
};
const hideTypes = () => {
} else {
// Manually delete the keys from the state object to hide them.
const basicTypes = store.state['type-map'].basicTypes[productName];
if (basicTypes) {
types.forEach((t) => delete basicTypes[t]);
}
};
// Adds or removes the resource IDs from the product visibility whitelist.
const setMenuVisibility = (visible) => {
const accessibleTypes = visible ? types.filter(hasAccessibleSchema) : [];
// Always clear first to remove any previously-registered types that are
// no longer accessible (e.g. partial permission changes like types=[A,B] where B is dropped).
hideTypes();
if (accessibleTypes.length > 0) {
showTypes(accessibleTypes);
}
kickSideNav();
};

View File

@ -1,21 +0,0 @@
const AUTH_ERROR_CODES = [401, 403, 404];
export function getLoginAwareErrors(err, message = '') {
const errors = Array.isArray(err) ? err : (err ? [err] : []);
if (!errors.length) {
return [];
}
const generic = message;
if (errors.some((e) => AUTH_ERROR_CODES.includes(e?._status || e?.response?.status))) {
return [generic];
}
const msgs = errors
.map((e) => (typeof e === 'string' ? e : (e?.message || e?._statusText || '')))
.filter(Boolean);
return msgs.length ? msgs : [generic];
}

View File

@ -27,6 +27,23 @@ OUTPUT_DIR=dist/${DIR}-embedded
echo "Building..."
COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard/' RESOURCE_BASE='/dashboard/' RANCHER_ENV=harvester yarn run build
if [ -v EMBED_PKG ]; then
echo "Build and embed plugin from: $EMBED_PKG"
PKG_FILE_NAME=${EMBED_PKG##*/}
echo PKG_FILE_NAME: $PKG_FILE_NAME
PKG_NAME="${PKG_FILE_NAME/.tar.gz/""}"
echo "Plugin name: '$PKG_NAME'"
# Fetch file, unpack and move to dist
curl $EMBED_PKG --output $PKG_FILE_NAME
OUTPUT_DIR_PKG=$OUTPUT_DIR/$PKG_NAME
mkdir -p $OUTPUT_DIR_PKG
tar xvfz $PKG_FILE_NAME -C $OUTPUT_DIR/$PKG_NAME
echo "Plugin contents that will be served from $PKG_NAME"
ls -alR $OUTPUT_DIR/$PKG_NAME
fi
echo "Destroying..."
find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \;
find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \;

3027
yarn.lock

File diff suppressed because it is too large Load Diff