mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 11:46:06 +08:00
Compare commits
431
Commits
v1.5.1-rc1
..
v1.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b12a8e41a5 | ||
|
|
11ff73704b | ||
|
|
47b2b638cb | ||
|
|
b224604389 | ||
|
|
95f35c633c | ||
|
|
981287af0a | ||
|
|
94be1c1c09 | ||
|
|
09469c67e2 | ||
|
|
b12d2b5df2 | ||
|
|
887a0d0517 | ||
|
|
73e91d19a4 | ||
|
|
bee30483ad | ||
|
|
71dad21796 | ||
|
|
5704240ac5 | ||
|
|
085d216a30 | ||
|
|
ff692affb0 | ||
|
|
cfb3392e2e | ||
|
|
fe42708361 | ||
|
|
62df6f5cbb | ||
|
|
2da7d0eb58 | ||
|
|
251d92afba | ||
|
|
7f3865343d | ||
|
|
8f7a153665 | ||
|
|
2a376933f2 | ||
|
|
e5c253f044 | ||
|
|
f0a9fe76a1 | ||
|
|
a0a797d5b9 | ||
|
|
1cc6f34d84 | ||
|
|
ebfc4e8369 | ||
|
|
bd647738ae | ||
|
|
c6fb969d7e | ||
|
|
2afb04947d | ||
|
|
730c68bf14 | ||
|
|
20bee39a6c | ||
|
|
dbb199d7bb | ||
|
|
5488979448 | ||
|
|
9e588e90c2 | ||
|
|
9378277102 | ||
|
|
b5e78018a5 | ||
|
|
f411a0c0af | ||
|
|
cfa58985cf | ||
|
|
66a8f9d0e7 | ||
|
|
bf61c7dd7d | ||
|
|
56d97260c4 | ||
|
|
beabb34920 | ||
|
|
d2609157bd | ||
|
|
8fbe1943d8 | ||
|
|
ec6bc4d639 | ||
|
|
3824a14730 | ||
|
|
0fc8bece02 | ||
|
|
39764af627 | ||
|
|
bdc87bda0e | ||
|
|
e0dc77624b | ||
|
|
c3ba10bd22 | ||
|
|
5b7d54d0a3 | ||
|
|
99a216dfa0 | ||
|
|
e4c85f510e | ||
|
|
f391f018de | ||
|
|
4b2e92ea15 | ||
|
|
2f956d5946 | ||
|
|
5f8d556ea2 | ||
|
|
396ab48f1c | ||
|
|
6b8c079018 | ||
|
|
7f638e86c8 | ||
|
|
0c4955a766 | ||
|
|
3f4ff30275 | ||
|
|
8e0332a364 | ||
|
|
6700b2055e | ||
|
|
e486852f7a | ||
|
|
c19341bec9 | ||
|
|
8cbb9d6b18 | ||
|
|
00f0953592 | ||
|
|
58507f0b2e | ||
|
|
7785d7f469 | ||
|
|
2c043e0a8e | ||
|
|
6f90cae482 | ||
|
|
b4980a51e7 | ||
|
|
756ed383ac | ||
|
|
7e0a9dcd80 | ||
|
|
5fae6c3087 | ||
|
|
a994d9861e | ||
|
|
5d996c42dc | ||
|
|
88cd302ce4 | ||
|
|
e6dd8d6771 | ||
|
|
1a3822881e | ||
|
|
ee8bb21a10 | ||
|
|
e3d30a0eec | ||
|
|
532b6c4d50 | ||
|
|
f22644e058 | ||
|
|
cd128d0444 | ||
|
|
0278f51260 | ||
|
|
98efd63110 | ||
|
|
78e78ac8dd | ||
|
|
2aaa0a55a2 | ||
|
|
5c2a23924d | ||
|
|
7af8a82838 | ||
|
|
3b343bcaca | ||
|
|
6ddd35d661 | ||
|
|
bd28ba6f71 | ||
|
|
4bb67153ce | ||
|
|
7dbd442519 | ||
|
|
5a0d7f283d | ||
|
|
9fdbe9c58f | ||
|
|
f652ed9d4b | ||
|
|
21a1cd4e89 | ||
|
|
facc74ca51 | ||
|
|
74c12a0d1b | ||
|
|
3277ab4a2b | ||
|
|
4aabf0b7a3 | ||
|
|
4e8cb31e9d | ||
|
|
1b214b2b6f | ||
|
|
748c88866a | ||
|
|
c3e5c2161e | ||
|
|
f932afee68 | ||
|
|
0bbdf3bf17 | ||
|
|
bdf3ac2803 | ||
|
|
0d183a8174 | ||
|
|
8f72c33f4b | ||
|
|
22c99211f1 | ||
|
|
775330d829 | ||
|
|
18be022a9f | ||
|
|
56b4b46b5a | ||
|
|
db398ecad3 | ||
|
|
d175e3b11a | ||
|
|
905db4b12c | ||
|
|
fc31b4bb9d | ||
|
|
db29a7c31b | ||
|
|
2b3541164d | ||
|
|
ea5e9aa1f4 | ||
|
|
18a5608e72 | ||
|
|
18599fc94c | ||
|
|
e155d46483 | ||
|
|
795e5d178f | ||
|
|
2e03ac3cf7 | ||
|
|
76605fdc07 | ||
|
|
c289b001db | ||
|
|
e8d63da1eb | ||
|
|
4a456ac07d | ||
|
|
57dfddd593 | ||
|
|
66eac96575 | ||
|
|
182d92d80b | ||
|
|
1b362f570b | ||
|
|
cf798048f8 | ||
|
|
93c8399935 | ||
|
|
b72f523ddf | ||
|
|
e5ffb08df3 | ||
|
|
98a6322c11 | ||
|
|
be7e4bd80b | ||
|
|
ed2bc3100b | ||
|
|
b6ffb3e9f1 | ||
|
|
3d8a6bba7e | ||
|
|
be9311dc0c | ||
|
|
e294f4c00f | ||
|
|
a9fa928912 | ||
|
|
ef2b4d1589 | ||
|
|
a73e9f0ac1 | ||
|
|
11b3bf4c1f | ||
|
|
b775ce5f50 | ||
|
|
193d4536e3 | ||
|
|
9ca9fdb521 | ||
|
|
bcabefe9f3 | ||
|
|
c541f81dc3 | ||
|
|
4486f71c8f | ||
|
|
060105ead3 | ||
|
|
3ea73978ee | ||
|
|
f4e363396d | ||
|
|
fa16e24983 | ||
|
|
ce63ee6a19 | ||
|
|
ee28697161 | ||
|
|
85a06feb91 | ||
|
|
17dd46cee9 | ||
|
|
0ef4ff65cc | ||
|
|
cebb302730 | ||
|
|
f12717a8f4 | ||
|
|
03f54643fd | ||
|
|
7fb6d44208 | ||
|
|
dc683a50a4 | ||
|
|
be421054d8 | ||
|
|
fcef0391bb | ||
|
|
1d89aafeab | ||
|
|
7386a2deb6 | ||
|
|
26f5ebc8e2 | ||
|
|
8cd482870b | ||
|
|
57cbd799dd | ||
|
|
3694b316ab | ||
|
|
717258defd | ||
|
|
7aae6264f7 | ||
|
|
c88a083e04 | ||
|
|
52c4556e64 | ||
|
|
90cb147938 | ||
|
|
d9c97de0fd | ||
|
|
fb59b396d1 | ||
|
|
bdcea54eeb | ||
|
|
912ca7883f | ||
|
|
a89678cc81 | ||
|
|
d4d3774c3b | ||
|
|
32ebdc3acd | ||
|
|
703abd7ab9 | ||
|
|
e8f7f0e06b | ||
|
|
feddcd247a | ||
|
|
29b1ab2fb9 | ||
|
|
01528f7429 | ||
|
|
9e015ec3b1 | ||
|
|
7ecc9c320d | ||
|
|
d6da4898b4 | ||
|
|
d023104371 | ||
|
|
cb452b9627 | ||
|
|
dbbad01b0f | ||
|
|
b689e3aacf | ||
|
|
796d98e61a | ||
|
|
fe32b4372b | ||
|
|
3dcec52aff | ||
|
|
6d8f6579c7 | ||
|
|
9e240792e8 | ||
|
|
aa9dd32cce | ||
|
|
4f3e532327 | ||
|
|
cbfcf4dbae | ||
|
|
8877dcf639 | ||
|
|
8d196c4244 | ||
|
|
220e40feaa | ||
|
|
258476876a | ||
|
|
63c4810b99 | ||
|
|
0cd5651c38 | ||
|
|
6f66c80c63 | ||
|
|
0e5a78d8a6 | ||
|
|
d16802365e | ||
|
|
8410f75859 | ||
|
|
f8079c5924 | ||
|
|
1e270268d8 | ||
|
|
43064576d6 | ||
|
|
9dd36bbd87 | ||
|
|
235373045c | ||
|
|
5841508b26 | ||
|
|
b941902088 | ||
|
|
485db3066f | ||
|
|
af43ebbf6f | ||
|
|
74300f24b3 | ||
|
|
2993ddb82c | ||
|
|
a5d4604dce | ||
|
|
daa6d6942b | ||
|
|
eb92642b3b | ||
|
|
78234b9e1e | ||
|
|
3dc6dda1ca | ||
|
|
fca05d8489 | ||
|
|
238c660796 | ||
|
|
19bea97106 | ||
|
|
de4edbbf3b | ||
|
|
9256391627 | ||
|
|
25439aee65 | ||
|
|
f2fe1f5bd6 | ||
|
|
f8d5aa1a21 | ||
|
|
38cf667830 | ||
|
|
18667836c2 | ||
|
|
2940f25fe0 | ||
|
|
a24d01717a | ||
|
|
22a032e56c | ||
|
|
9343813ace | ||
|
|
2dff7b0a93 | ||
|
|
54c5d77198 | ||
|
|
1880043a80 | ||
|
|
d8bee7f4f5 | ||
|
|
90c923b480 | ||
|
|
374b904191 | ||
|
|
f2725e2773 | ||
|
|
487f9abc10 | ||
|
|
1862cfcc08 | ||
|
|
480f95a5cc | ||
|
|
b939df9b7d | ||
|
|
4e831cbc5f | ||
|
|
0283cfb2bb | ||
|
|
4e2562190c | ||
|
|
64b59a0c27 | ||
|
|
3e7b2338ff | ||
|
|
104e98e390 | ||
|
|
72415622d9 | ||
|
|
a861450874 | ||
|
|
cbd5e45200 | ||
|
|
8ad7a57ab4 | ||
|
|
966d4d6709 | ||
|
|
208f1f29f5 | ||
|
|
d923239b9d | ||
|
|
54f85963ea | ||
|
|
36257299e1 | ||
|
|
91d078068f | ||
|
|
8fbbfc90f1 | ||
|
|
efdfdbf0b8 | ||
|
|
8db4de1c4a | ||
|
|
05ff8e4f19 | ||
|
|
8f76d5ad30 | ||
|
|
ff4a865c55 | ||
|
|
30dc56cad5 | ||
|
|
c10573d24a | ||
|
|
ca7eccd688 | ||
|
|
fc606dc4d1 | ||
|
|
046947de32 | ||
|
|
6e5532e497 | ||
|
|
7e00a08e35 | ||
|
|
48ec40fe3d | ||
|
|
50901a2eaf | ||
|
|
ff439e66a9 | ||
|
|
b0b5e5f749 | ||
|
|
be2db9d37c | ||
|
|
9cd3cd6492 | ||
|
|
e07042b9c9 | ||
|
|
799252a789 | ||
|
|
36c68eefb1 | ||
|
|
dea3c098df | ||
|
|
0a782fa4d5 | ||
|
|
5905448b90 | ||
|
|
4553bdc666 | ||
|
|
7f98dfe9c8 | ||
|
|
6a3153a7ec | ||
|
|
188f058df8 | ||
|
|
629cb0c601 | ||
|
|
0c3fe22df7 | ||
|
|
f4edfb8f43 | ||
|
|
66f53c8a00 | ||
|
|
f855a0add5 | ||
|
|
7c2317fae9 | ||
|
|
50fcec0c11 | ||
|
|
4a303e514c | ||
|
|
b828c2f66d | ||
|
|
40794d89a0 | ||
|
|
a6a99520a6 | ||
|
|
e2daa1ea0c | ||
|
|
923f36a5c3 | ||
|
|
b499f62c56 | ||
|
|
9d02623b3a | ||
|
|
762ab100b9 | ||
|
|
cbe66f4660 | ||
|
|
5b39432481 | ||
|
|
9e32052329 | ||
|
|
a55353fd1b | ||
|
|
548bdd2835 | ||
|
|
0153f9d985 | ||
|
|
70a56c75b0 | ||
|
|
31a9abd5c2 | ||
|
|
6d5139a994 | ||
|
|
b92f22fa75 | ||
|
|
f9a277d893 | ||
|
|
3b054b35c6 | ||
|
|
2493287334 | ||
|
|
9af8526ef9 | ||
|
|
07e0dc0d5e | ||
|
|
fd3f5ec1e6 | ||
|
|
72c6741af1 | ||
|
|
375127a78a | ||
|
|
fc09e030a0 | ||
|
|
5f76da4629 | ||
|
|
3a3122f3a1 | ||
|
|
229cbf1f69 | ||
|
|
176946e5e9 | ||
|
|
f3d2d2f6a8 | ||
|
|
a175d1e142 | ||
|
|
26afffc2f1 | ||
|
|
5eb19106ea | ||
|
|
7b3857cb38 | ||
|
|
2232e56ba1 | ||
|
|
d023fa1de4 | ||
|
|
527d31e4d0 | ||
|
|
1651e201e8 | ||
|
|
43b30ec3c3 | ||
|
|
68ba934b5a | ||
|
|
67754427c7 | ||
|
|
cbb66175f6 | ||
|
|
84bacfaff5 | ||
|
|
e8017e14c8 | ||
|
|
d11d9d4e37 | ||
|
|
50bdd53186 | ||
|
|
5c96e90d58 | ||
|
|
5c06287648 | ||
|
|
c75755a527 | ||
|
|
e8f282395e | ||
|
|
2bb65801d9 | ||
|
|
ccd4ff2d7a | ||
|
|
ac6958fe40 | ||
|
|
426e15084e | ||
|
|
8477f0754d | ||
|
|
12cadbf667 | ||
|
|
b9fab8bb57 | ||
|
|
69e639ff96 | ||
|
|
cb4cdcb0c7 | ||
|
|
c4e9a89ba5 | ||
|
|
f2d5172224 | ||
|
|
f0a66774b8 | ||
|
|
e95955fad6 | ||
|
|
fc2be69469 | ||
|
|
fdc635955b | ||
|
|
f56f46a543 | ||
|
|
f5961f3a59 | ||
|
|
8f3a2b202d | ||
|
|
0ea3f56f2c | ||
|
|
6e9350e63d | ||
|
|
1c15ed8003 | ||
|
|
9f3e689727 | ||
|
|
526d1bb875 | ||
|
|
b0c7b1fefe | ||
|
|
6644651fe5 | ||
|
|
2d6bc65b26 | ||
|
|
3565b44e3a | ||
|
|
43b10b250e | ||
|
|
7b6efa624f | ||
|
|
d9db1263d1 | ||
|
|
10a8f18784 | ||
|
|
993706fead | ||
|
|
1098ef8f6a | ||
|
|
4480dbdeed | ||
|
|
a7b4366d7c | ||
|
|
d72857f374 | ||
|
|
57ab9e2830 | ||
|
|
8116358011 | ||
|
|
c791399a61 | ||
|
|
5bd8ad2a93 | ||
|
|
345fe636ec | ||
|
|
7e32ea453b | ||
|
|
8f3b5265de | ||
|
|
4ca6caefb9 | ||
|
|
fb7d604ed6 | ||
|
|
9a60a621de | ||
|
|
7645d6161f | ||
|
|
c3f2665912 | ||
|
|
2a58745550 | ||
|
|
69d7046bc7 | ||
|
|
692c775b51 | ||
|
|
8b43c200b2 | ||
|
|
ab71cb377c | ||
|
|
814d296d75 | ||
|
|
eb667fd56b | ||
|
|
e4b3f84905 | ||
|
|
ea68b95c26 | ||
|
|
da77762b7f |
@@ -14,3 +14,4 @@ dist-pkg
|
||||
pkg/harvester/index.ts
|
||||
pkg/harvester/store/*
|
||||
scripts/pkgfile.js
|
||||
tmp
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Setup node and install dependencies
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
addAssignees: author
|
||||
addReviewers: true
|
||||
numberOfReviewers: 0
|
||||
reviewers:
|
||||
- a110605
|
||||
- houhoucoop
|
||||
+12
-18
@@ -1,20 +1,14 @@
|
||||
pull_request_rules:
|
||||
- name: Automatically open backport PR to release-harvester-v1.5
|
||||
conditions:
|
||||
- base=main
|
||||
- label="require backport 1.5"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- "release-harvester-v1.5"
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
labels:
|
||||
- "v1.5 backport PR"
|
||||
- name: Ask to resolve conflict
|
||||
conditions:
|
||||
- conflict
|
||||
actions:
|
||||
comment:
|
||||
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
||||
|
||||
- name: Ask to resolve conflict
|
||||
conditions:
|
||||
- conflict
|
||||
actions:
|
||||
comment:
|
||||
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
||||
commands_restrictions:
|
||||
backport:
|
||||
conditions:
|
||||
- or:
|
||||
- sender-permission>=write
|
||||
- sender=github-actions[bot]
|
||||
@@ -2,18 +2,13 @@
|
||||
### Summary
|
||||
|
||||
### PR Checklists
|
||||
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)?
|
||||
- [ ] Yes, the relevant PR is at:
|
||||
- Are backend engineers aware of UI changes?
|
||||
- Are backend engineers aware of UI changes ?
|
||||
- [ ] Yes, the backend owner is:
|
||||
|
||||
### Related Issue #
|
||||
<!-- Define findings related to the feature or bug issue. -->
|
||||
|
||||
### Test screenshot/video
|
||||
### Test screenshot or video
|
||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
||||
|
||||
### Extra technical notes summary
|
||||
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# .github/release.yml
|
||||
changelog:
|
||||
categories:
|
||||
- title: Features
|
||||
labels:
|
||||
- feat
|
||||
- title: Bug Fixes
|
||||
labels:
|
||||
- fix
|
||||
- title: Dependencies
|
||||
labels:
|
||||
- deps
|
||||
- title: Performance
|
||||
labels:
|
||||
- perf
|
||||
- title: Documentation
|
||||
labels:
|
||||
- docs
|
||||
- title: Style
|
||||
labels:
|
||||
- style
|
||||
- title: Build & Continuous Integration
|
||||
labels:
|
||||
- build
|
||||
- ci
|
||||
- title: Security
|
||||
labels:
|
||||
- security
|
||||
- title: Tests
|
||||
labels:
|
||||
- test
|
||||
- title: Refactoring
|
||||
labels:
|
||||
- refactor
|
||||
- title: Chores
|
||||
labels:
|
||||
- chore
|
||||
- title: Others
|
||||
labels:
|
||||
- other
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":semanticCommitTypeAll(deps)",
|
||||
":semanticCommitScopeDisabled"
|
||||
],
|
||||
"baseBranches": [
|
||||
"main",
|
||||
"/^release-harvester-v\\d+\\.\\d+$/"
|
||||
],
|
||||
"automergeMajor": false,
|
||||
"semanticCommits": "enabled",
|
||||
"semanticCommitType": "deps",
|
||||
"prHourlyLimit": 12,
|
||||
"timezone": "Asia/Taipei",
|
||||
"schedule": ["after 10am on sunday"],
|
||||
"postUpdateOptions": ["yarnDedupeFewer"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["major"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["vue", "vue-router", "vuex"],
|
||||
"matchUpdateTypes": ["major"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["@rancher/shell"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["minor"],
|
||||
"groupName": "minor dependencies",
|
||||
"minimumReleaseAge": "7 days",
|
||||
"labels": ["minor-update"],
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["patch", "digest"],
|
||||
"automerge": true,
|
||||
"minimumReleaseAge": "7 days",
|
||||
"groupName": "patch digest dependencies",
|
||||
"labels": ["patch-update", "automerge"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "[PR Management] Auto Assign Reviewer & Assignee"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-assign:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: rancher/gh-issue-mgr/auto-assign-action@b70f0bdf12a03e5e3f33e4f92ccb6c89deb3ebd9 # main
|
||||
with:
|
||||
configuration-path: .github/auto-assign-config.yaml
|
||||
@@ -0,0 +1,94 @@
|
||||
name: "[PR Management] Add Labels"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
- 'release-harvester-v*'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
add-require-backport-label:
|
||||
if: github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.base.ref == 'main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
- name: Fetch release branches and PR labels
|
||||
id: fetch_info
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
repo="${{ github.repository }}"
|
||||
pr_number=${{ github.event.pull_request.number }}
|
||||
|
||||
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
||||
|
||||
if [[ -z "$release_branches" ]]; then
|
||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
|
||||
version="${latest_branch#release-harvester-v}"
|
||||
release_tag="v${version}.0"
|
||||
|
||||
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
||||
if echo "$tags" | grep -Fxq "$release_tag"; then
|
||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
label="require backport/v${version}"
|
||||
echo "should_label=true" >> "$GITHUB_OUTPUT"
|
||||
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
|
||||
|
||||
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
||||
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
|
||||
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Add label if needed
|
||||
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
|
||||
|
||||
add-backport-label:
|
||||
if: github.event.pull_request.draft == false &&
|
||||
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check conditions for backport label
|
||||
id: check
|
||||
run: |
|
||||
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
|
||||
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
|
||||
|
||||
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Add label if needed
|
||||
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
|
||||
version="${TARGET_BRANCH#release-harvester-v}"
|
||||
label="backport/v${version}"
|
||||
echo "Adding label $label"
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--add-label "$label"
|
||||
@@ -0,0 +1,49 @@
|
||||
name: "[PR Management] Request Backport via Mergify"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment-backport:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
- name: Post Mergify backport command
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
pr_number=${{ github.event.pull_request.number }}
|
||||
repo="${{ github.repository }}"
|
||||
|
||||
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
||||
labels=$(echo "$labels_json" | jq -r '.[] // empty')
|
||||
|
||||
echo "Labels on PR: $labels"
|
||||
|
||||
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
||||
|
||||
if [[ -z "$matches" ]]; then
|
||||
echo "No back‑port labels found — skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
branches=$(echo "$matches" \
|
||||
| sed -E 's/^require backport\/v/release-harvester-v/' \
|
||||
| sort -u | tr '\n' ' ')
|
||||
branches=${branches%% }
|
||||
|
||||
cmd="@Mergifyio backport $branches"
|
||||
echo "Posting Mergify command: $cmd"
|
||||
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
|
||||
@@ -11,18 +11,19 @@ defaults:
|
||||
working-directory: ./
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
build-and-push-extension-catalog:
|
||||
needs: lint
|
||||
needs: check-version
|
||||
uses: ./.github/workflows/build-extension-catalog.yml
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
@@ -25,12 +25,12 @@ jobs:
|
||||
name: Build & Upload Hosted
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
@@ -45,19 +45,20 @@ jobs:
|
||||
run: ./scripts/build-upload-gate
|
||||
|
||||
- name: Get gcs auth
|
||||
uses: rancher-eio/read-vault-secrets@main
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||
|
||||
- name: Apply gcs auth
|
||||
# https://github.com/google-github-actions/auth
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||
|
||||
with:
|
||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||
|
||||
- name: Upload build
|
||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||
# https://github.com/google-github-actions/upload-cloud-storage
|
||||
with:
|
||||
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
||||
@@ -71,12 +72,12 @@ jobs:
|
||||
name: Build & Upload Embedded
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
@@ -89,19 +90,19 @@ jobs:
|
||||
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
|
||||
|
||||
- name: Get gcs auth
|
||||
uses: rancher-eio/read-vault-secrets@main
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||
|
||||
- name: Apply gcs auth
|
||||
# https://github.com/google-github-actions/auth
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||
with:
|
||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||
|
||||
- name: Upload tar
|
||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||
with:
|
||||
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
||||
destination: releases.rancher.com/harvester-ui/dashboard
|
||||
@@ -114,12 +115,12 @@ jobs:
|
||||
name: Build & Upload Plugin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
@@ -133,19 +134,19 @@ jobs:
|
||||
run: ./scripts/build-upload-gate
|
||||
|
||||
- name: Get gcs auth
|
||||
uses: rancher-eio/read-vault-secrets@main
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||
|
||||
- name: Apply gcs auth
|
||||
# https://github.com/google-github-actions/auth
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||
with:
|
||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||
|
||||
- name: Upload plugin tar
|
||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||
with:
|
||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
||||
destination: releases.rancher.com/harvester-ui/plugin
|
||||
@@ -155,7 +156,7 @@ jobs:
|
||||
process_gcloudignore: false
|
||||
|
||||
- name: Upload plugin directory
|
||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||
with:
|
||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||
|
||||
@@ -27,14 +27,14 @@ jobs:
|
||||
build-status: ${{ job.status }}
|
||||
steps:
|
||||
- name: Read Secrets
|
||||
uses: rancher-eio/read-vault-secrets@main
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
|
||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
||||
|
||||
- name: Checkout repository (normal flow)
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
@@ -45,18 +45,18 @@ jobs:
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
username: ${{ env.DOCKER_USERNAME }}
|
||||
password: ${{ env.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v3
|
||||
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||
with:
|
||||
version: v3.8.0
|
||||
|
||||
- name: Setup Nodejs with yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: yarn
|
||||
@@ -67,4 +67,4 @@ jobs:
|
||||
- name: Build and push UI image
|
||||
run: |
|
||||
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
||||
$publish
|
||||
$publish
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'release-harvester-v*'
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
setup-target-branch:
|
||||
@@ -12,19 +13,22 @@ jobs:
|
||||
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Determine target branch
|
||||
id: get-version
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
|
||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
TARGET_BRANCH="main-head"
|
||||
elif [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
|
||||
TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
|
||||
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
|
||||
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Error: invalid branch format." && exit 1
|
||||
fi
|
||||
|
||||
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
|
||||
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Ensure target branch exists
|
||||
run: |
|
||||
git fetch --all
|
||||
@@ -40,7 +44,7 @@ jobs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Extract version from package.json
|
||||
id: get_version
|
||||
@@ -58,7 +62,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
@@ -66,7 +70,7 @@ jobs:
|
||||
yarn install --frozen-lockfile
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v3
|
||||
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||
with:
|
||||
version: v3.8.0
|
||||
|
||||
@@ -75,7 +79,7 @@ jobs:
|
||||
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
||||
|
||||
- name: Upload charts artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: charts
|
||||
path: tmp
|
||||
@@ -90,7 +94,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout release branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: '${{ github.ref_name }}'
|
||||
|
||||
@@ -101,7 +105,7 @@ jobs:
|
||||
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout target branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
||||
|
||||
@@ -117,7 +121,7 @@ jobs:
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: charts
|
||||
|
||||
@@ -128,7 +132,7 @@ jobs:
|
||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||
|
||||
- name: Run Helm chart releaser
|
||||
uses: helm/chart-releaser-action@v1.4.1
|
||||
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
|
||||
with:
|
||||
charts_dir: ./charts
|
||||
env:
|
||||
|
||||
@@ -17,37 +17,36 @@ jobs:
|
||||
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Determine release tag
|
||||
id: determine_tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
|
||||
echo "${RELEASE_TAG}"
|
||||
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Error: invalid tag format." && exit 1
|
||||
fi
|
||||
lint:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
build-extension-charts:
|
||||
needs:
|
||||
- setup-release-tag
|
||||
- lint
|
||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
||||
- check-version
|
||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@9bf23dff6650116673346447bcf0b088e0edb27f # creators-pkg-v3.0.12
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
deployments: write
|
||||
pages: write
|
||||
pull-requests: write
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
|
||||
|
||||
@@ -5,14 +5,20 @@ on:
|
||||
- v[1-9].*
|
||||
|
||||
jobs:
|
||||
build-validation:
|
||||
name: Build Test
|
||||
uses: ./.github/workflows/run-lint.yaml
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{github.ref_name}}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
build:
|
||||
name: Build and Upload Package
|
||||
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
||||
needs:
|
||||
- build-validation
|
||||
needs: check-version
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: FOSSA Scanning
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "release-harvester-v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
fossa-scanning:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
# The FOSSA token is shared between all repos in Harvester's GH org. It can
|
||||
# be used directly and there is no need to request specific access to EIO.
|
||||
- name: Read FOSSA token
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
|
||||
|
||||
- name: FOSSA scan
|
||||
uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.0
|
||||
with:
|
||||
api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }}
|
||||
# Only runs the scan and do not provide/returns any results back to the
|
||||
# pipeline.
|
||||
run-tests: false
|
||||
@@ -0,0 +1,30 @@
|
||||
name: "[PR Management] Add PR Label"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, edited]
|
||||
branches:
|
||||
- main
|
||||
- 'release-harvester-v*'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-assign-pr-label:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
- name: Setup Nodejs and yarn install
|
||||
uses: ./.github/actions/setup
|
||||
- name: Set PR label
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||
echo "PR_LABEL = $PR_LABEL"
|
||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- main
|
||||
- 'release-*'
|
||||
pull_request:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
branches:
|
||||
- main
|
||||
- 'release-*'
|
||||
@@ -15,9 +16,58 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-depth: 0 # Need full history for commit-lint
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
- name: Validate PR title lint
|
||||
if: github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
|
||||
|
||||
- name: Validate commit messages
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
|
||||
echo "GITHUB_BASE_SHA=$GITHUB_BASE_SHA"
|
||||
echo "GITHUB_HEAD_SHA=$GITHUB_HEAD_SHA"
|
||||
echo "GITHUB_BEFORE=$GITHUB_BEFORE"
|
||||
echo "GITHUB_AFTER=$GITHUB_AFTER"
|
||||
|
||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "$GITHUB_BASE_SHA" ] && [ -n "$GITHUB_HEAD_SHA" ]; then
|
||||
FROM="$GITHUB_BASE_SHA"
|
||||
TO="$GITHUB_HEAD_SHA"
|
||||
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
||||
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
||||
# first push to HEAD
|
||||
FROM=""
|
||||
TO="$GITHUB_AFTER"
|
||||
else
|
||||
FROM="$GITHUB_BEFORE"
|
||||
TO="$GITHUB_AFTER"
|
||||
fi
|
||||
else
|
||||
echo "No valid commit range found, skipping commitlint."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "FROM=$FROM"
|
||||
echo "TO=$TO"
|
||||
|
||||
if [ -z "$FROM" ]; then
|
||||
echo "Linting last commit $TO"
|
||||
npx commitlint --last --verbose
|
||||
|
||||
else
|
||||
echo "Linting commits from $FROM to $TO"
|
||||
npx commitlint --from "$FROM" --to "$TO" --verbose
|
||||
fi
|
||||
env:
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_BEFORE: ${{ github.event.before }}
|
||||
GITHUB_AFTER: ${{ github.event.after }}
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit $1
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
yarn lint
|
||||
@@ -1,62 +1,168 @@
|
||||
# harvester-ui-extension
|
||||
|
||||
Rancher Extension used in [rancher/dashboard](https://github.com/rancher/dashboard) for [Harvester](https://harvesterhci.io) Dashboard UI.
|
||||
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
||||
|
||||
`
|
||||
From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
|
||||
`
|
||||
> **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.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the Harvester UI Extension in Rancher 2.10.0 and newer, please refer to [installation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210) on harvester doc.
|
||||
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||
|
||||
## Running for Development
|
||||
|
||||
This is what you probably want to get started.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
Nodejs version >= 20
|
||||
## Development Setup
|
||||
|
||||
Ensure **Node.js v20 or later** is installed for development and debugging.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
For development, serve with hot reload at https://localhost:8005
|
||||
Run the extension standalone with hot reload at `https://localhost:8005`.
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
# Use RANCHER_ENV to specify the endpoint for your Harvester API
|
||||
# Start the development server
|
||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||
|
||||
# Use VUE_APP_SERVER_VERSION to specify the Harvester server version
|
||||
# in case your are running a dev build of Harvester
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=your-harvester-server-version API=https://your-harvester-ip yarn dev
|
||||
# Example with specific server version
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
||||
```
|
||||
|
||||
# or put your env variables into a .env file
|
||||
You may also define environment variables in a `.env` file:
|
||||
|
||||
# Example
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
|
||||
```env
|
||||
RANCHER_ENV=harvester
|
||||
VUE_APP_SERVER_VERSION=v1.5.0
|
||||
API=https://192.168.1.123
|
||||
```
|
||||
|
||||
### Rancher Integration Mode
|
||||
|
||||
In this mode, you need to have Rancher and run Harvester UI Extension as Rancher extension, see more detail in [Rancher UI extension](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||
|
||||
```bash
|
||||
API=https://your-rancher-ip yarn dev
|
||||
|
||||
# Example
|
||||
API=https://192.168.1.123 yarn dev
|
||||
```
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
All commit messages must follow the conventional commit format:
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
### Supported Types
|
||||
|
||||
- **feat**: New features
|
||||
- **fix**: Bug fixes
|
||||
- **docs**: Documentation changes
|
||||
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
||||
- **refactor**: Code refactoring
|
||||
- **perf**: Performance improvements
|
||||
- **test**: Adding or updating tests
|
||||
- **build**: Build system or external dependencies
|
||||
- **ci**: CI/CD changes
|
||||
- **chore**: Other changes that don't modify src or test files
|
||||
- **revert**: Reverts a previous commit
|
||||
- **wip**: Work in progress
|
||||
- **deps**: Dependency updates
|
||||
- **security**: Security fixes
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Feature
|
||||
git commit -m "feat: add new virtual machine creation wizard"
|
||||
|
||||
# Bug fix
|
||||
git commit -m "fix: resolve memory leak in VM console"
|
||||
|
||||
# Documentation
|
||||
git commit -m "docs: update installation instructions"
|
||||
|
||||
# Breaking change
|
||||
git commit -m "feat!: change API endpoint structure
|
||||
|
||||
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
||||
```
|
||||
|
||||
### Git Hooks
|
||||
|
||||
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
||||
|
||||
- **pre-commit**: Runs ESLint to ensure code quality
|
||||
- **commit-msg**: Validates commit message format using commit-lint
|
||||
|
||||
These hooks are automatically installed when you run `yarn install`.
|
||||
|
||||
### Manual Validation
|
||||
|
||||
You can manually validate commit messages:
|
||||
|
||||
```bash
|
||||
# Validate the last commit
|
||||
yarn commitlint
|
||||
|
||||
# Validate a specific commit
|
||||
npx commitlint --from <commit-hash>
|
||||
|
||||
# Validate a range of commits
|
||||
npx commitlint --from <start-hash> --to <end-hash>
|
||||
```
|
||||
|
||||
## Branch Structure
|
||||
|
||||
- **`main`** – Main development branch
|
||||
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
||||
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
||||
|
||||
> **Note:**
|
||||
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### UI Extension Testing
|
||||
|
||||
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
||||
|
||||
- Examples:
|
||||
- Test `1.0.x` series → `v1.0-head`
|
||||
- Test `1.5.x` series → `v1.5-head`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
||||
3. Go to the **Extensions** page and install the desired version
|
||||
|
||||
### Standalone Mode Testing
|
||||
|
||||
To test the standalone UI, configure Harvester to load the UI from an external source.
|
||||
|
||||
- Examples of `ui-index`:
|
||||
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
||||
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
||||
|
||||
**Steps:**
|
||||
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
||||
2. Set **ui-source** to `External`
|
||||
3. Set **ui-index** to the desired URL
|
||||
|
||||
## Contributing
|
||||
|
||||
For developers, after reading through the introduction on this page, head over to our [Getting Started](https://extensions.rancher.io/extensions/next/extensions-getting-started) guide to learn more.
|
||||
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.
|
||||
|
||||
## License
|
||||
|
||||
License
|
||||
=======
|
||||
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
// Enforce conventional commit format
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat', // New features
|
||||
'fix', // Bug fixes
|
||||
'docs', // Documentation changes
|
||||
'style', // Code style changes (formatting, missing semicolons, etc.)
|
||||
'refactor', // Code refactoring
|
||||
'perf', // Performance improvements
|
||||
'test', // Adding or updating tests
|
||||
'build', // Build system or external dependencies
|
||||
'ci', // CI/CD changes
|
||||
'chore', // Other changes that don't modify src or test files
|
||||
'revert', // Reverts a previous commit
|
||||
'wip', // Work in progress
|
||||
'deps', // Dependency updates
|
||||
'security', // Security fixes
|
||||
]
|
||||
],
|
||||
// Allows the scope to be empty. [0] means the rule is disabled.
|
||||
'scope-empty': [0, 'always'],
|
||||
// Allows any string for the scope.
|
||||
'scope-case': [2, 'always', 'kebab-case'],
|
||||
'type-case': [2, 'always', 'lower-case'],
|
||||
'type-empty': [2, 'never'],
|
||||
'subject-case': [0, 'never'],
|
||||
'subject-empty': [2, 'never'],
|
||||
'subject-full-stop': [2, 'never', '.'],
|
||||
'subject-max-length': [0, 'never'],
|
||||
'body-leading-blank': [2, 'always'],
|
||||
'body-max-line-length': [2, 'always', 100],
|
||||
'footer-leading-blank': [2, 'always'],
|
||||
'footer-max-line-length': [2, 'always', 100],
|
||||
},
|
||||
// Ignore merge commits and revert commits
|
||||
ignores: [
|
||||
(commit) => commit.includes('Merge'),
|
||||
(commit) => commit.includes('Revert'),
|
||||
(commit) => commit.includes('merge'),
|
||||
(commit) => commit.includes('revert'),
|
||||
],
|
||||
};
|
||||
+21
-15
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"name": "harvester-ui-extension",
|
||||
"version": "1.5.1-rc1",
|
||||
"version": "1.7.2",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rancher/shell": "3.0.5-rc.2",
|
||||
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||
"@rancher/shell": "3.0.8-rc.8",
|
||||
"cache-loader": "^4.1.0",
|
||||
"color": "4.2.3",
|
||||
"ip": "2.0.1",
|
||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||
"vue-draggable-next": "^2.2.1",
|
||||
"yaml": "^2.5.1",
|
||||
"@babel/plugin-transform-class-static-block": "7.26.0"
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "~20.10.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"@types/node": "~20.19.0",
|
||||
"cronstrue": "2.59.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.10",
|
||||
"follow-redirects": "1.15.6",
|
||||
"follow-redirects": "1.16.0",
|
||||
"glob": "7.2.3",
|
||||
"glob-parent": "6.0.2",
|
||||
"json5": "2.2.3",
|
||||
"@types/lodash": "4.17.5",
|
||||
"@types/lodash": "4.17.24",
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.3.1",
|
||||
"node-forge": "1.4.0",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.11.1",
|
||||
"roarr": "7.0.4",
|
||||
"semver": "7.5.4",
|
||||
"qs": "6.15.3",
|
||||
"roarr": "7.21.6",
|
||||
"semver": "7.8.5",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -41,10 +41,16 @@
|
||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name"
|
||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||
"commitlint": "commitlint --edit",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/load": "^19.8.1",
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^7.1.0"
|
||||
"eslint-plugin-promise": "^7.1.0",
|
||||
"husky": "^9.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
# harvester-ui-extension
|
||||
|
||||
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
||||
|
||||
> **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.
|
||||
|
||||
## Installation
|
||||
|
||||
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||
|
||||
|
||||
## Development Setup
|
||||
|
||||
Ensure **Node.js v20 or later** is installed for development and debugging.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Run the extension standalone with hot reload at `https://localhost:8005`.
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
# Start the development server
|
||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||
|
||||
# Example with specific server version
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
||||
```
|
||||
|
||||
You may also define environment variables in a `.env` file:
|
||||
|
||||
```env
|
||||
RANCHER_ENV=harvester
|
||||
VUE_APP_SERVER_VERSION=v1.5.0
|
||||
API=https://192.168.1.123
|
||||
```
|
||||
|
||||
### Rancher Integration Mode
|
||||
|
||||
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||
|
||||
```bash
|
||||
API=https://your-rancher-ip yarn dev
|
||||
```
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
All commit messages must follow the conventional commit format:
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
### Supported Types
|
||||
|
||||
- **feat**: New features
|
||||
- **fix**: Bug fixes
|
||||
- **docs**: Documentation changes
|
||||
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
||||
- **refactor**: Code refactoring
|
||||
- **perf**: Performance improvements
|
||||
- **test**: Adding or updating tests
|
||||
- **build**: Build system or external dependencies
|
||||
- **ci**: CI/CD changes
|
||||
- **chore**: Other changes that don't modify src or test files
|
||||
- **revert**: Reverts a previous commit
|
||||
- **wip**: Work in progress
|
||||
- **deps**: Dependency updates
|
||||
- **security**: Security fixes
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Feature
|
||||
git commit -m "feat: add new virtual machine creation wizard"
|
||||
|
||||
# Bug fix
|
||||
git commit -m "fix: resolve memory leak in VM console"
|
||||
|
||||
# Documentation
|
||||
git commit -m "docs: update installation instructions"
|
||||
|
||||
# Breaking change
|
||||
git commit -m "feat!: change API endpoint structure
|
||||
|
||||
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
||||
```
|
||||
|
||||
### Git Hooks
|
||||
|
||||
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
||||
|
||||
- **pre-commit**: Runs ESLint to ensure code quality
|
||||
- **commit-msg**: Validates commit message format using commit-lint
|
||||
|
||||
These hooks are automatically installed when you run `yarn install`.
|
||||
|
||||
### Manual Validation
|
||||
|
||||
You can manually validate commit messages:
|
||||
|
||||
```bash
|
||||
# Validate the last commit
|
||||
yarn commitlint
|
||||
|
||||
# Validate a specific commit
|
||||
npx commitlint --from <commit-hash>
|
||||
|
||||
# Validate a range of commits
|
||||
npx commitlint --from <start-hash> --to <end-hash>
|
||||
```
|
||||
|
||||
## Branch Structure
|
||||
|
||||
- **`main`** – Main development branch
|
||||
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
||||
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
||||
|
||||
> **Note:**
|
||||
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### UI Extension Testing
|
||||
|
||||
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
||||
|
||||
- Examples:
|
||||
- Test `1.0.x` series → `v1.0-head`
|
||||
- Test `1.5.x` series → `v1.5-head`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
||||
3. Go to the **Extensions** page and install the desired version
|
||||
|
||||
### Standalone Mode Testing
|
||||
|
||||
To test the standalone UI, configure Harvester to load the UI from an external source.
|
||||
|
||||
- Examples of `ui-index`:
|
||||
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
||||
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
||||
|
||||
**Steps:**
|
||||
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
||||
2. Set **ui-source** to `External`
|
||||
3. Set **ui-index** to the desired URL
|
||||
|
||||
## 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.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,14 +1,16 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ActionMenu from '@shell/components/ActionMenuShell';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||
import { DOC } from '../config/doc-links';
|
||||
import { docLink } from '../utils/feature-flags';
|
||||
|
||||
const CATEGORY = {
|
||||
ui: [
|
||||
'branding',
|
||||
'ui-source',
|
||||
'ui-plugin-index',
|
||||
'ui-index',
|
||||
]
|
||||
};
|
||||
@@ -19,6 +21,7 @@ export default {
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
ActionMenu
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -30,15 +33,23 @@ export default {
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
searchQuery: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const categorySettings = this.filterCategorySettings();
|
||||
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
|
||||
|
||||
return {
|
||||
HCI_SETTING,
|
||||
categorySettings,
|
||||
filteredSettings,
|
||||
originalHideMap: this.createHideMap(categorySettings)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -48,12 +59,85 @@ export default {
|
||||
settings: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this['categorySettings'] = this.filterCategorySettings();
|
||||
this.categorySettings = this.filterCategorySettings();
|
||||
this.filteredSettings = this.filterSearchSettings(this.categorySettings, this.searchQuery);
|
||||
}
|
||||
},
|
||||
searchQuery: {
|
||||
immediate: true,
|
||||
handler(newQuery) {
|
||||
const filtered = this.filterSearchSettings(this.categorySettings, newQuery);
|
||||
|
||||
this.filteredSettings = newQuery ? this.openJsonSettings(filtered) : filtered.map((s) => ({ ...s, hide: this.originalHideMap[s.id] ?? false }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
createHideMap(settings = []) {
|
||||
const map = settings.reduce((acc, s) => {
|
||||
acc[s.id] = s.hide ?? false;
|
||||
|
||||
return acc;
|
||||
}, {} );
|
||||
|
||||
return map;
|
||||
},
|
||||
filterSearchSettings(settings, searchKey) {
|
||||
if (!searchKey) {
|
||||
return this.filterCategorySettings();
|
||||
}
|
||||
const searchQuery = searchKey.toLowerCase();
|
||||
|
||||
return settings.filter((setting) => {
|
||||
const id = setting.id?.toLowerCase() || '';
|
||||
|
||||
// filter by id
|
||||
if (id.includes(searchQuery) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
||||
|
||||
// remove <a> tags from description
|
||||
if (description.includes('<a')) {
|
||||
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
|
||||
}
|
||||
// filter by description
|
||||
if (description.includes(searchQuery)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// filter by customized value
|
||||
if (setting.customized === true && setting.data?.value) {
|
||||
const value = setting.data.value?.toLowerCase() || '';
|
||||
|
||||
return value.includes(searchQuery);
|
||||
}
|
||||
|
||||
// filter by json value
|
||||
if (setting.kind === 'json' && setting.json) {
|
||||
try {
|
||||
const json = JSON.parse(setting.json);
|
||||
const jsonString = JSON.stringify(json).toLowerCase();
|
||||
|
||||
return jsonString.includes(searchQuery);
|
||||
} catch (e) {
|
||||
console.error(`${ setting.id }: wrong format`, e); // eslint-disable-line no-console
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// filter by default value
|
||||
if (setting.data?.default) {
|
||||
return setting.data?.default.includes(searchQuery);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
filterCategorySettings() {
|
||||
return this.settings.filter((s) => {
|
||||
if (!this.getFeatureEnabled(s.featureFlag)) {
|
||||
@@ -74,25 +158,21 @@ export default {
|
||||
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
|
||||
},
|
||||
|
||||
showActionMenu(e, setting) {
|
||||
const actionElement = e.srcElement;
|
||||
|
||||
this.$store.commit(`action-menu/show`, {
|
||||
resources: setting.data,
|
||||
elem: actionElement
|
||||
});
|
||||
},
|
||||
|
||||
getSettingOption(id) {
|
||||
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
|
||||
},
|
||||
|
||||
openJsonSettings(settings) {
|
||||
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
|
||||
},
|
||||
|
||||
toggleHide(s) {
|
||||
this.categorySettings.find((setting) => {
|
||||
if (setting.id === s.id) {
|
||||
setting.hide = !setting.hide;
|
||||
}
|
||||
});
|
||||
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
|
||||
|
||||
if (setting) {
|
||||
setting.hide = !setting.hide;
|
||||
this.originalHideMap[setting.id] = setting.hide;
|
||||
}
|
||||
},
|
||||
|
||||
async testConnect(buttonDone, value) {
|
||||
@@ -118,6 +198,19 @@ export default {
|
||||
}
|
||||
buttonDone(false);
|
||||
}
|
||||
},
|
||||
|
||||
getDocLinkParams(setting) {
|
||||
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
|
||||
|
||||
if (settingConfig?.docPath) {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
const url = docLink(DOC[settingConfig.docPath], version);
|
||||
|
||||
return { url };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -126,7 +219,7 @@ export default {
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(setting, i) in categorySettings"
|
||||
v-for="(setting, i) in filteredSettings"
|
||||
:key="i"
|
||||
class="advanced-setting mb-20"
|
||||
>
|
||||
@@ -148,7 +241,7 @@ export default {
|
||||
Experimental
|
||||
</span>
|
||||
</h1>
|
||||
<h2 v-clean-html="t(setting.description, {}, true)">
|
||||
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
@@ -156,15 +249,12 @@ export default {
|
||||
:id="setting.id"
|
||||
class="action"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
type="button"
|
||||
class="btn btn-sm role-multi-action actions"
|
||||
@click="showActionMenu($event, setting)"
|
||||
>
|
||||
<i class="icon icon-actions" />
|
||||
</button>
|
||||
<ActionMenu
|
||||
:resource="setting.data"
|
||||
:button-aria-label="t('advancedSettings.edit.label')"
|
||||
data-testid="action-button"
|
||||
button-role="tertiary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div value>
|
||||
@@ -221,6 +311,12 @@ export default {
|
||||
{{ setting.data.errMessage }}
|
||||
</Banner>
|
||||
</div>
|
||||
<div
|
||||
v-if="filteredSettings.length === 0"
|
||||
class="advanced-setting mb-20 no-search-match"
|
||||
>
|
||||
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -271,4 +367,8 @@ export default {
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.no-search-match {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -257,8 +257,12 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
reconnect() {
|
||||
this.$refs.novncConsole.reconnect();
|
||||
},
|
||||
|
||||
softReboot() {
|
||||
this.vmResource.softrebootVM();
|
||||
this.vmResource.doSoftReboot();
|
||||
},
|
||||
|
||||
showKeysModal() {
|
||||
@@ -306,6 +310,13 @@ export default {
|
||||
{{ t("harvester.action.softreboot") }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm bg-primary"
|
||||
@click="reconnect"
|
||||
>
|
||||
{{ t("harvester.action.reconnect") }}
|
||||
</button>
|
||||
|
||||
<v-dropdown
|
||||
v-if="!hideCustomKeysBar"
|
||||
ref="customKeyPopover"
|
||||
@@ -372,3 +383,15 @@ export default {
|
||||
background: rgb(40, 40, 40);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.vm-console .v-popper__arrow-container {
|
||||
display: none;
|
||||
}
|
||||
.vm-console .v-popper__popper{
|
||||
margin-top: 8px;
|
||||
}
|
||||
.vm-console .v-popper__inner{
|
||||
overflow-y: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<script>
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { HCI_SETTING } from '../../config/settings';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { Banner } from '@components/Banner';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditClusterRegistrationURL',
|
||||
|
||||
components: {
|
||||
LabeledInput, MessageLink, Checkbox, Banner
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
parseDefaultValue = JSON.parse(this.value.value);
|
||||
} catch (error) {
|
||||
parseDefaultValue.url = this.value.value;
|
||||
parseDefaultValue.insecureSkipTLSVerify = true;
|
||||
}
|
||||
|
||||
return {
|
||||
parseDefaultValue,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
toCA() {
|
||||
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
|
||||
},
|
||||
clusterRegistrationTLSVerifyEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
|
||||
},
|
||||
registrationURL: {
|
||||
get() {
|
||||
return this.clusterRegistrationTLSVerifyEnabled ? this.parseDefaultValue.url : this.parseDefaultValue;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||
this.parseDefaultValue.url = value;
|
||||
} else {
|
||||
this.parseDefaultValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDefaultValue() {
|
||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||
return { url: '', insecureSkipTLSVerify: false };
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
updateUrl() {
|
||||
this.update();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||
this.value['value'] = JSON.stringify(this.parseDefaultValue);
|
||||
} else {
|
||||
this.value['value'] = this.parseDefaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.parseDefaultValue = this.getDefaultValue();
|
||||
},
|
||||
|
||||
updateInsecureSkipTLSVerify(newValue) {
|
||||
const { url = '' } = this.parseDefaultValue;
|
||||
|
||||
this.parseDefaultValue = { url, insecureSkipTLSVerify: newValue };
|
||||
this.update();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<Banner color="info">
|
||||
<MessageLink
|
||||
:to="toCA"
|
||||
target="_blank"
|
||||
prefix-label="harvester.setting.clusterRegistrationUrl.tip.prefix"
|
||||
middle-label="harvester.setting.clusterRegistrationUrl.tip.middle"
|
||||
suffix-label="harvester.setting.clusterRegistrationUrl.tip.suffix"
|
||||
/>
|
||||
</Banner>
|
||||
<LabeledInput
|
||||
v-model:value="registrationURL"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.clusterRegistrationUrl.url')"
|
||||
@update:value="updateUrl"
|
||||
/>
|
||||
<div v-if="clusterRegistrationTLSVerifyEnabled">
|
||||
<Checkbox
|
||||
v-model:value="parseDefaultValue.insecureSkipTLSVerify"
|
||||
class="check mb-5"
|
||||
type="checkbox"
|
||||
:label="t('harvester.setting.clusterRegistrationUrl.insecureSkipTLSVerify')"
|
||||
@update:value="updateInsecureSkipTLSVerify"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,294 @@
|
||||
<script>
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import InfoBox from '@shell/components/InfoBox';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { CSI_DRIVER } from '../../types';
|
||||
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||
|
||||
export default {
|
||||
name: 'CSIOnlineExpandValidation',
|
||||
|
||||
components: {
|
||||
Banner,
|
||||
InfoBox,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
try {
|
||||
await allHash({ csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) });
|
||||
this.fetchError = null;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch CSI drivers:', error); // eslint-disable-line no-console
|
||||
this.fetchError = this.t(
|
||||
'harvester.setting.csiOnlineExpandValidation.failedToLoadDrivers',
|
||||
{ error: error.message || error },
|
||||
true
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
configArr: [],
|
||||
parseError: null,
|
||||
fetchError: null,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
const initValue = this.value.value || this.value.default || '{}';
|
||||
|
||||
this.configArr = this.parseValue(initValue);
|
||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||
},
|
||||
|
||||
computed: {
|
||||
allErrors() {
|
||||
const errors = [];
|
||||
|
||||
if (this.fetchError) {
|
||||
errors.push(this.fetchError);
|
||||
}
|
||||
if (this.parseError) {
|
||||
errors.push(this.parseError);
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
csiDrivers() {
|
||||
if (this.fetchError) return [];
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
||||
},
|
||||
|
||||
provisioners() {
|
||||
const usedKeys = this.configArr.map(({ key }) => key);
|
||||
|
||||
return this.csiDrivers
|
||||
.filter(({ name }) => !usedKeys.includes(name))
|
||||
.map(({ name }) => name);
|
||||
},
|
||||
|
||||
provisionerValue() {
|
||||
return [
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false },
|
||||
];
|
||||
},
|
||||
|
||||
disableAdd() {
|
||||
return this.parseError || this.fetchError || this.configArr.length >= this.csiDrivers.length;
|
||||
},
|
||||
|
||||
disableConfigEditing() {
|
||||
return this.parseError || this.fetchError;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.value'(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.configArr = this.parseValue(newVal || '{}');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
_convertToBoolean(value) {
|
||||
if (typeof value === 'boolean') return value;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const lowerCaseValue = value.toLowerCase();
|
||||
|
||||
if (lowerCaseValue === 'true') return true;
|
||||
if (lowerCaseValue === 'false') return false;
|
||||
}
|
||||
|
||||
return false; // default to false for any other string or non-boolean type
|
||||
},
|
||||
parseValue(raw) {
|
||||
try {
|
||||
const json = JSON.parse(raw);
|
||||
|
||||
this.parseError = null;
|
||||
|
||||
return Object.entries(json).map(([key, value]) => ({
|
||||
key,
|
||||
value: this._convertToBoolean(value),
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('[CSIOnlineExpandValidation] JSON Parsing Error:', raw, e); // eslint-disable-line no-console
|
||||
this.parseError = this.t(
|
||||
'harvester.setting.csiOnlineExpandValidation.invalidJsonFormat',
|
||||
{ error: e.message },
|
||||
true
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
stringifyConfig() {
|
||||
const obj = {};
|
||||
|
||||
this.configArr.forEach(({ key, value }) => {
|
||||
obj[key] = value;
|
||||
});
|
||||
|
||||
return this.configArr.length ? JSON.stringify(obj) : '';
|
||||
},
|
||||
|
||||
update() {
|
||||
this.value.value = this.stringifyConfig();
|
||||
},
|
||||
|
||||
willSave() {
|
||||
const errors = [];
|
||||
|
||||
this.configArr.forEach(({ key }) => {
|
||||
if (!key) {
|
||||
errors.push(
|
||||
this.t('validation.required', { key: this.t('harvester.setting.csiOnlineExpandValidation.provisioner') }, true)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.value.value = this.stringifyConfig();
|
||||
|
||||
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.configArr = this.parseValue(this.value.default || '{}');
|
||||
this.update();
|
||||
},
|
||||
|
||||
disableEdit(driverKey) {
|
||||
return this.fetchError || driverKey === LONGHORN_DRIVER;
|
||||
},
|
||||
|
||||
add() {
|
||||
if (this.disableConfigEditing) return;
|
||||
|
||||
this.configArr.push({ key: '', value: true });
|
||||
},
|
||||
|
||||
remove(index) {
|
||||
if (this.disableConfigEditing) return;
|
||||
|
||||
this.configArr.splice(index, 1);
|
||||
this.update();
|
||||
},
|
||||
|
||||
onValueChange(idx, newVal) {
|
||||
if (this.disableConfigEditing) return;
|
||||
|
||||
const val = newVal === 'true' ? true : newVal === 'false' ? false : newVal;
|
||||
|
||||
this.configArr[idx].value = val;
|
||||
this.update();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Banner
|
||||
v-for="(errorMsg, index) in allErrors"
|
||||
:key="index"
|
||||
color="error"
|
||||
>
|
||||
{{ errorMsg }}
|
||||
</Banner>
|
||||
<InfoBox
|
||||
v-for="(driver, idx) in configArr"
|
||||
:key="idx"
|
||||
class="box"
|
||||
>
|
||||
<button
|
||||
class="role-link btn btn-sm remove"
|
||||
type="button"
|
||||
:disabled="disableEdit(driver.key)"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="driver.key"
|
||||
label-key="harvester.setting.csiOnlineExpandValidation.provisioner"
|
||||
required
|
||||
searchable
|
||||
:mode="mode"
|
||||
:disabled="disableEdit(driver.key)"
|
||||
:options="provisioners"
|
||||
@update:value="update"
|
||||
@keydown.native.enter.prevent
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="driver.value"
|
||||
:value="driver.value.toString()"
|
||||
label-key="harvester.setting.csiOnlineExpandValidation.value"
|
||||
required
|
||||
searchable
|
||||
:mode="mode"
|
||||
:disabled="disableEdit(driver.key)"
|
||||
:options="provisionerValue"
|
||||
@update:value="val => onValueChange(idx, val)"
|
||||
@keydown.native.enter.prevent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoBox>
|
||||
|
||||
<button
|
||||
class="btn btn-sm role-primary"
|
||||
:disabled="disableAdd"
|
||||
@click="add"
|
||||
>
|
||||
{{ t('generic.add') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
position: relative;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.remove {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
|
||||
import { Banner } from '@components/Banner';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
@@ -62,7 +61,7 @@ export default {
|
||||
|
||||
<div
|
||||
class="row"
|
||||
@update:value="update"
|
||||
@input="update"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<LabeledInput
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<script>
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { MEBIBYTE } from '../../utils/unit';
|
||||
import { Banner } from '@components/Banner';
|
||||
|
||||
export default {
|
||||
name: 'KubevirtMigration',
|
||||
|
||||
components: {
|
||||
LabeledInput,
|
||||
UnitInput,
|
||||
RadioGroup,
|
||||
Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
value: '',
|
||||
default: '{}'
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
||||
|
||||
return {
|
||||
MEBIBYTE,
|
||||
migration,
|
||||
parseError: null,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||
},
|
||||
|
||||
methods: {
|
||||
parseJSON(string) {
|
||||
try {
|
||||
return JSON.parse(string);
|
||||
} catch (e) {
|
||||
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to parse JSON:', e.message);
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
updateValue() {
|
||||
if (this.value) {
|
||||
this.value.value = JSON.stringify(this.migration);
|
||||
}
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
if (this.value?.default) {
|
||||
const defaultMigration = this.parseJSON(this.value.default) || {};
|
||||
|
||||
this.migration = defaultMigration;
|
||||
this.updateValue();
|
||||
}
|
||||
},
|
||||
|
||||
async willSave() {
|
||||
this.updateValue();
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Banner
|
||||
v-if="parseError"
|
||||
color="error"
|
||||
>
|
||||
{{ parseError }}
|
||||
</Banner>
|
||||
|
||||
<div class="migration-field">
|
||||
<LabeledInput
|
||||
v-model:value.number="migration.parallelMigrationsPerCluster"
|
||||
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
|
||||
type="number"
|
||||
min="1"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
|
||||
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
|
||||
type="number"
|
||||
min="1"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="migration.bandwidthPerMigration"
|
||||
min="0"
|
||||
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
|
||||
:mode="mode"
|
||||
:suffix="MEBIBYTE"
|
||||
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="migration.completionTimeoutPerGiB"
|
||||
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
|
||||
:mode="mode"
|
||||
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
|
||||
min="10"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="migration.progressTimeout"
|
||||
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
|
||||
:mode="mode"
|
||||
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
|
||||
min="10"
|
||||
/>
|
||||
<div
|
||||
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
|
||||
:key="field"
|
||||
>
|
||||
<label
|
||||
class="mb-5"
|
||||
:for="field"
|
||||
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
|
||||
<RadioGroup
|
||||
:id="field"
|
||||
v-model:value="migration[field]"
|
||||
:options="[
|
||||
{ label: t('advancedSettings.edit.trueOption'), value: true },
|
||||
{ label: t('advancedSettings.edit.falseOption'), value: false },
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.migration-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { SECRET } from '@shell/config/types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
|
||||
import YamlEditor from '@shell/components/YamlEditor';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterRancherCluster',
|
||||
|
||||
components: {
|
||||
RadioGroup,
|
||||
FileSelector,
|
||||
YamlEditor
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
const data = this.value.value || this.value.default || '{}';
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
parseDefaultValue = {
|
||||
kubeConfig: '',
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: parsed.removeUpstreamClusterWhenNamespaceIsDeleted || false
|
||||
};
|
||||
} catch (error) {
|
||||
parseDefaultValue = {
|
||||
kubeConfig: '',
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
parseDefaultValue,
|
||||
errors: [],
|
||||
existingSecret: null,
|
||||
};
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.checkExistingSecret();
|
||||
if (this.registerBeforeHook) {
|
||||
this.registerBeforeHook(this.willSave, 'willSave');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeySelected: createOnSelected('parseDefaultValue.kubeConfig'),
|
||||
|
||||
update() {
|
||||
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||
this.value['value'] = JSON.stringify({ removeUpstreamClusterWhenNamespaceIsDeleted: true });
|
||||
} else {
|
||||
this.value['value'] = this.value.default || '{}';
|
||||
}
|
||||
},
|
||||
|
||||
async checkExistingSecret() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||
const secrets = this.$store.getters[`${ inStore }/all`](SECRET) || [];
|
||||
|
||||
this.existingSecret = secrets.find((secret) => secret.metadata.name === 'rancher-cluster-config' &&
|
||||
secret.metadata.namespace === 'harvester-system'
|
||||
);
|
||||
|
||||
// If the secret exists and has data, populate the kubeConfig
|
||||
if (this.existingSecret?.data?.kubeConfig) {
|
||||
const decodedContent = atob(this.existingSecret.data.kubeConfig);
|
||||
|
||||
this.parseDefaultValue.kubeConfig = decodedContent;
|
||||
this.$nextTick(() => {
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async createOrUpdateRancherKubeConfigSecret() {
|
||||
this.errors = [];
|
||||
// Check if kubeConfig is provided
|
||||
if (!this.parseDefaultValue.kubeConfig) {
|
||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.setting.rancherCluster.kubeConfig') }, true));
|
||||
|
||||
return Promise.reject(this.errors);
|
||||
}
|
||||
|
||||
try {
|
||||
let secret;
|
||||
|
||||
if (this.existingSecret) {
|
||||
secret = this.existingSecret;
|
||||
secret.setData('kubeConfig', this.parseDefaultValue.kubeConfig);
|
||||
} else {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
secret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
metadata: {
|
||||
name: 'rancher-cluster-config',
|
||||
namespace: 'harvester-system'
|
||||
},
|
||||
type: 'secret',
|
||||
data: { kubeConfig: btoa(this.parseDefaultValue.kubeConfig) }
|
||||
});
|
||||
}
|
||||
await secret.save();
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
|
||||
return Promise.reject(this.errors);
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRancherKubeConfigSecret() {
|
||||
if (this.existingSecret) {
|
||||
this.existingSecret.remove();
|
||||
}
|
||||
},
|
||||
|
||||
async willSave() {
|
||||
// Only create or update secret if enabled
|
||||
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||
await this.createOrUpdateRancherKubeConfigSecret();
|
||||
} else {
|
||||
await this.deleteRancherKubeConfigSecret();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.parseDefaultValue = {
|
||||
kubeConfig: '',
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted'(val, oldVal) {
|
||||
if (val && !oldVal && this.existingSecret?.data?.kubeConfig) {
|
||||
// Populate kubeConfig with the existing secret value
|
||||
this.parseDefaultValue.kubeConfig = atob(this.existingSecret.data.kubeConfig);
|
||||
}
|
||||
},
|
||||
'parseDefaultValue.kubeConfig'(val) {
|
||||
this.$refs.yaml?.updateValue(val);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-12">
|
||||
<RadioGroup
|
||||
v-model:value="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||
:label="t('harvester.setting.rancherCluster.removeUpstreamClusterWhenNamespaceIsDeleted')"
|
||||
name="removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||
:options="[true, false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||
class="row mt-20"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<FileSelector
|
||||
class="btn btn-sm bg-primary mb-10"
|
||||
:label="t('generic.readFromFile')"
|
||||
@selected="onKeySelected"
|
||||
/>
|
||||
<YamlEditor
|
||||
ref="yaml"
|
||||
v-model:value="parseDefaultValue.kubeConfig"
|
||||
class="yaml-editor"
|
||||
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$yaml-height: 540px;
|
||||
|
||||
:deep() .yaml-editor{
|
||||
flex: 1;
|
||||
min-height: $yaml-height;
|
||||
& .code-mirror .CodeMirror {
|
||||
position: initial;
|
||||
height: auto;
|
||||
min-height: $yaml-height;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,9 @@ import { NAMESPACE } from '@shell/config/types';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
const SELECT_ALL = 'select_all';
|
||||
const UNSELECT_ALL = 'unselect_all';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterBundleNamespaces',
|
||||
|
||||
@@ -11,47 +14,92 @@ export default {
|
||||
mixins: [CreateEditView],
|
||||
|
||||
async fetch() {
|
||||
this.loading = true;
|
||||
|
||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||
|
||||
if (this.customSupportBundleFeatureEnabled) {
|
||||
try {
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||
const response = await this.$store.dispatch('harvester/request', { url });
|
||||
|
||||
this.defaultNamespaces = response.data || [];
|
||||
} catch (error) {
|
||||
this.defaultNamespaces = [];
|
||||
}
|
||||
} else {
|
||||
this.defaultNamespaces = [];
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
data() {
|
||||
let namespaces = [];
|
||||
const namespacesStr = this.value?.value || this.value?.default || '';
|
||||
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
|
||||
|
||||
if (namespacesStr) {
|
||||
namespaces = namespacesStr.split(',');
|
||||
}
|
||||
|
||||
return { namespaces };
|
||||
return {
|
||||
namespaces,
|
||||
defaultNamespaces: [],
|
||||
loading: true
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
customSupportBundleFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||
},
|
||||
|
||||
allNamespaces() {
|
||||
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||
},
|
||||
|
||||
filteredNamespaces() {
|
||||
if (!this.customSupportBundleFeatureEnabled) {
|
||||
return this.allNamespaces;
|
||||
}
|
||||
|
||||
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||
|
||||
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
||||
},
|
||||
|
||||
namespaceOptions() {
|
||||
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
|
||||
return {
|
||||
label: N.id,
|
||||
value: N.id
|
||||
};
|
||||
});
|
||||
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
|
||||
|
||||
if (!this.customSupportBundleFeatureEnabled) {
|
||||
return mappedNamespaces;
|
||||
}
|
||||
|
||||
const allSelected =
|
||||
this.namespaces.length === this.filteredNamespaces.length &&
|
||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||
|
||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||
|
||||
return [controlOption, ...mappedNamespaces];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
update() {
|
||||
const namespaceStr = this.namespaces.join(',');
|
||||
update(selected) {
|
||||
if (selected.includes(SELECT_ALL)) {
|
||||
this.namespaces = [...this.filteredNamespaces];
|
||||
} else if (selected.includes(UNSELECT_ALL)) {
|
||||
this.namespaces = [];
|
||||
} else {
|
||||
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||
}
|
||||
|
||||
this.value['value'] = namespaceStr;
|
||||
this.value.value = this.namespaces.join(',');
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.value': {
|
||||
handler(neu) {
|
||||
if (neu === this.value.default || !neu) {
|
||||
this.namespaces = [];
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
'value.value'(newVal) {
|
||||
const raw = newVal || this.value.default || '';
|
||||
|
||||
this.namespaces = raw ? raw.split(',') : [];
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -62,6 +110,7 @@ export default {
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="namespaces"
|
||||
:loading="loading"
|
||||
:multiple="true"
|
||||
label-key="nameNsDescription.namespace.label"
|
||||
:mode="mode"
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterUpgradeConfig',
|
||||
|
||||
components: {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup
|
||||
},
|
||||
mixins: [CreateEditView],
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
|
||||
} catch (error) {
|
||||
parseDefaultValue = JSON.parse(this.value.default);
|
||||
}
|
||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||
|
||||
return {
|
||||
parseDefaultValue,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
strategyOptions() {
|
||||
return [
|
||||
{ value: 'sequential', label: 'sequential' },
|
||||
{ value: 'skip', label: 'skip' },
|
||||
{ value: 'parallel', label: 'parallel' }
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.update();
|
||||
},
|
||||
|
||||
methods: {
|
||||
normalizeValue(obj) {
|
||||
if (!obj.imagePreloadOption) {
|
||||
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
||||
}
|
||||
if (!obj.imagePreloadOption.strategy) {
|
||||
obj.imagePreloadOption.strategy = { type: 'sequential' };
|
||||
}
|
||||
if (!obj.imagePreloadOption.strategy.type) {
|
||||
obj.imagePreloadOption.strategy.type = 'sequential';
|
||||
}
|
||||
// Only set concurrency if type is 'parallel'
|
||||
if (obj.imagePreloadOption.strategy.type === 'parallel') {
|
||||
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
|
||||
obj.imagePreloadOption.strategy.concurrency = 0;
|
||||
}
|
||||
} else {
|
||||
delete obj.imagePreloadOption.strategy.concurrency;
|
||||
}
|
||||
if (typeof obj.restoreVM !== 'boolean') {
|
||||
obj.restoreVM = false;
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
update() {
|
||||
try {
|
||||
// Clone to avoid mutating the form state
|
||||
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
|
||||
|
||||
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
|
||||
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
|
||||
delete valueToSave.imagePreloadOption.strategy.concurrency;
|
||||
}
|
||||
}
|
||||
|
||||
this.value['value'] = JSON.stringify(valueToSave, null, 2);
|
||||
this.errors = [];
|
||||
} catch (e) {
|
||||
this.errors = ['Invalid JSON'];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: {
|
||||
handler(neu) {
|
||||
let parseDefaultValue;
|
||||
|
||||
try {
|
||||
parseDefaultValue = JSON.parse(neu.value);
|
||||
} catch (err) {
|
||||
parseDefaultValue = JSON.parse(this.value.default);
|
||||
}
|
||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||
this['parseDefaultValue'] = parseDefaultValue;
|
||||
this.update();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="row"
|
||||
@input="update"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
|
||||
<LabeledSelect
|
||||
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.upgrade.strategyType')"
|
||||
:options="strategyOptions"
|
||||
@update:value="update"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
|
||||
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.upgrade.concurrency')"
|
||||
min="0"
|
||||
type="number"
|
||||
/>
|
||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
|
||||
<RadioGroup
|
||||
v-model:value="parseDefaultValue.restoreVM"
|
||||
class="mb-20"
|
||||
name="restoreVM"
|
||||
:options="[true, false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="update"
|
||||
/>
|
||||
<div
|
||||
v-if="errors.length"
|
||||
class="error"
|
||||
>
|
||||
{{ errors[0] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error {
|
||||
color: #d9534f;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,270 @@
|
||||
<script>
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { Banner } from '@components/Banner';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||
import { NODE } from '@shell/config/types';
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import { HCI } from '../../types';
|
||||
|
||||
const DEFAULT_NETWORK = {
|
||||
clusterNetwork: '',
|
||||
vlan: '',
|
||||
range: '',
|
||||
exclude: [],
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'VMMigrationNetwork',
|
||||
|
||||
components: {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup,
|
||||
Banner,
|
||||
ArrayList,
|
||||
},
|
||||
|
||||
props: {
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({ value: '' }),
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
try {
|
||||
await allHash({
|
||||
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||
});
|
||||
|
||||
this.fetchError = null;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
|
||||
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const { parsed, enabled, parseError } = this.parseInitialValue();
|
||||
|
||||
return {
|
||||
enabled,
|
||||
network: { ...DEFAULT_NETWORK, ...parsed },
|
||||
fetchError: null,
|
||||
parseError,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||
},
|
||||
|
||||
computed: {
|
||||
allErrors() {
|
||||
return [this.fetchError, this.parseError].filter(Boolean);
|
||||
},
|
||||
|
||||
clusterNetworkOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||
|
||||
return networks.map((net) => ({
|
||||
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
|
||||
value: net.id,
|
||||
disabled: !net.isReadyForStorageNetwork,
|
||||
}));
|
||||
},
|
||||
|
||||
disableEdit() {
|
||||
return !!(this.fetchError || this.parseError);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
parseInitialValue() {
|
||||
let parsed = {};
|
||||
let enabled = false;
|
||||
let parseError = null;
|
||||
|
||||
try {
|
||||
if (typeof this.value.value === 'string' && this.value.value.trim()) {
|
||||
parsed = JSON.parse(this.value.value);
|
||||
enabled = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
|
||||
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed.exclude)) {
|
||||
parsed.exclude = [];
|
||||
}
|
||||
|
||||
return {
|
||||
parsed, enabled, parseError
|
||||
};
|
||||
},
|
||||
|
||||
clearErrors() {
|
||||
this.fetchError = null;
|
||||
this.parseError = null;
|
||||
},
|
||||
|
||||
inputVlan(val) {
|
||||
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
|
||||
this.update();
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.network = { ...DEFAULT_NETWORK };
|
||||
this.value.value = '';
|
||||
this.enabled = false;
|
||||
this.clearErrors();
|
||||
},
|
||||
|
||||
update() {
|
||||
try {
|
||||
this.value.value = this.enabled ? JSON.stringify({
|
||||
...this.network,
|
||||
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
|
||||
}) : '';
|
||||
} catch (e) {
|
||||
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
|
||||
this.value.value = '';
|
||||
}
|
||||
},
|
||||
|
||||
validateInputs() {
|
||||
const errors = [];
|
||||
|
||||
if (!this.network.clusterNetwork) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
|
||||
}
|
||||
|
||||
if (!this.network.range) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
|
||||
} else if (!isValidCIDR(this.network.range)) {
|
||||
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
|
||||
}
|
||||
|
||||
if (this.network.vlan === '') {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
|
||||
} else {
|
||||
const vlan = Number(this.network.vlan);
|
||||
|
||||
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
|
||||
errors.push(this.t('validation.between', {
|
||||
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
|
||||
min: 1,
|
||||
max: 4094,
|
||||
}, true));
|
||||
}
|
||||
}
|
||||
|
||||
for (const cidr of this.network.exclude || []) {
|
||||
if (cidr && !isValidCIDR(cidr)) {
|
||||
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
async willSave() {
|
||||
if (!this.enabled) {
|
||||
this.useDefault();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.update();
|
||||
|
||||
const errors = this.validateInputs();
|
||||
|
||||
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Banner
|
||||
v-for="(errorMsg, index) in allErrors"
|
||||
:key="index"
|
||||
color="error"
|
||||
>
|
||||
{{ errorMsg }}
|
||||
</Banner>
|
||||
<RadioGroup
|
||||
v-model:value="enabled"
|
||||
class="mb-20"
|
||||
name="enableMigrationNetwork"
|
||||
:options="[true, false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="update"
|
||||
/>
|
||||
<template v-if="enabled">
|
||||
<LabeledSelect
|
||||
v-model:value="network.clusterNetwork"
|
||||
required
|
||||
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:options="clusterNetworkOptions"
|
||||
:disabled="disableEdit"
|
||||
@update:value="update"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value.number="network.vlan"
|
||||
required
|
||||
type="number"
|
||||
class="mb-20"
|
||||
:min="1"
|
||||
:max="4094"
|
||||
:mode="mode"
|
||||
placeholder="e.g. 1 - 4094"
|
||||
label-key="harvester.setting.vmMigrationNetwork.vlan"
|
||||
:disabled="disableEdit"
|
||||
@update:value="inputVlan"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="network.range"
|
||||
required
|
||||
class="mb-5"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
|
||||
label-key="harvester.setting.vmMigrationNetwork.range.label"
|
||||
:disabled="disableEdit"
|
||||
@update:value="update"
|
||||
/>
|
||||
<ArrayList
|
||||
v-model:value="network.exclude"
|
||||
:show-header="true"
|
||||
:default-add-value="''"
|
||||
:mode="mode"
|
||||
:add-disabled="disableEdit"
|
||||
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
|
||||
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
|
||||
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||
@update:value="update"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,8 +1,13 @@
|
||||
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
||||
export const DOC = {
|
||||
CONSOLE_URL: `/host/#remote-console`,
|
||||
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||
UPGRADE_URL: `/upgrade/index`
|
||||
CONSOLE_URL: `/host/#remote-console`,
|
||||
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||
UPGRADE_URL: `/upgrade/index`,
|
||||
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
|
||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
||||
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
|
||||
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
|
||||
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const FEATURE_FLAGS = {
|
||||
'vmSnapshotQuota',
|
||||
'longhornV2LVMSupport',
|
||||
'improveMaintenanceMode',
|
||||
'upgradeConfigSetting'
|
||||
],
|
||||
'v1.4.1': [],
|
||||
'v1.4.2': [
|
||||
@@ -33,9 +34,32 @@ const FEATURE_FLAGS = {
|
||||
'efiPersistentState',
|
||||
'untaggedNetworkSetting',
|
||||
'skipSingleReplicaDetachedVol',
|
||||
'thirdPartyStorage'
|
||||
'thirdPartyStorage',
|
||||
'liveMigrationProgress'
|
||||
],
|
||||
'v1.5.1': [],
|
||||
'v1.6.0': [
|
||||
'customSupportBundle',
|
||||
'csiOnlineExpandValidation',
|
||||
'vmNetworkMigration',
|
||||
'kubeovnVpcSubnet',
|
||||
'rancherClusterSetting',
|
||||
'cpuMemoryHotplug',
|
||||
'cdiSettings',
|
||||
'vmCloneRunStrategy',
|
||||
],
|
||||
'v1.6.1': [],
|
||||
'v1.7.0': [
|
||||
'vmMachineTypeAuto',
|
||||
'lhV2VolExpansion',
|
||||
'l2VlanTrunkMode',
|
||||
'kubevirtMigration',
|
||||
'hotplugNic'
|
||||
],
|
||||
'v1.7.1': [],
|
||||
'v1.7.2': [
|
||||
'clusterRegistrationTLSVerify'
|
||||
],
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
LOGGING,
|
||||
STORAGE_CLASS,
|
||||
SECRET,
|
||||
NETWORK_POLICY
|
||||
} from '@shell/config/types';
|
||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||
import {
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
CONFIGURED_PROVIDERS,
|
||||
SUB_TYPE,
|
||||
ADDRESS,
|
||||
DESCRIPTION,
|
||||
} from '@shell/config/table-headers';
|
||||
import { IF_HAVE } from '@shell/store/type-map';
|
||||
import {
|
||||
@@ -32,7 +34,8 @@ import {
|
||||
IMAGE_PROGRESS,
|
||||
SNAPSHOT_TARGET_VOLUME,
|
||||
IMAGE_VIRTUAL_SIZE,
|
||||
IMAGE_STORAGE_CLASS
|
||||
IMAGE_STORAGE_CLASS,
|
||||
HARVESTER_DESCRIPTION
|
||||
} from './table-headers';
|
||||
|
||||
const TEMPLATE = HCI.VM_VERSION;
|
||||
@@ -222,6 +225,7 @@ export function init($plugin, store) {
|
||||
STATE,
|
||||
NAME_COL,
|
||||
NAMESPACE_COL,
|
||||
HARVESTER_DESCRIPTION,
|
||||
IMAGE_STORAGE_CLASS,
|
||||
IMAGE_PROGRESS,
|
||||
IMAGE_DOWNLOAD_SIZE,
|
||||
@@ -255,7 +259,7 @@ export function init($plugin, store) {
|
||||
});
|
||||
|
||||
if (isSingleVirtualCluster) {
|
||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
|
||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
|
||||
basicType([NAMESPACE]);
|
||||
virtualType({
|
||||
labelKey: 'harvester.namespace.label',
|
||||
@@ -421,6 +425,8 @@ export function init($plugin, store) {
|
||||
[
|
||||
HCI.CLUSTER_NETWORK,
|
||||
HCI.NETWORK_ATTACHMENT,
|
||||
HCI.VPC,
|
||||
NETWORK_POLICY,
|
||||
HCI.LB,
|
||||
HCI.IP_POOL,
|
||||
],
|
||||
@@ -451,6 +457,7 @@ export function init($plugin, store) {
|
||||
HCI.PCI_DEVICE,
|
||||
HCI.SR_IOVGPU_DEVICE,
|
||||
HCI.VGPU_DEVICE,
|
||||
HCI.MIG_CONFIGURATION,
|
||||
HCI.USB_DEVICE,
|
||||
HCI.ADD_ONS,
|
||||
HCI.SECRET,
|
||||
@@ -547,6 +554,36 @@ export function init($plugin, store) {
|
||||
exact: false
|
||||
});
|
||||
|
||||
configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||
|
||||
virtualType({
|
||||
labelKey: 'harvester.vpc.label',
|
||||
name: HCI.VPC,
|
||||
namespaced: true,
|
||||
weight: 187,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VPC }
|
||||
},
|
||||
exact: false,
|
||||
ifHaveType: HCI.VPC,
|
||||
});
|
||||
|
||||
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||
|
||||
virtualType({
|
||||
labelKey: 'harvester.networkPolicy.label',
|
||||
name: NETWORK_POLICY,
|
||||
namespaced: true,
|
||||
weight: 186,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: NETWORK_POLICY }
|
||||
},
|
||||
exact: false,
|
||||
ifHaveType: NETWORK_POLICY,
|
||||
});
|
||||
|
||||
configureType(HCI.SNAPSHOT, {
|
||||
isCreatable: false,
|
||||
location: {
|
||||
@@ -813,6 +850,26 @@ export function init($plugin, store) {
|
||||
]
|
||||
});
|
||||
|
||||
virtualType({
|
||||
labelKey: 'harvester.migconfiguration.label',
|
||||
group: 'advanced',
|
||||
weight: 12,
|
||||
name: HCI.MIG_CONFIGURATION,
|
||||
namespaced: false,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.MIG_CONFIGURATION }
|
||||
},
|
||||
exact: false,
|
||||
ifHaveType: HCI.MIG_CONFIGURATION,
|
||||
});
|
||||
|
||||
configureType(HCI.MIG_CONFIGURATION, {
|
||||
isCreatable: false,
|
||||
hiddenNamespaceGroupButton: true,
|
||||
canYaml: false,
|
||||
});
|
||||
|
||||
virtualType({
|
||||
labelKey: 'harvester.usb.label',
|
||||
group: 'advanced',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { HCI } from '../types';
|
||||
|
||||
// TODO: delete this not used variable
|
||||
export const MemoryUnit = [{
|
||||
label: 'Mi',
|
||||
@@ -70,6 +72,7 @@ export const ADD_ONS = {
|
||||
RANCHER_MONITORING: 'rancher-monitoring',
|
||||
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
||||
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
||||
KUBEOVN_OPERATOR: 'kubeovn-operator',
|
||||
};
|
||||
|
||||
export const CSI_SECRETS = {
|
||||
@@ -80,3 +83,14 @@ export const CSI_SECRETS = {
|
||||
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
||||
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
||||
};
|
||||
|
||||
// Some harvester CRD type is not equal to model file name, define the mapping here
|
||||
export const HARVESTER_CRD_MAP = {
|
||||
node: HCI.HOST,
|
||||
configmap: HCI.CLOUD_TEMPLATE,
|
||||
persistentvolumeclaim: HCI.VOLUME,
|
||||
'snapshot.storage.k8s.io.volumesnapshot': HCI.SNAPSHOT,
|
||||
// specific groupable table detail page
|
||||
'network.harvesterhci.io.vlanconfig': HCI.CLUSTER_NETWORK,
|
||||
'kubeovn.io.subnet': HCI.VPC
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ export const HCI = {
|
||||
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
||||
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
||||
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
||||
UPGRADE: 'harvesterhci.io/upgrade',
|
||||
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
||||
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
||||
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
||||
@@ -49,6 +50,7 @@ export const HCI = {
|
||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
|
||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||
@@ -66,5 +68,14 @@ export const HCI = {
|
||||
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
||||
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
||||
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||
K8S_ARCH: 'kubernetes.io/arch'
|
||||
K8S_ARCH: 'kubernetes.io/arch',
|
||||
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
|
||||
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
||||
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
|
||||
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
|
||||
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
|
||||
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
||||
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
||||
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
||||
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export const CLUSTER_NETWORK = 'clusterNetwork';
|
||||
export const VPC = 'vpc';
|
||||
|
||||
@@ -4,7 +4,6 @@ export const HCI_SETTING = {
|
||||
LOG_LEVEL: 'log-level',
|
||||
SERVER_VERSION: 'server-version',
|
||||
UI_INDEX: 'ui-index',
|
||||
UI_PLUGIN_INDEX: 'ui-plugin-index',
|
||||
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
||||
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
||||
VLAN: 'vlan',
|
||||
@@ -28,12 +27,18 @@ export const HCI_SETTING = {
|
||||
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
||||
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
||||
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
||||
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
|
||||
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
||||
NTP_SERVERS: 'ntp-servers',
|
||||
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',
|
||||
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'
|
||||
};
|
||||
|
||||
export const HCI_ALLOWED_SETTINGS = {
|
||||
@@ -52,18 +57,21 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
from: 'import',
|
||||
featureFlag: 'autoRotateRke2CertsSetting'
|
||||
},
|
||||
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
|
||||
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
|
||||
},
|
||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||
kind: 'multiline', canReset: true, from: 'import'
|
||||
},
|
||||
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||
kind: 'custom', from: 'import', canReset: true
|
||||
@@ -76,7 +84,6 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
|
||||
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
|
||||
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
|
||||
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
|
||||
[HCI_SETTING.CONTAINERD_REGISTRY]: {
|
||||
kind: 'json', from: 'import', canReset: true
|
||||
},
|
||||
@@ -89,18 +96,35 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
[HCI_SETTING.NTP_SERVERS]: {
|
||||
kind: 'json', from: 'import', canReset: true
|
||||
},
|
||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
||||
kind: 'boolean',
|
||||
experimental: true,
|
||||
featureFlag: 'longhornV2LVMSupport'
|
||||
},
|
||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||
[HCI_SETTING.UPGRADE_CONFIG]: {
|
||||
kind: 'json',
|
||||
from: 'import',
|
||||
featureFlag: 'upgradeConfigSetting',
|
||||
docPath: 'UPGRADE_CONFIG_URL'
|
||||
},
|
||||
[HCI_SETTING.RANCHER_CLUSTER]: {
|
||||
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
|
||||
},
|
||||
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
||||
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
||||
},
|
||||
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
||||
}
|
||||
};
|
||||
|
||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
|
||||
kind: 'url',
|
||||
kind: 'custom',
|
||||
from: 'import',
|
||||
canReset: true,
|
||||
},
|
||||
[HCI_SETTING.UI_PL]: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Harvester
|
||||
*/
|
||||
import { DESCRIPTION } from '@shell/config/table-headers';
|
||||
|
||||
// image
|
||||
export const IMAGE_DOWNLOAD_SIZE = {
|
||||
@@ -96,5 +97,37 @@ export const IMAGE_STORAGE_CLASS = {
|
||||
sort: 'imageStorageClass',
|
||||
value: 'imageStorageClass',
|
||||
align: 'left',
|
||||
width: 200,
|
||||
width: 150,
|
||||
};
|
||||
|
||||
export const HARVESTER_DESCRIPTION = {
|
||||
...DESCRIPTION,
|
||||
width: 150,
|
||||
};
|
||||
|
||||
// The CIDR_BLOCK column in VPC list page
|
||||
export const CIDR_BLOCK = {
|
||||
name: 'cidrBlock',
|
||||
labelKey: 'harvester.subnet.cidrBlock.label',
|
||||
sort: 'cidrBlock',
|
||||
value: 'spec.cidrBlock',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// The Protocol column in VPC list page
|
||||
export const PROTOCOL = {
|
||||
name: 'protocol',
|
||||
labelKey: 'harvester.subnet.protocol.label',
|
||||
sort: 'protocol',
|
||||
value: 'spec.protocol',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// The Provider column in VPC list page
|
||||
export const PROVIDER = {
|
||||
name: 'provider',
|
||||
labelKey: 'harvester.subnet.provider.label',
|
||||
sort: 'provider',
|
||||
value: 'spec.provider',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
@@ -4,11 +4,28 @@ export const BACKUP_TYPE = {
|
||||
};
|
||||
|
||||
export const NETWORK_TYPE = {
|
||||
L2VLAN: 'L2VlanNetwork',
|
||||
UNTAGGED: 'UntaggedNetwork'
|
||||
L2VLAN: 'L2VlanNetwork',
|
||||
UNTAGGED: 'UntaggedNetwork',
|
||||
OVERLAY: 'OverlayNetwork',
|
||||
L2TRUNK_VLAN: 'L2VlanTrunkNetwork',
|
||||
};
|
||||
|
||||
export const VOLUME_MODE = {
|
||||
BLOCK: 'Block',
|
||||
FILE_SYSTEM: 'Filesystem'
|
||||
};
|
||||
|
||||
export const NETWORK_PROTOCOL = {
|
||||
IPv4: 'IPv4',
|
||||
IPv6: 'IPv6',
|
||||
};
|
||||
|
||||
export const INTERNAL_STORAGE_CLASS = {
|
||||
VMSTATE_PERSISTENCE: 'vmstate-persistence',
|
||||
LONGHORN_STATIC: 'longhorn-static',
|
||||
};
|
||||
|
||||
export const L2VLAN_MODE = {
|
||||
ACCESS: 'access',
|
||||
TRUNK: 'trunk',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
ResourceTabs,
|
||||
Tab,
|
||||
SortableTable,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
name: 'profileName',
|
||||
labelKey: 'harvester.migconfiguration.tableHeaders.profileName',
|
||||
value: 'name',
|
||||
width: 75,
|
||||
sort: 'name',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
{
|
||||
name: 'vGPUID',
|
||||
labelKey: 'harvester.migconfiguration.tableHeaders.vGPUID',
|
||||
value: 'vGPUID',
|
||||
width: 75,
|
||||
sort: 'vGPUID',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
{
|
||||
name: 'available',
|
||||
labelKey: 'harvester.migconfiguration.tableHeaders.available',
|
||||
value: 'available',
|
||||
width: 75,
|
||||
sort: 'available',
|
||||
align: 'center',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
{
|
||||
name: 'requested',
|
||||
labelKey: 'harvester.migconfiguration.tableHeaders.requested',
|
||||
value: 'requested',
|
||||
width: 75,
|
||||
sort: 'requested',
|
||||
align: 'center',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
{
|
||||
name: 'total',
|
||||
labelKey: 'harvester.migconfiguration.tableHeaders.total',
|
||||
value: 'total',
|
||||
width: 75,
|
||||
sort: 'total',
|
||||
align: 'center',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
rows() {
|
||||
let out = (this.value?.status?.profileStatus || []).map((profile) => {
|
||||
const {
|
||||
id, name, total, available
|
||||
} = profile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
total,
|
||||
available,
|
||||
vGPUID: profile.vGPUID?.join(', ') || '',
|
||||
};
|
||||
});
|
||||
|
||||
out = out.map((row) => {
|
||||
const requested = this.value?.spec?.profileSpec.find((p) => p.id === row.id)?.requested || 0;
|
||||
|
||||
return { ...row, requested };
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ResourceTabs
|
||||
:value="value"
|
||||
:need-events="false"
|
||||
:need-related="false"
|
||||
:mode="mode"
|
||||
>
|
||||
<Tab
|
||||
name="Profile Status"
|
||||
:label="t('harvester.migconfiguration.profileStatus')"
|
||||
>
|
||||
<SortableTable
|
||||
:headers="headers"
|
||||
:rows="rows"
|
||||
key-field="condition"
|
||||
default-sort-by="condition"
|
||||
:table-actions="false"
|
||||
:row-actions="false"
|
||||
:search="false"
|
||||
/>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</template>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script>
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { HCI } from '../../types';
|
||||
import { DOC } from '../../config/doc-links';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHugepages',
|
||||
components: { LabelValue },
|
||||
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
docsTransparentHugepagesLink() {
|
||||
return DOC.TRANSPARENT_HUGEPAGES;
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
||||
|
||||
this.hugepages = hash.find((node) => {
|
||||
return node.id === this.node.id;
|
||||
}) || {};
|
||||
},
|
||||
|
||||
data() {
|
||||
return { hugepages: {} };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="hugepages.status">
|
||||
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.anon')"
|
||||
:value="hugepages.status.meminfo.anonHugePages"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.size')"
|
||||
:value="hugepages.status.meminfo.hugepageSize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.total')"
|
||||
:value="hugepages.status.meminfo.hugePagesTotal"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.free')"
|
||||
:value="hugepages.status.meminfo.hugePagesFree"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.rsvd')"
|
||||
:value="hugepages.status.meminfo.hugePagesRsvd"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.surp')"
|
||||
:value="hugepages.status.meminfo.hugePagesSurp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<hr class="divider" />
|
||||
<h3>
|
||||
<t
|
||||
k="harvester.host.hugepages.transparent.title"
|
||||
:raw="true"
|
||||
:url="docsTransparentHugepagesLink"
|
||||
/>
|
||||
</h3>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.transparent.enabled')"
|
||||
:value="hugepages.spec.transparent.enabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
||||
:value="hugepages.spec.transparent.shmemEnabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.transparent.defrag')"
|
||||
:value="hugepages.spec.transparent.defrag"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -47,20 +47,26 @@ export default {
|
||||
STATE,
|
||||
NAME,
|
||||
{
|
||||
name: 'vmCPU',
|
||||
labelKey: 'tableHeaders.cpu',
|
||||
search: false,
|
||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||
value: 'spec.template.spec.domain.cpu.cores',
|
||||
width: 120
|
||||
name: 'CPU',
|
||||
label: 'CPU',
|
||||
sort: ['displayCPU'],
|
||||
value: 'displayCPU',
|
||||
align: 'center',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
{
|
||||
name: 'vmRAM',
|
||||
labelKey: 'glance.memory',
|
||||
search: false,
|
||||
sort: ['memorySort'],
|
||||
value: 'spec.template.spec.domain.resources.limits.memory',
|
||||
width: 120
|
||||
name: 'Memory',
|
||||
value: 'displayMemory',
|
||||
sort: ['memorySort'],
|
||||
align: 'center',
|
||||
labelKey: 'tableHeaders.memory',
|
||||
formatter: 'Si',
|
||||
formatterOpts: {
|
||||
opts: {
|
||||
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
|
||||
},
|
||||
needParseSi: true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ip',
|
||||
|
||||
@@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
|
||||
import Disk from './HarvesterHostDisk';
|
||||
import VlanStatus from './VlanStatus';
|
||||
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
||||
import HarvesterHugepages from './HarvesterHugepages.vue';
|
||||
import HarvesterSeeder from './HarvesterSeeder';
|
||||
|
||||
const LONGHORN_SYSTEM = 'longhorn-system';
|
||||
@@ -46,6 +47,7 @@ export default {
|
||||
VlanStatus,
|
||||
LabelValue,
|
||||
HarvesterKsmtuned,
|
||||
HarvesterHugepages,
|
||||
Loading,
|
||||
SortableTable,
|
||||
HarvesterSeeder,
|
||||
@@ -209,6 +211,12 @@ export default {
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||
},
|
||||
|
||||
hasHugepagesSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
||||
},
|
||||
|
||||
hasBlockDevicesSchema() {
|
||||
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
||||
},
|
||||
@@ -468,6 +476,16 @@ export default {
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="hasHugepagesSchema"
|
||||
name="hugepages"
|
||||
:weight="0"
|
||||
:show-header="false"
|
||||
:label="t('harvester.host.tabs.hugepages')"
|
||||
>
|
||||
<HarvesterHugepages :node="value" />
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="seederEnabled"
|
||||
name="seeder"
|
||||
|
||||
@@ -125,6 +125,7 @@ export default {
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<Tabbed
|
||||
v-if="spec"
|
||||
@@ -171,6 +172,9 @@ export default {
|
||||
:cpu="cpu"
|
||||
:mode="mode"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
:max-memory="maxMemory"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -118,6 +118,10 @@ export default {
|
||||
imageName() {
|
||||
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
|
||||
},
|
||||
|
||||
sourceType() {
|
||||
return this.value?.spec?.sourceType;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -252,6 +256,16 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<LabelValue
|
||||
:name="t('harvester.image.source')"
|
||||
:value="sourceType"
|
||||
class="mb-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorMessage !== '-'"
|
||||
class="row"
|
||||
|
||||
@@ -125,6 +125,7 @@ export default {
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<Tabbed
|
||||
v-if="spec"
|
||||
@@ -171,6 +172,9 @@ export default {
|
||||
:cpu="cpu"
|
||||
:mode="mode"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
:max-memory="maxMemory"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
/>
|
||||
|
||||
<div class="row mb-10">
|
||||
|
||||
+3
-2
@@ -5,6 +5,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
||||
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||
import { HCI } from '../../../types';
|
||||
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
||||
|
||||
const UNDEFINED = 'n/a';
|
||||
|
||||
@@ -91,9 +92,9 @@ export default {
|
||||
},
|
||||
|
||||
flavor() {
|
||||
const domain = this.value?.spec?.template?.spec?.domain;
|
||||
const { cpu, memory } = getVmCPUMemoryValues(this.value);
|
||||
|
||||
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||
},
|
||||
|
||||
kernelRelease() {
|
||||
|
||||
+25
@@ -17,6 +17,13 @@ export default {
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
vmimResource: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,6 +32,12 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
liveMigrationProgressEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
||||
},
|
||||
migrationPhase() {
|
||||
return this.vmimResource?.status?.phase || 'N/A';
|
||||
},
|
||||
migrationState() {
|
||||
return this.localResource?.status?.migrationState;
|
||||
},
|
||||
@@ -58,6 +71,18 @@ export default {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="liveMigrationProgressEnabled"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.detail.details.phase')"
|
||||
:value="migrationPhase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
|
||||
@@ -27,6 +27,8 @@ import { formatSi } from '@shell/utils/units';
|
||||
|
||||
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
|
||||
|
||||
const VM_MIGRATION_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-migration-details-1/harvester-vm-migration-details?orgId=1';
|
||||
|
||||
export default {
|
||||
name: 'VMIDetailsPage',
|
||||
|
||||
@@ -63,6 +65,7 @@ export default {
|
||||
hasResourceQuotaSchema: false,
|
||||
switchToCloud: false,
|
||||
VM_METRICS_DETAIL_URL,
|
||||
VM_MIGRATION_DETAIL_URL,
|
||||
showVmMetrics: false,
|
||||
};
|
||||
},
|
||||
@@ -78,6 +81,7 @@ export default {
|
||||
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
|
||||
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
|
||||
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
|
||||
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
|
||||
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
||||
};
|
||||
|
||||
@@ -131,6 +135,39 @@ export default {
|
||||
return this.$store.getters[`${ inStore }/all`](EVENT);
|
||||
},
|
||||
|
||||
vmim() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
|
||||
|
||||
const vmiName = this.vmi?.name || '';
|
||||
|
||||
// filter the corresponding vmims by vmim.spec.vmiName and find the latest one by creationTimestamp
|
||||
const vmim = vmimList.filter((VMIM) => VMIM?.spec?.vmiName === vmiName).sort((a, b) => {
|
||||
if (a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
|
||||
return vmim.length > 0 && vmim[0] ? vmim[0] : null;
|
||||
},
|
||||
|
||||
migrationEvents() {
|
||||
const migrationVMName = this.vmim?.metadata.name || '';
|
||||
|
||||
if (migrationVMName === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.allEvents.filter((e) => {
|
||||
const { creationTimestamp } = this.value?.metadata || {};
|
||||
const involvedName = e?.involvedObject?.name;
|
||||
|
||||
return involvedName === migrationVMName && e.firstTimestamp >= creationTimestamp;
|
||||
}).sort((a, b) => a.lastTimestamp > b.lastTimestamp);
|
||||
},
|
||||
|
||||
events() {
|
||||
return this.allEvents.filter((e) => {
|
||||
const { name, creationTimestamp } = this.value?.metadata || {};
|
||||
@@ -138,7 +175,6 @@ export default {
|
||||
const pvcName = this.value.persistentVolumeClaimName || [];
|
||||
|
||||
const involvedName = e?.involvedObject?.name;
|
||||
|
||||
const matchPVC = pvcName.find((name) => name === involvedName);
|
||||
|
||||
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
|
||||
@@ -157,6 +193,10 @@ export default {
|
||||
vm: this.value.name
|
||||
};
|
||||
},
|
||||
|
||||
liveMigrationProgressEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -173,6 +213,7 @@ export default {
|
||||
const diskRows = this.getDiskRows(neu);
|
||||
|
||||
this['diskRows'] = diskRows;
|
||||
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
@@ -225,6 +266,7 @@ export default {
|
||||
<Network
|
||||
v-model:value="networkRows"
|
||||
mode="view"
|
||||
:vm="value"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
@@ -346,6 +388,20 @@ export default {
|
||||
<Migration
|
||||
:value="value"
|
||||
:vmi-resource="vmi"
|
||||
:vmim-resource="vmim"
|
||||
/>
|
||||
<DashboardMetrics
|
||||
v-if="showVmMetrics && liveMigrationProgressEnabled"
|
||||
:detail-url="VM_MIGRATION_DETAIL_URL"
|
||||
graph-height="640px"
|
||||
:has-summary-and-detail="false"
|
||||
:vars="graphVars"
|
||||
class="mb-30"
|
||||
/>
|
||||
<Events
|
||||
v-if="liveMigrationProgressEnabled"
|
||||
:resource="vmi"
|
||||
:events="migrationEvents"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
@@ -14,7 +15,7 @@ export default {
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -26,9 +27,11 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: [],
|
||||
runStrategy: runStrategyOptions[1],
|
||||
runStrategyOptions
|
||||
};
|
||||
},
|
||||
|
||||
@@ -38,6 +41,9 @@ export default {
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
vmCloneRunStrategyEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -58,7 +64,8 @@ export default {
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
|
||||
const res = await this.actionResource.doAction('clone', payload, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
@@ -106,6 +113,13 @@ export default {
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-if="vmCloneRunStrategyEnabled"
|
||||
v-model:value="runStrategy"
|
||||
label-key="harvester.virtualMachine.runStrategy"
|
||||
:options="runStrategyOptions"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
...mapState('action-menu', ['modalData']),
|
||||
|
||||
title() {
|
||||
return this.modalData.title || 'dialog.promptRemove.title';
|
||||
return this.modalData?.title || 'dialog.promptRemove.title';
|
||||
},
|
||||
|
||||
formattedType() {
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
},
|
||||
|
||||
warningMessage() {
|
||||
if (this.modalData.warningMessage) return this.modalData.warningMessage;
|
||||
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
|
||||
|
||||
const isPlural = this.type.endsWith('s');
|
||||
const thisOrThese = isPlural ? 'these' : 'this';
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
},
|
||||
|
||||
protip() {
|
||||
return this.t('promptRemove.protip', { alternateLabel });
|
||||
return this.t('dialog.promptRemove.protip', { alternateLabel });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -145,6 +145,7 @@ export default {
|
||||
try {
|
||||
for (const resource of this.resources) {
|
||||
await resource.remove();
|
||||
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
|
||||
}
|
||||
buttonDone(true);
|
||||
this.close();
|
||||
@@ -164,7 +165,7 @@ export default {
|
||||
>
|
||||
<template #title>
|
||||
<h4 class="text-default-text">
|
||||
{{ t('promptRemove.title') }}
|
||||
{{ t(title, { type }, true) }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async create(buttonCb) {
|
||||
// shadow clone
|
||||
if (!this.cloneData) {
|
||||
this.resources[0].goToClone();
|
||||
buttonCb(false);
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('harvester.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this['errors'] = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this['errors'] = message;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.cloneVM.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Checkbox
|
||||
v-model:value="cloneData"
|
||||
class="mb-10"
|
||||
label-key="harvester.modal.cloneVM.type"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-show="cloneData"
|
||||
v-model:value="name"
|
||||
class="mb-20"
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
||||
:disabled="cloneData && !name"
|
||||
@click="create"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { NETWORK_TYPE } from '../config/types';
|
||||
|
||||
export default {
|
||||
name: 'AddHotplugNic',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
try {
|
||||
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
this.allVMNetworks = [];
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
interfaceName: '',
|
||||
networkName: '',
|
||||
macAddress: '',
|
||||
allVMNetworks: [],
|
||||
errors: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources?.[0];
|
||||
},
|
||||
|
||||
isFormValid() {
|
||||
return this.interfaceName !== '' && this.networkName !== '';
|
||||
},
|
||||
|
||||
vmNetworksOption() {
|
||||
return this.allVMNetworks
|
||||
.filter((network) => {
|
||||
const labels = network.metadata?.labels || {};
|
||||
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
|
||||
|
||||
const isValidType = [
|
||||
NETWORK_TYPE.L2VLAN,
|
||||
NETWORK_TYPE.UNTAGGED,
|
||||
NETWORK_TYPE.L2TRUNK_VLAN,
|
||||
].includes(type);
|
||||
|
||||
return isValidType && !network.isSystem;
|
||||
})
|
||||
.map((network) => {
|
||||
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
|
||||
|
||||
return ({
|
||||
label,
|
||||
value: network.id || '',
|
||||
disabled: network.isNotReady,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.interfaceName = '';
|
||||
this.networkName = '';
|
||||
this.macAddress = '';
|
||||
this.errors = [];
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
if (!this.actionResource) {
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
interfaceName: this.interfaceName,
|
||||
networkName: this.networkName
|
||||
};
|
||||
|
||||
if (this.macAddress) {
|
||||
payload.macAddress = this.macAddress;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.actionResource.doAction('addNic', payload);
|
||||
|
||||
if ([200, 204].includes(res?._status)) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.hotplugNic.success', {
|
||||
interfaceName: this.interfaceName,
|
||||
vm: this.actionResource.nameDisplay
|
||||
})
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
this.errors = exceptionToErrorsArray(res);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
ref="modal"
|
||||
name="modal"
|
||||
:show-highlight-border="false"
|
||||
>
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.modal.hotplugNic.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<LabeledInput
|
||||
v-model:value="interfaceName"
|
||||
:label="t('generic.name')"
|
||||
required
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-model:value="networkName"
|
||||
class="mt-20"
|
||||
:label="t('harvester.modal.hotplugNic.vmNetwork')"
|
||||
:options="vmNetworksOption"
|
||||
required
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="macAddress"
|
||||
class="mt-20"
|
||||
label-key="harvester.modal.hotplugNic.macAddress"
|
||||
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:disabled="!isFormValid"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
+4
-4
@@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'HotplugModal',
|
||||
name: 'HotplugVolumeModal',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !pvc.attachVM;
|
||||
return true;
|
||||
})
|
||||
.map((pvc) => {
|
||||
return {
|
||||
@@ -90,7 +90,7 @@ export default {
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||
message: this.t('harvester.modal.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
@@ -122,7 +122,7 @@ export default {
|
||||
>
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.modal.hotplug.title')"
|
||||
v-clean-html="t('harvester.modal.hotplugVolume.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,185 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { GIBIBYTE } from '../utils/unit';
|
||||
|
||||
export default {
|
||||
name: 'CPUMemoryHotplugModal',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Card, Banner, UnitInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const { cpu, memory } = getVmCPUMemoryValues(this.resources[0] || {});
|
||||
|
||||
return {
|
||||
cpu,
|
||||
memory,
|
||||
errors: [],
|
||||
GIBIBYTE
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0] || {};
|
||||
},
|
||||
maxResourcesMessage() {
|
||||
const { maxCpu, maxMemory } = getVmCPUMemoryValues(this.actionResource);
|
||||
|
||||
if (maxCpu && maxMemory) {
|
||||
return this.t('harvester.modal.cpuMemoryHotplug.maxResourcesMessage', { maxCpu, maxMemory });
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.cpu = '';
|
||||
this.memory = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
change() {
|
||||
if (parseInt(this.memory, 10) < 1 ) {
|
||||
this.memory = '1Gi';
|
||||
}
|
||||
if (this.cpu < 1) {
|
||||
this.cpu = 1;
|
||||
}
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
if (this.actionResource) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction('cpuAndMemoryHotplug', { sockets: this.cpu, memory: this.memory });
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.cpuMemoryHotplug.success', { vm: this.actionResource.nameDisplay })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this['errors'] = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this['errors'] = message;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
ref="modal"
|
||||
name="modal"
|
||||
:show-highlight-border="false"
|
||||
>
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.modal.cpuMemoryHotplug.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Banner
|
||||
v-if="maxResourcesMessage"
|
||||
:label="maxResourcesMessage"
|
||||
color="info"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="cpu"
|
||||
:label="t('harvester.virtualMachine.input.cpu')"
|
||||
:delay="0"
|
||||
required
|
||||
suffix="C"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:min="1"
|
||||
@update:value="change"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="memory"
|
||||
:label="t('harvester.virtualMachine.input.memory')"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:min="1"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterExportImageDialog',
|
||||
@@ -105,11 +106,17 @@ export default {
|
||||
}
|
||||
|
||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
const isInternal = isInternalStorageClass(s.metadata?.name);
|
||||
|
||||
if (isInternal) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
value: s.name,
|
||||
disabled: isInternal,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
|
||||
+38
-12
@@ -1,13 +1,12 @@
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHotUnplugModal',
|
||||
name: 'HarvesterHotUnplug',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
@@ -35,8 +34,25 @@ export default {
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
diskName() {
|
||||
return this.modalData.diskName;
|
||||
|
||||
name() {
|
||||
return this.modalData.name;
|
||||
},
|
||||
|
||||
isVolume() {
|
||||
return this.modalData.type === 'volume';
|
||||
},
|
||||
|
||||
titleKey() {
|
||||
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
|
||||
},
|
||||
|
||||
actionLabelKey() {
|
||||
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
|
||||
},
|
||||
|
||||
successMessageKey() {
|
||||
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,14 +63,20 @@ export default {
|
||||
|
||||
async save(buttonCb) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
|
||||
let res;
|
||||
|
||||
if (this.isVolume) {
|
||||
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
||||
} else {
|
||||
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
||||
}
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch(
|
||||
'growl/success',
|
||||
{
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
|
||||
message: this.t(this.successMessageKey, { name: this.name })
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
@@ -64,14 +86,14 @@ export default {
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this['errors'] = error;
|
||||
this.errors = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this['errors'] = message;
|
||||
this.errors = message;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +109,7 @@ export default {
|
||||
>
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
||||
v-clean-html="t(titleKey, { name })"
|
||||
class="text-default-text"
|
||||
/>
|
||||
<Banner
|
||||
@@ -111,9 +133,9 @@ export default {
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||
:action-label="t(actionLabelKey)"
|
||||
:waiting-label="t(actionLabelKey)"
|
||||
:success-label="t(actionLabelKey)"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
@@ -132,4 +154,8 @@ export default {
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep(.card-title) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
@@ -6,7 +7,15 @@ import AsyncButton from '@shell/components/AsyncButton';
|
||||
import GraphCircle from '@shell/components/graph/Circle';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AppModal from '@shell/components/AppModal';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { HCI } from '../types';
|
||||
import { HCI_SETTING } from '../config/settings';
|
||||
import { DOC } from '../config/doc-links';
|
||||
import { docLink } from '../utils/feature-flags';
|
||||
|
||||
const SELECT_ALL = 'select_all';
|
||||
const UNSELECT_ALL = 'unselect_all';
|
||||
|
||||
export default {
|
||||
name: 'SupportBundle',
|
||||
@@ -17,14 +26,36 @@ export default {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
AppModal,
|
||||
LabeledSelect,
|
||||
UnitInput
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||
|
||||
try {
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||
const response = await this.$store.dispatch('harvester/request', { url });
|
||||
|
||||
this.defaultNamespaces = response.data || [];
|
||||
} catch (error) {
|
||||
this.defaultNamespaces = [];
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
url: '',
|
||||
description: '',
|
||||
errors: [],
|
||||
isOpen: false,
|
||||
isOpen: false,
|
||||
errors: [],
|
||||
version: '',
|
||||
clusterName: '',
|
||||
url: '',
|
||||
description: '',
|
||||
namespaces: [],
|
||||
defaultNamespaces: [],
|
||||
timeout: '',
|
||||
expiration: '',
|
||||
nodeTimeout: '',
|
||||
};
|
||||
},
|
||||
|
||||
@@ -39,23 +70,51 @@ export default {
|
||||
|
||||
percentage() {
|
||||
return this.$store.getters['harvester-common/getBundlePercentage'];
|
||||
}
|
||||
},
|
||||
|
||||
availableNamespaces() {
|
||||
const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||
const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||
|
||||
return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns));
|
||||
},
|
||||
|
||||
namespaceOptions() {
|
||||
if (this.availableNamespaces.length === 0) return [];
|
||||
|
||||
const allSelected = this.namespaces.length === this.availableNamespaces.length &&
|
||||
this.availableNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||
|
||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||
|
||||
return [controlOption, ...this.availableNamespaces];
|
||||
},
|
||||
|
||||
docLink() {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
|
||||
return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version);
|
||||
},
|
||||
|
||||
customSupportBundleFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
isShowBundleModal: {
|
||||
immediate: true,
|
||||
handler(show) {
|
||||
if (show) {
|
||||
this.$nextTick(() => {
|
||||
this.isOpen = true;
|
||||
});
|
||||
} else {
|
||||
this.isOpen = false;
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
this.isOpen = show;
|
||||
}
|
||||
},
|
||||
|
||||
isOpen(newVal) {
|
||||
if (newVal) {
|
||||
this.loadDefaultSettings();
|
||||
} else {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -65,33 +124,93 @@ export default {
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||
this.backUpName = '';
|
||||
},
|
||||
|
||||
loadDefaultSettings() {
|
||||
const cluster = this.$store.getters['currentCluster'];
|
||||
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
|
||||
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
|
||||
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
|
||||
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
|
||||
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
|
||||
|
||||
this.version = versionSetting?.currentVersion || '';
|
||||
this.clusterName = cluster?.id || '';
|
||||
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
|
||||
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
|
||||
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
|
||||
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
this.errors = [];
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
this.namespaces = [];
|
||||
this.timeout = '';
|
||||
this.expiration = '';
|
||||
this.nodeTimeout = '';
|
||||
this.errors = [];
|
||||
},
|
||||
|
||||
updateNamespaces(selected) {
|
||||
if (selected.includes(SELECT_ALL)) {
|
||||
this.namespaces = [...this.availableNamespaces];
|
||||
} else if (selected.includes(UNSELECT_ALL)) {
|
||||
this.namespaces = [];
|
||||
} else {
|
||||
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||
}
|
||||
},
|
||||
|
||||
updateNumberValue(field, value) {
|
||||
if (value === '' || value === null || isNaN(value)) {
|
||||
this[field] = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const num = Number(value);
|
||||
const isValid = Number.isInteger(num) && num >= 0;
|
||||
|
||||
this[field] = isValid ? String(num) : '';
|
||||
},
|
||||
|
||||
onKeyDown(e) {
|
||||
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
this.errors = [];
|
||||
|
||||
const name = `bundle-${ randomStr(5).toLowerCase() }`;
|
||||
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
|
||||
const namespace = 'harvester-system';
|
||||
|
||||
const spec = {
|
||||
description: this.description.trim(),
|
||||
...(this.url.trim() && { issueURL: this.url.trim() }),
|
||||
...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }),
|
||||
...(this.timeout !== '' && { timeout: Number(this.timeout) }),
|
||||
...(this.expiration !== '' && { expiration: Number(this.expiration) }),
|
||||
...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }),
|
||||
};
|
||||
|
||||
const bundleCrd = {
|
||||
apiVersion: 'harvesterhci.io/v1beta1',
|
||||
type: HCI.SUPPORT_BUNDLE,
|
||||
kind: 'SupportBundle',
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
},
|
||||
spec: {
|
||||
issueURL: this.url,
|
||||
description: this.description
|
||||
}
|
||||
metadata: { name, namespace },
|
||||
spec,
|
||||
};
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||
|
||||
try {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||
|
||||
await bundleValue.save();
|
||||
|
||||
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
||||
@@ -118,69 +237,110 @@ export default {
|
||||
@close="close"
|
||||
>
|
||||
<div class="p-20">
|
||||
<h2>
|
||||
{{ t('harvester.modal.bundle.title') }}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
v-if="!bundlePending"
|
||||
class="content"
|
||||
>
|
||||
<LabeledInput
|
||||
v-model:value="url"
|
||||
:label="t('harvester.modal.bundle.url')"
|
||||
class="mb-20"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model:value="description"
|
||||
:label="t('harvester.modal.bundle.description')"
|
||||
type="multiline"
|
||||
:min-height="120"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="content"
|
||||
>
|
||||
<div class="circle">
|
||||
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
|
||||
<div class="content">
|
||||
<div
|
||||
v-if="bundlePending"
|
||||
class="circle mb-20"
|
||||
>
|
||||
<GraphCircle
|
||||
primary-stroke-color="green"
|
||||
secondary-stroke-color="white"
|
||||
secondary-stroke-color="lightgrey"
|
||||
:stroke-width="6"
|
||||
:percentage="percentage"
|
||||
:show-text="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<p
|
||||
v-if="customSupportBundleFeatureEnabled"
|
||||
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
|
||||
class="mb-20"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="url"
|
||||
:label="t('harvester.modal.bundle.url')"
|
||||
class="mb-10"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="description"
|
||||
required
|
||||
:label="t('harvester.modal.bundle.description')"
|
||||
type="multiline"
|
||||
:min-height="80"
|
||||
class="mb-10"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-for="(err, idx) in errors"
|
||||
:key="idx"
|
||||
>
|
||||
<Banner
|
||||
color="error"
|
||||
:label="stringify(err)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="footer mt-20">
|
||||
<button
|
||||
class="btn btn-sm role-secondary mr-10"
|
||||
@click="close"
|
||||
<template v-if="customSupportBundleFeatureEnabled">
|
||||
<LabeledSelect
|
||||
v-model:value="namespaces"
|
||||
:label="t('harvester.modal.bundle.namespaces.label')"
|
||||
:clearable="true"
|
||||
:multiple="true"
|
||||
:options="namespaceOptions"
|
||||
class="mb-10 label-select"
|
||||
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
||||
@update:value="updateNamespaces"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="timeout"
|
||||
:label="t('harvester.modal.bundle.timeout.label')"
|
||||
class="mb-10"
|
||||
type="number"
|
||||
:min="0"
|
||||
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
|
||||
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
|
||||
@keydown="onKeyDown"
|
||||
@update:value="val => updateNumberValue('timeout', val)"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="expiration"
|
||||
:label="t('harvester.modal.bundle.expiration.label')"
|
||||
class="mb-10"
|
||||
type="number"
|
||||
:min="0"
|
||||
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
|
||||
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
|
||||
@keydown="onKeyDown"
|
||||
@update:value="val => updateNumberValue('expiration', val)"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="nodeTimeout"
|
||||
:label="t('harvester.modal.bundle.nodeTimeout.label')"
|
||||
class="mb-10"
|
||||
type="number"
|
||||
:min="0"
|
||||
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
|
||||
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
|
||||
@keydown="onKeyDown"
|
||||
@update:value="val => updateNumberValue('nodeTimeout', val)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<div
|
||||
v-for="(err, idx) in errors"
|
||||
:key="idx"
|
||||
>
|
||||
{{ t('generic.close') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
type="submit"
|
||||
mode="generate"
|
||||
class="btn btn-sm bg-primary"
|
||||
:disabled="bundlePending"
|
||||
@click="save"
|
||||
/>
|
||||
<Banner
|
||||
color="error"
|
||||
:label="stringify(err)"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer mt-20">
|
||||
<button
|
||||
class="btn btn-sm role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.close') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
type="submit"
|
||||
mode="generate"
|
||||
class="btn btn-sm bg-primary"
|
||||
:disabled="bundlePending"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-modal>
|
||||
@@ -194,6 +354,10 @@ export default {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
|
||||
margin: 0 7px;
|
||||
}
|
||||
|
||||
.bundle {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
@@ -204,10 +368,8 @@ export default {
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 218px;
|
||||
|
||||
.circle {
|
||||
padding-top: 20px;
|
||||
padding: 10px 0;
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,10 @@ export default {
|
||||
|
||||
data.spec.clusterNetwork = this.clusterNetwork;
|
||||
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](`v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`);
|
||||
|
||||
await this.$store.dispatch('harvester/request', {
|
||||
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<script>
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditMIGConfiguration',
|
||||
|
||||
components: {
|
||||
Tab,
|
||||
Tabbed,
|
||||
CruResource,
|
||||
LabeledInput,
|
||||
NameNsDescription,
|
||||
LabelValue
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const { profileSpec } = this.value.spec;
|
||||
|
||||
return { profileSpec: profileSpec || [] };
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.registerBeforeHook) {
|
||||
this.registerBeforeHook(this.updateBeforeSave);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === 'view';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateBeforeSave() {
|
||||
// MIGConfiguration CRD don't have any namespace field,
|
||||
// so we need to remove the namespace field before saving
|
||||
delete this.value.metadata.namespace;
|
||||
// enable the MIGConfiguration when saving
|
||||
this.value.spec.enabled = true;
|
||||
},
|
||||
|
||||
labelTitle(profile) {
|
||||
return `${ profile.name } (available : ${ this.available(profile) })`;
|
||||
},
|
||||
|
||||
available(profile) {
|
||||
const count = this.value.status?.profileStatus?.find((p) => p.id === profile.id)?.available;
|
||||
|
||||
return count || 0;
|
||||
},
|
||||
|
||||
updateRequested(neu, profile) {
|
||||
if (neu === null || neu === '') return;
|
||||
const newValue = Number(neu);
|
||||
const availableCount = this.available(profile);
|
||||
|
||||
if (newValue < 0) {
|
||||
profile.requested = 0;
|
||||
} else if ( newValue > availableCount ) {
|
||||
profile.requested = availableCount;
|
||||
} else {
|
||||
profile.requested = newValue;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
finish-button-mode="enable"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Tabbed
|
||||
v-bind="$attrs"
|
||||
class="mt-15"
|
||||
:side-tabs="true"
|
||||
>
|
||||
<Tab
|
||||
name="profileSpec"
|
||||
:label="t('harvester.migconfiguration.profileSpec')"
|
||||
:weight="1"
|
||||
class="bordered-table"
|
||||
>
|
||||
<div
|
||||
v-for="(profile, index) in profileSpec"
|
||||
:key="index"
|
||||
>
|
||||
<LabelValue
|
||||
:value="labelTitle(profile)"
|
||||
class="mb-10"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="profile.requested"
|
||||
:min="0"
|
||||
:disabled="isView"
|
||||
type="number"
|
||||
class="mb-20"
|
||||
:label="`${t('harvester.migconfiguration.requested')}`"
|
||||
@update:value="updateRequested($event, profile)"
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
@@ -38,6 +38,7 @@ export default {
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<component
|
||||
:is="currentComponent"
|
||||
|
||||
@@ -80,6 +80,7 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -8,7 +8,7 @@ import LabelValue from '@shell/components/LabelValue';
|
||||
import { BadgeState } from '@components/BadgeState';
|
||||
import { Banner } from '@components/Banner';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { RadioGroup, RadioButton } from '@components/Form/Radio';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import HarvesterDisk from '../../mixins/harvester-disk';
|
||||
import Tags from '../../components/DiskTags';
|
||||
import { HCI } from '../../types';
|
||||
@@ -30,7 +30,6 @@ export default {
|
||||
BadgeState,
|
||||
Banner,
|
||||
RadioGroup,
|
||||
RadioButton,
|
||||
ModalWithCard,
|
||||
Tags,
|
||||
},
|
||||
@@ -184,12 +183,11 @@ export default {
|
||||
},
|
||||
|
||||
forceFormattedDisabled() {
|
||||
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
|
||||
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
|
||||
|
||||
const systems = ['ext4', 'XFS'];
|
||||
|
||||
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
|
||||
if (this.blockDevice?.childParts?.length > 0) {
|
||||
return true;
|
||||
} else if (systems.includes(fileSystem)) {
|
||||
return false;
|
||||
@@ -446,7 +444,7 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted"
|
||||
v-if="(value.isNew && isLonghornV1) || isCorrupted"
|
||||
class="col span-6"
|
||||
>
|
||||
<RadioGroup
|
||||
@@ -459,15 +457,6 @@ export default {
|
||||
:disabled="forceFormattedDisabled"
|
||||
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
|
||||
>
|
||||
<template #1="{option, listeners}">
|
||||
<RadioButton
|
||||
:label="option.label"
|
||||
:val="option.value"
|
||||
:value="value.forceFormatted"
|
||||
:disabled="forceFormattedDisabled && !value.forceFormatted"
|
||||
v-on="listeners"
|
||||
/>
|
||||
</template>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<script>
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import { HCI } from '../../types';
|
||||
import { DOC } from '../../config/doc-links';
|
||||
|
||||
export const hugepagesTHPEnabledMode = [{
|
||||
label: 'Always',
|
||||
value: 'always',
|
||||
}, {
|
||||
label: 'Madvise',
|
||||
value: 'madvise',
|
||||
}, {
|
||||
label: 'Never',
|
||||
value: 'never',
|
||||
}];
|
||||
|
||||
export const hugepagesTHPShmemEnabledMode = [{
|
||||
label: 'Always',
|
||||
value: 'always',
|
||||
}, {
|
||||
label: 'Within Size',
|
||||
value: 'within_size',
|
||||
}, {
|
||||
label: 'Advise',
|
||||
value: 'advise',
|
||||
}, {
|
||||
label: 'Never',
|
||||
value: 'never',
|
||||
}, {
|
||||
label: 'Deny',
|
||||
value: 'deny',
|
||||
}, {
|
||||
label: 'Force',
|
||||
value: 'force',
|
||||
}];
|
||||
|
||||
export const hugepagesTHPDefragMode = [{
|
||||
label: 'Always',
|
||||
value: 'always',
|
||||
}, {
|
||||
label: 'Defer',
|
||||
value: 'defer',
|
||||
}, {
|
||||
label: 'Defer+Madvise',
|
||||
value: 'defer+madvise',
|
||||
}, {
|
||||
label: 'Madvise',
|
||||
value: 'madvise',
|
||||
}, {
|
||||
label: 'Never',
|
||||
value: 'never'
|
||||
}];
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHugepages',
|
||||
components: { LabeledSelect },
|
||||
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
docsTransparentHugepagesLink() {
|
||||
return DOC.TRANSPARENT_HUGEPAGES;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveHugepages() {
|
||||
this.hugepages['spec'] = this.spec;
|
||||
|
||||
await this.hugepages.save().catch((reason) => {
|
||||
if (reason?.type === 'error') {
|
||||
this.$store.dispatch('growl/error', {
|
||||
title: this.t('harvester.notification.title.error'),
|
||||
message: reason?.message,
|
||||
}, { root: true });
|
||||
|
||||
return Promise.reject(new Error('saveHugepages error'));
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
||||
|
||||
this.hugepages = hash.find((node) => {
|
||||
return node.id === this.node.id;
|
||||
});
|
||||
this.spec = this.hugepages.spec;
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
hugepages: {},
|
||||
spec: { transparent: {} },
|
||||
hugepagesTHPEnabledMode,
|
||||
hugepagesTHPShmemEnabledMode,
|
||||
hugepagesTHPDefragMode,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<hr class="divider" />
|
||||
<h3>
|
||||
<t
|
||||
k="harvester.host.hugepages.transparent.title"
|
||||
:raw="true"
|
||||
:url="docsTransparentHugepagesLink"
|
||||
/>
|
||||
</h3>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="spec.transparent.enabled"
|
||||
:label="t('harvester.host.hugepages.transparent.enabled')"
|
||||
:options="hugepagesTHPEnabledMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="spec.transparent.shmemEnabled"
|
||||
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
||||
:options="hugepagesTHPShmemEnabledMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="spec.transparent.defrag"
|
||||
:label="t('harvester.host.hugepages.transparent.defrag')"
|
||||
:options="hugepagesTHPDefragMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,6 +7,8 @@ import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { HCI } from '../../types';
|
||||
import { DOC } from '../../config/doc-links';
|
||||
import { docLink } from '../../utils/feature-flags';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { clone } from '@shell/utils/object';
|
||||
|
||||
export const ksmtunedMode = [{
|
||||
value: 'standard',
|
||||
@@ -64,11 +66,13 @@ export default {
|
||||
|
||||
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
|
||||
this.spec = this.ksmtuned.spec;
|
||||
this.originSpec = clone(this.ksmtuned.spec);
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
ksmtuned: {},
|
||||
originSpec: {},
|
||||
spec: {},
|
||||
thresCoef: 30,
|
||||
ksmtunedMode,
|
||||
@@ -99,6 +103,11 @@ export default {
|
||||
|
||||
methods: {
|
||||
async saveKsmtuned() {
|
||||
// no ksmtuned spec change, skip save
|
||||
if (isEqual(this.spec, this.originSpec)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
|
||||
this.ksmtuned['spec'] = this.spec;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { HCI } from '../../types';
|
||||
import HarvesterDisk from './HarvesterDisk';
|
||||
import HarvesterSeeder from './HarvesterSeeder';
|
||||
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
||||
import HarvesterHugepages from './HarvesterHugepages';
|
||||
import Tags from '../../components/DiskTags';
|
||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
@@ -50,6 +51,7 @@ export default {
|
||||
ArrayListGrouped,
|
||||
HarvesterDisk,
|
||||
HarvesterKsmtuned,
|
||||
HarvesterHugepages,
|
||||
ButtonDropdown,
|
||||
KeyValue,
|
||||
Banner,
|
||||
@@ -225,6 +227,12 @@ export default {
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||
},
|
||||
|
||||
hasHugepagesSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
||||
},
|
||||
|
||||
hasBlockDevicesSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
@@ -527,7 +535,6 @@ export default {
|
||||
id: this.longhornNode.id,
|
||||
opt: { force: true },
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, '5000'));
|
||||
await retrySave();
|
||||
} else {
|
||||
@@ -648,6 +655,17 @@ export default {
|
||||
</template>
|
||||
</ArrayListGrouped>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="hasHugepagesSchema"
|
||||
name="Hugepages"
|
||||
:weight="70"
|
||||
:label="t('harvester.host.tabs.hugepages')"
|
||||
>
|
||||
<HarvesterHugepages
|
||||
:node="value"
|
||||
:register-before-hook="registerBeforeHook"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="hasKsmtunedSchema"
|
||||
name="Ksmtuned"
|
||||
|
||||
@@ -75,6 +75,7 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<div class="header mb-20">
|
||||
<FileSelector
|
||||
|
||||
@@ -191,7 +191,7 @@ export default {
|
||||
:validation-passed="true"
|
||||
:errors="errors"
|
||||
:can-yaml="true"
|
||||
@error="e=>errors = e"
|
||||
@error="e=>errors=e"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
:resource="value"
|
||||
:subtypes="[]"
|
||||
:cancel-event="true"
|
||||
@error="e=>errors = e"
|
||||
@error="e=>errors=e"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
|
||||
@@ -6,14 +6,17 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
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 { HCI } from '../types';
|
||||
import { NETWORK_TYPE } from '../config/types';
|
||||
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
|
||||
import { removeObject } from '@shell/utils/array';
|
||||
|
||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||
const {
|
||||
L2VLAN, UNTAGGED, OVERLAY, L2TRUNK_VLAN
|
||||
} = NETWORK_TYPE;
|
||||
const { ACCESS, TRUNK } = L2VLAN_MODE;
|
||||
|
||||
const AUTO = 'auto';
|
||||
const MANUAL = 'manual';
|
||||
@@ -44,18 +47,17 @@ export default {
|
||||
|
||||
data() {
|
||||
const config = JSON.parse(this.value.spec.config);
|
||||
|
||||
const annotations = this.value?.metadata?.annotations || {};
|
||||
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
||||
|
||||
if ((config.bridge || '').endsWith('-br')) {
|
||||
config.bridge = config.bridge.slice(0, -3);
|
||||
}
|
||||
|
||||
const type = this.value.vlanType || L2VLAN ;
|
||||
|
||||
return {
|
||||
config,
|
||||
type,
|
||||
l2VlanMode: this.value.vlanType === L2TRUNK_VLAN ? TRUNK : ACCESS,
|
||||
vlanTrunk: this.parseVlanTrunk(config),
|
||||
layer3Network: {
|
||||
mode: layer3Network.mode || AUTO,
|
||||
serverIPAddr: layer3Network.serverIPAddr || '',
|
||||
@@ -78,6 +80,30 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
clusterBridge: {
|
||||
get() {
|
||||
if (!this.config.bridge) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// remove -br suffix if exists
|
||||
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
|
||||
},
|
||||
|
||||
set(neu) {
|
||||
if (neu === '') {
|
||||
this.config.bridge = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!neu.endsWith('-br')) {
|
||||
this.config.bridge = `${ neu }-br`;
|
||||
} else {
|
||||
this.config.bridge = neu;
|
||||
}
|
||||
}
|
||||
},
|
||||
modeOptions() {
|
||||
return [{
|
||||
label: this.t('harvester.network.layer3Network.mode.auto'),
|
||||
@@ -88,6 +114,18 @@ export default {
|
||||
}];
|
||||
},
|
||||
|
||||
l2VlanTrunkModeFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('l2VlanTrunkMode');
|
||||
},
|
||||
|
||||
kubeovnVpcSubnetSupport() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
|
||||
},
|
||||
|
||||
longhornV2LVMSupport() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
|
||||
},
|
||||
|
||||
clusterNetworkOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||
@@ -103,8 +141,56 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
networkType() {
|
||||
return [L2VLAN, UNTAGGED];
|
||||
l2VlanModeOptions() {
|
||||
return [{
|
||||
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.access'),
|
||||
value: ACCESS,
|
||||
}, {
|
||||
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.trunk'),
|
||||
value: TRUNK,
|
||||
}];
|
||||
},
|
||||
|
||||
networkTypes() {
|
||||
const types = [L2VLAN, UNTAGGED];
|
||||
|
||||
if (this.kubeovnVpcSubnetSupport) {
|
||||
types.push(OVERLAY);
|
||||
}
|
||||
|
||||
return types;
|
||||
},
|
||||
|
||||
isL2VlanNetwork() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === L2VLAN;
|
||||
}
|
||||
|
||||
return this.type === L2VLAN;
|
||||
},
|
||||
|
||||
isL2VlanTrunkMode() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === L2TRUNK_VLAN && this.l2VlanMode === TRUNK;
|
||||
}
|
||||
|
||||
return this.type === L2TRUNK_VLAN || (this.l2VlanMode === TRUNK && this.type === L2VLAN);
|
||||
},
|
||||
|
||||
isL2VlanAccessMode() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === L2VLAN && this.l2VlanMode === ACCESS;
|
||||
}
|
||||
|
||||
return this.type === L2VLAN && this.l2VlanMode === ACCESS;
|
||||
},
|
||||
|
||||
isOverlayNetwork() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === OVERLAY;
|
||||
}
|
||||
|
||||
return this.type === OVERLAY;
|
||||
},
|
||||
|
||||
isUntaggedNetwork() {
|
||||
@@ -116,39 +202,102 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
type(newType) {
|
||||
if (newType === OVERLAY) { // overlay network configuration
|
||||
this.config.type = 'kube-ovn';
|
||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
|
||||
} else { // l2vlan or untagged network configuration
|
||||
this.config.type = 'bridge';
|
||||
this.config.promiscMode = true;
|
||||
this.config.ipam = {};
|
||||
this.config.bridge = '';
|
||||
delete this.config.provider;
|
||||
delete this.config.server_socket;
|
||||
}
|
||||
},
|
||||
l2VlanMode(newAccessMode) {
|
||||
if (this.type !== L2VLAN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newAccessMode === TRUNK) { // trunk mode
|
||||
this.config.vlan = 0;
|
||||
this.config.vlanTrunk = this.vlanTrunk;
|
||||
} else { // access mode
|
||||
delete this.config.vlanTrunk;
|
||||
this.config.vlan = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveNetwork(buttonCb) {
|
||||
const errors = [];
|
||||
|
||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||
}
|
||||
|
||||
if (!this.config.bridge) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||
}
|
||||
|
||||
if (this.layer3Network.mode === MANUAL) {
|
||||
if (!this.layer3Network.gateway) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
||||
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.minID === '')) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId') }));
|
||||
}
|
||||
if (!this.layer3Network.cidr) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||
|
||||
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.maxID === '')) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId') }));
|
||||
}
|
||||
|
||||
if (this.isL2VlanAccessMode && !this.config.vlan && !this.isUntaggedNetwork) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||
}
|
||||
|
||||
if (!this.config.bridge) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||
}
|
||||
|
||||
if (this.layer3Network.mode === MANUAL) {
|
||||
if (!this.layer3Network.gateway) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||
}
|
||||
if (!this.layer3Network.cidr) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||
|
||||
await this.save(buttonCb);
|
||||
},
|
||||
|
||||
parseVlanTrunk(config) {
|
||||
if (config?.vlanTrunk && config?.vlanTrunk?.length > 0) {
|
||||
return config.vlanTrunk;
|
||||
}
|
||||
|
||||
return [{ minID: '', maxID: '' }];
|
||||
},
|
||||
|
||||
removeVlanTrunk(trunk) {
|
||||
removeObject(this.vlanTrunk, trunk);
|
||||
},
|
||||
|
||||
addVlanTrunk() {
|
||||
if (!this.config.vlanTrunk) {
|
||||
this.config.vlanTrunk = [];
|
||||
}
|
||||
this.vlanTrunk.push({ minID: this.minId, maxID: this.maxId });
|
||||
this.config.vlanTrunk = this.vlanTrunk;
|
||||
},
|
||||
|
||||
vlanTrunkChange() {
|
||||
this.config.vlanTrunk = this.vlanTrunk;
|
||||
},
|
||||
|
||||
input(neu) {
|
||||
if (neu === '') {
|
||||
this.config.vlan = '';
|
||||
@@ -169,14 +318,19 @@ export default {
|
||||
updateBeforeSave() {
|
||||
this.config.name = this.value.metadata.name;
|
||||
|
||||
if (this.isOverlayNetwork) {
|
||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||
delete this.config.bridge;
|
||||
delete this.config.promiscMode;
|
||||
delete this.config.vlan;
|
||||
delete this.config.ipam;
|
||||
}
|
||||
|
||||
if (this.isUntaggedNetwork) {
|
||||
delete this.config.vlan;
|
||||
}
|
||||
|
||||
this.value.spec.config = JSON.stringify({
|
||||
...this.config,
|
||||
bridge: `${ this.config.bridge }-br`,
|
||||
});
|
||||
this.value.spec.config = JSON.stringify({ ...this.config });
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -190,6 +344,7 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="saveNetwork"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
ref="nd"
|
||||
@@ -211,14 +366,83 @@ export default {
|
||||
<LabeledSelect
|
||||
v-model:value="type"
|
||||
class="mb-20"
|
||||
:options="networkType"
|
||||
:options="networkTypes"
|
||||
:mode="mode"
|
||||
:disabled="isEdit"
|
||||
:label="t('harvester.fields.type')"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-if="isL2VlanNetwork && l2VlanTrunkModeFeatureEnabled"
|
||||
v-model:value="l2VlanMode"
|
||||
class="mb-20"
|
||||
:options="l2VlanModeOptions"
|
||||
:mode="mode"
|
||||
:disabled="isEdit"
|
||||
:label="t('harvester.vlanStatus.vlanConfig.l2VlanMode.label')"
|
||||
required
|
||||
/>
|
||||
<div v-if="isL2VlanTrunkMode && l2VlanTrunkModeFeatureEnabled">
|
||||
<div
|
||||
v-for="(trunk, i) in vlanTrunk"
|
||||
:key="i"
|
||||
>
|
||||
<div class="row mt-10">
|
||||
<div class="col trunk-span">
|
||||
<LabeledInput
|
||||
v-model:value.number="trunk.minID"
|
||||
class="mb-20"
|
||||
required
|
||||
placeholder="e.g. 1-4094"
|
||||
:min="1"
|
||||
:max="4094"
|
||||
type="number"
|
||||
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId')"
|
||||
:mode="mode"
|
||||
@update:value="vlanTrunkChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="col trunk-span">
|
||||
<LabeledInput
|
||||
v-model:value.number="trunk.maxID"
|
||||
class="mb-20"
|
||||
:max="4094"
|
||||
:min="1"
|
||||
placeholder="e.g. 1-4094"
|
||||
required
|
||||
type="number"
|
||||
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId')"
|
||||
:mode="mode"
|
||||
@update:value="vlanTrunkChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="col remove-btn mb-20">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isView || vlanTrunk.length <= 1"
|
||||
:aria-label="t('generic.ariaLabel.remove', {index: i+1})"
|
||||
role="button"
|
||||
class="btn role-link"
|
||||
@click="removeVlanTrunk(trunk)"
|
||||
>
|
||||
{{ t('harvester.fields.remove') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="isL2VlanTrunkMode"
|
||||
type="button"
|
||||
class="btn btn-sm bg-primary mb-20"
|
||||
:disabled="isView"
|
||||
@click="addVlanTrunk"
|
||||
>
|
||||
{{ t('harvester.vlanStatus.vlanConfig.vlanTrunk.add') }}
|
||||
</button>
|
||||
</div>
|
||||
<LabeledInput
|
||||
v-if="!isUntaggedNetwork"
|
||||
v-if="isL2VlanAccessMode"
|
||||
v-model:value.number="config.vlan"
|
||||
class="mb-20"
|
||||
required
|
||||
@@ -228,25 +452,20 @@ export default {
|
||||
:mode="mode"
|
||||
@update:value="input"
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
class="col span-12"
|
||||
>
|
||||
<LabeledSelect
|
||||
v-model:value="config.bridge"
|
||||
class="mb-20"
|
||||
:label="t('harvester.network.clusterNetwork.label')"
|
||||
required
|
||||
:options="clusterNetworkOptions"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LabeledSelect
|
||||
v-if="!isOverlayNetwork"
|
||||
v-model:value="clusterBridge"
|
||||
class="mb-20"
|
||||
:label="t('harvester.network.clusterNetwork.label')"
|
||||
required
|
||||
:disabled="isEdit"
|
||||
:options="clusterNetworkOptions"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="!isUntaggedNetwork"
|
||||
v-if="isL2VlanAccessMode"
|
||||
name="layer3Network"
|
||||
:label="t('harvester.network.tabs.layer3Network')"
|
||||
:weight="98"
|
||||
@@ -305,3 +524,12 @@ export default {
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.remove-btn {
|
||||
align-self: center;
|
||||
}
|
||||
.trunk-span{
|
||||
flex: 5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { HCI } from '../types';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
|
||||
|
||||
export default {
|
||||
name: 'CreateVMSchedule',
|
||||
@@ -93,7 +94,7 @@ export default {
|
||||
return this.settings.find( (O) => O.id === 'backup-target');
|
||||
},
|
||||
isEmptyValue() {
|
||||
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
|
||||
return isBackupTargetSettingEmpty(this.backupTargetResource);
|
||||
},
|
||||
canUpdate() {
|
||||
return this?.backupTargetResource?.canUpdate;
|
||||
@@ -109,7 +110,7 @@ export default {
|
||||
!!this.value.spec.maxFailure;
|
||||
},
|
||||
isBackupTargetUnAvailable() {
|
||||
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
|
||||
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource);
|
||||
},
|
||||
vmOptions() {
|
||||
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
|
||||
@@ -166,20 +167,6 @@ export default {
|
||||
onTypeChange(newType) {
|
||||
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
|
||||
},
|
||||
getBackupTargetValueIsEmpty(resource) {
|
||||
let out = true;
|
||||
|
||||
if (resource?.value) {
|
||||
try {
|
||||
const valueJson = JSON.parse(resource?.value);
|
||||
|
||||
out = !valueJson.type;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
validateFailure(count) {
|
||||
if (this.value.spec.retain && count > this.value.spec.retain) {
|
||||
this.value.spec['maxFailure'] = this.value.spec.retain;
|
||||
@@ -198,7 +185,7 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
:validation-passed="canSave"
|
||||
@finish="save"
|
||||
@error="e=>errors = e"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<div class="banner">
|
||||
<Banner
|
||||
|
||||
@@ -84,6 +84,10 @@ export default {
|
||||
this.value['_type'] = TYPES.OPAQUE;
|
||||
}
|
||||
|
||||
if ( this.realMode === _CLONE) {
|
||||
this.value['_type'] = this.liveValue._type;
|
||||
}
|
||||
|
||||
return {
|
||||
isCloud,
|
||||
nodeDrivers: null,
|
||||
@@ -234,7 +238,7 @@ export default {
|
||||
:resource="value"
|
||||
:errors="errors"
|
||||
@finish="saveSecret"
|
||||
@error="e=>errors = e"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -4,8 +4,10 @@ import { RadioGroup } from '@components/Form/Radio';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
||||
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { DOC } from '../config/doc-links';
|
||||
import { docLink } from '../utils/feature-flags';
|
||||
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||
|
||||
@@ -15,7 +17,8 @@ export default {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup,
|
||||
TextAreaAutoGrow
|
||||
TextAreaAutoGrow,
|
||||
UnitInput
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
@@ -57,7 +60,13 @@ export default {
|
||||
|
||||
return {
|
||||
setting,
|
||||
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
|
||||
description: isHarvester ? t(
|
||||
`advancedSettings.descriptions.harv-${ this.value.id }`,
|
||||
this.getDocLinkParams()
|
||||
) : t(
|
||||
`advancedSettings.descriptions.${ this.value.id }`,
|
||||
this.getDocLinkParams()
|
||||
),
|
||||
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
||||
enumOptions,
|
||||
canReset,
|
||||
@@ -128,6 +137,13 @@ export default {
|
||||
await this.clusterRegistrationUrlTip();
|
||||
}
|
||||
|
||||
// remove any leading zeros (e.g. '0123' becomes 123)
|
||||
if (this.setting.kind === 'number' && this.value.value) {
|
||||
const num = Number(this.value.value);
|
||||
|
||||
this.value.value = isNaN(num) ? 0 : `${ num }`;
|
||||
}
|
||||
|
||||
this.save(done);
|
||||
},
|
||||
|
||||
@@ -163,6 +179,18 @@ export default {
|
||||
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
||||
this.$refs.settingComp.useDefault();
|
||||
}
|
||||
},
|
||||
getDocLinkParams() {
|
||||
const setting = HCI_ALLOWED_SETTINGS[this.value.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[this.value.id];
|
||||
|
||||
if (setting?.docPath) {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
const url = docLink(DOC[setting.docPath], version);
|
||||
|
||||
return { url };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -177,7 +205,7 @@ export default {
|
||||
:subtypes="[]"
|
||||
:can-yaml="false"
|
||||
:cancel-event="true"
|
||||
@error="e => (errors = e)"
|
||||
@error="e=>errors=e"
|
||||
@finish="saveSettings"
|
||||
@cancel="done"
|
||||
>
|
||||
@@ -238,6 +266,13 @@ export default {
|
||||
:min-height="254"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="setting.kind === 'number'">
|
||||
<LabeledInput
|
||||
v-model:value="value.value"
|
||||
:label="t('advancedSettings.edit.value')"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<LabeledInput
|
||||
v-model:value="value.value"
|
||||
@@ -261,4 +296,9 @@ export default {
|
||||
:deep() .edit-help code {
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
::v-deep(.banner__content.closable) {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
<script>
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
|
||||
import { HCI_SETTING } from '../../config/settings';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
|
||||
export default {
|
||||
name: 'CDISettings',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
provisioner: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
ArrayList,
|
||||
Checkbox,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
emits: ['update:cdiSettings'],
|
||||
|
||||
async fetch() {
|
||||
const hash = {
|
||||
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
|
||||
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
|
||||
};
|
||||
|
||||
await allHash(hash);
|
||||
|
||||
if (this.mode === _CREATE ) {
|
||||
this.setDefaultVolumeSnapshotClass();
|
||||
} else {
|
||||
this.initCDISettingsFromAnnotations();
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
cdiSettings: {
|
||||
volumeModeAccessModes: [],
|
||||
volumeSnapshotClass: null,
|
||||
cloneStrategy: null,
|
||||
filesystemOverhead: null,
|
||||
},
|
||||
defaultAddValue: { volumeMode: null, accessModes: [] },
|
||||
noneOption: { label: 'None', value: '' },
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
inStore() {
|
||||
return this.$store.getters['currentProduct'].inStore;
|
||||
},
|
||||
|
||||
allVolumeModeOptions() {
|
||||
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
|
||||
},
|
||||
|
||||
selectedVolumeModes() {
|
||||
return this.cdiSettings.volumeModeAccessModes
|
||||
.map((item) => item.volumeMode)
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
volumeModeOptions() {
|
||||
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
|
||||
},
|
||||
|
||||
allVolumeModesSelected() {
|
||||
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
|
||||
},
|
||||
|
||||
accessModeOptions() {
|
||||
return [
|
||||
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
||||
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
||||
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
||||
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
|
||||
];
|
||||
},
|
||||
|
||||
cloneStrategyOptions() {
|
||||
return [
|
||||
this.noneOption,
|
||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
|
||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
|
||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
|
||||
];
|
||||
},
|
||||
|
||||
volumeSnapshotClassOptions() {
|
||||
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
|
||||
|
||||
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
|
||||
},
|
||||
|
||||
isFilesystemOverheadValid() {
|
||||
const val = this.cdiSettings.filesystemOverhead;
|
||||
|
||||
if (val === null || val === '') return true;
|
||||
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
|
||||
|
||||
return regex.test(val);
|
||||
},
|
||||
|
||||
isCustomClass() {
|
||||
return this.mode === _CREATE || this.mode === _EDIT;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
provisioner() {
|
||||
this.resetCdiSettings();
|
||||
this.$nextTick(this.setDefaultVolumeSnapshotClass);
|
||||
},
|
||||
|
||||
cdiSettings: {
|
||||
handler(val) {
|
||||
this.$emit('update:cdiSettings', val);
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setDefaultVolumeSnapshotClass() {
|
||||
try {
|
||||
const setting = this.$store.getters[`${ this.inStore }/byId`](HCI.SETTING, HCI_SETTING.CSI_DRIVER_CONFIG);
|
||||
const config = JSON.parse(setting?.value || setting?.default || '{}');
|
||||
const defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
|
||||
|
||||
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
|
||||
|
||||
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
|
||||
this.cdiSettings.volumeSnapshotClass = null;
|
||||
}
|
||||
},
|
||||
|
||||
initCDISettingsFromAnnotations() {
|
||||
const annotations = this.value?.metadata?.annotations || {};
|
||||
|
||||
let volumeModeAccessModes = [];
|
||||
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||
|
||||
if (rawVolumeMode) {
|
||||
try {
|
||||
const parsed = JSON.parse(rawVolumeMode);
|
||||
|
||||
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
|
||||
volumeMode,
|
||||
accessModes: Array.isArray(accessModes) ? accessModes : [],
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
if (volumeModeAccessModes.length) {
|
||||
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
|
||||
}
|
||||
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
|
||||
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||
}
|
||||
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
|
||||
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||
}
|
||||
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
|
||||
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||
}
|
||||
},
|
||||
|
||||
resetCdiSettings() {
|
||||
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
|
||||
this.cdiSettings.volumeSnapshotClass = null;
|
||||
this.cdiSettings.cloneStrategy = null;
|
||||
this.cdiSettings.filesystemOverhead = null;
|
||||
},
|
||||
|
||||
validateFilesystemOverhead(e) {
|
||||
const val = e.target.value;
|
||||
|
||||
this.cdiSettings.filesystemOverhead = val;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ArrayList
|
||||
v-model:value="cdiSettings.volumeModeAccessModes"
|
||||
:initial-empty-row="true"
|
||||
:show-header="true"
|
||||
:mode="mode"
|
||||
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
|
||||
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
|
||||
:default-add-value="defaultAddValue"
|
||||
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
|
||||
:add-disabled="allVolumeModesSelected"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="column-headers">
|
||||
<div
|
||||
class="row"
|
||||
:class="{ custom: isCustomClass }"
|
||||
>
|
||||
<label
|
||||
class="col span-3 value text-label mb-10"
|
||||
for="volumeMode"
|
||||
>
|
||||
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
|
||||
</label>
|
||||
<label
|
||||
class="col span-9 value text-label mb-10"
|
||||
for="accessModes"
|
||||
>
|
||||
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #columns="{ row }">
|
||||
<div class="row">
|
||||
<div class="col span-3">
|
||||
<LabeledSelect
|
||||
id="volumeMode"
|
||||
v-model:value="row.value.volumeMode"
|
||||
:mode="mode"
|
||||
:options="volumeModeOptions"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="accessModes"
|
||||
class="col span-9"
|
||||
>
|
||||
<Checkbox
|
||||
v-for="opt in accessModeOptions"
|
||||
:key="opt.value"
|
||||
v-model:value="row.value.accessModes"
|
||||
:value-when-true="opt.value"
|
||||
:label="opt.label"
|
||||
type="checkbox"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="cdiSettings.volumeSnapshotClass"
|
||||
class="select mt-20 mb-20"
|
||||
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
|
||||
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
|
||||
:mode="mode"
|
||||
:options="volumeSnapshotClassOptions"
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="cdiSettings.cloneStrategy"
|
||||
class="select mb-20"
|
||||
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
|
||||
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
|
||||
:mode="mode"
|
||||
:options="cloneStrategyOptions"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model:value="cdiSettings.filesystemOverhead"
|
||||
class="select mb-20"
|
||||
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
|
||||
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
|
||||
:mode="mode"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
|
||||
:status="isFilesystemOverheadValid ? null : 'error'"
|
||||
@input="validateFilesystemOverhead"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.column-headers .row.custom {
|
||||
max-width: calc(100% - 75px);
|
||||
}
|
||||
|
||||
.row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
max-width: 480px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
@@ -6,11 +7,9 @@ import ArrayList from '@shell/components/form/ArrayList';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import Loading from '@shell/components/Loading';
|
||||
|
||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||
import {
|
||||
@@ -22,6 +21,7 @@ import { CSI_DRIVER } from '../../types';
|
||||
import Tags from '../../components/DiskTags';
|
||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||
import CDISettings from './CDISettings';
|
||||
|
||||
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||
|
||||
@@ -45,6 +45,7 @@ export default {
|
||||
Tabbed,
|
||||
Loading,
|
||||
Tags,
|
||||
CDISettings,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
@@ -117,6 +118,7 @@ export default {
|
||||
key: '',
|
||||
values: [],
|
||||
},
|
||||
cdiSettings: {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -124,10 +126,10 @@ export default {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER })
|
||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
||||
};
|
||||
|
||||
if (this.value.longhornV2LVMSupport) {
|
||||
@@ -197,6 +199,14 @@ export default {
|
||||
|
||||
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
||||
},
|
||||
|
||||
isCDISettingsFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
|
||||
},
|
||||
|
||||
shouldShowCDISettingsTab() {
|
||||
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -249,6 +259,10 @@ export default {
|
||||
});
|
||||
|
||||
this.formatAllowedTopoloties();
|
||||
|
||||
if (this.shouldShowCDISettingsTab) {
|
||||
this.formatCDISettings();
|
||||
}
|
||||
},
|
||||
|
||||
formatAllowedTopoloties() {
|
||||
@@ -270,6 +284,42 @@ export default {
|
||||
if (matchLabelExpressions.length > 0) {
|
||||
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
||||
}
|
||||
},
|
||||
formatCDISettings() {
|
||||
const annotations = this.value.metadata.annotations || {};
|
||||
const volumeModeAccessModes = {};
|
||||
|
||||
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
|
||||
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
|
||||
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(volumeModeAccessModes).length > 0) {
|
||||
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||
}
|
||||
|
||||
if (this.cdiSettings.volumeSnapshotClass) {
|
||||
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||
}
|
||||
|
||||
if (this.cdiSettings.cloneStrategy) {
|
||||
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||
}
|
||||
|
||||
if (this.cdiSettings.filesystemOverhead) {
|
||||
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||
}
|
||||
|
||||
this.value.metadata.annotations = annotations;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -286,7 +336,7 @@ export default {
|
||||
:validation-passed="true"
|
||||
:apply-hooks="applyHooks"
|
||||
:errors="errors"
|
||||
@error="e=>errors = e"
|
||||
@error="e=>errors=e"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
@@ -404,6 +454,19 @@ export default {
|
||||
</template>
|
||||
</ArrayList>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="shouldShowCDISettingsTab"
|
||||
name="cdiSettings"
|
||||
:label="t('harvester.storage.cdiSettings.label')"
|
||||
:weight="-2"
|
||||
>
|
||||
<CDISettings
|
||||
v-model:cdi-settings="cdiSettings"
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:provisioner="value.provisioner"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
|
||||
import { OS } from '../mixins/harvester-vm';
|
||||
import { HCI } from '../types';
|
||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
const ENCRYPT = 'encrypt';
|
||||
const DECRYPT = 'decrypt';
|
||||
@@ -110,6 +111,7 @@ export default {
|
||||
headers: {},
|
||||
fileUrl: '',
|
||||
file: '',
|
||||
HCI_ANNOTATIONS,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -167,11 +169,18 @@ export default {
|
||||
|
||||
return filteredStorages
|
||||
.map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let disabled = false;
|
||||
|
||||
if (isInternalStorageClass(s.name)) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled
|
||||
};
|
||||
}) || [];
|
||||
},
|
||||
@@ -290,7 +299,7 @@ export default {
|
||||
|
||||
const res = await this.value.save();
|
||||
|
||||
await res.uploadImage(file);
|
||||
res.uploadImage(file);
|
||||
|
||||
buttonCb(true);
|
||||
this.done();
|
||||
@@ -595,6 +604,8 @@ export default {
|
||||
:pad-left="false"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
:toggle-filter="true"
|
||||
:protected-keys="[HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME]"
|
||||
@focusKey="focusKey"
|
||||
@update:value="value.setLabels($event)"
|
||||
>
|
||||
@@ -611,7 +622,7 @@ export default {
|
||||
<input
|
||||
v-else
|
||||
v-model="row[valueName]"
|
||||
:disabled="isView"
|
||||
:disabled="isView || row[keyName] === HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME"
|
||||
:type="'text'"
|
||||
:placeholder="t('keyValue.valuePlaceholder')"
|
||||
autocorrect="off"
|
||||
|
||||
@@ -189,6 +189,16 @@ export default {
|
||||
}
|
||||
|
||||
this.value.spec['templateId'] = `${ namespace }/${ name }`;
|
||||
|
||||
// inherit labels and annotations so the VM gets them when created from the template
|
||||
this.value.spec.vm.metadata.labels = {
|
||||
...this.value.spec.vm.metadata.labels,
|
||||
...this.value.metadata.labels
|
||||
};
|
||||
this.value.spec.vm.metadata.annotations = {
|
||||
...this.value.spec.vm.metadata.annotations,
|
||||
...this.value.metadata.annotations
|
||||
};
|
||||
const res = await this.value.save();
|
||||
|
||||
await this.saveSecret(res);
|
||||
@@ -219,6 +229,7 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="saveVMT"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="templateValue"
|
||||
@@ -246,8 +257,12 @@ export default {
|
||||
:label="t('harvester.vmTemplate.tabs.basics')"
|
||||
>
|
||||
<CpuMemory
|
||||
class="mb-20"
|
||||
:cpu="cpu"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
:max-memory="maxMemory"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
:disabled="isConfig"
|
||||
@updateCpuMemory="updateCpuMemory"
|
||||
/>
|
||||
@@ -358,6 +373,26 @@ export default {
|
||||
@update:value="value.setInstanceLabels($event)"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="annotations"
|
||||
:label="t('harvester.tab.annotations')"
|
||||
:weight="-11"
|
||||
>
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.annotations.banner" />
|
||||
</Banner>
|
||||
<KeyValue
|
||||
key="annotations"
|
||||
:value="value.annotations"
|
||||
:protected-keys="value.systemAnnotations || []"
|
||||
:toggle-filter="true"
|
||||
:add-label="t('labels.addAnnotation')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@update:value="value.setAnnotations($event)"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="advanced"
|
||||
:label="t('harvester.tab.advanced')"
|
||||
|
||||
@@ -7,6 +7,7 @@ import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import Conditions from '@shell/components/form/Conditions';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { get } from '@shell/utils/object';
|
||||
@@ -23,6 +24,7 @@ import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
||||
import { GIBIBYTE } from '../utils/unit';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVolume',
|
||||
@@ -39,6 +41,7 @@ export default {
|
||||
LabeledSelect,
|
||||
LabeledInput,
|
||||
NameNsDescription,
|
||||
Conditions
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
@@ -206,11 +209,18 @@ export default {
|
||||
|
||||
storageClassOptions() {
|
||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let disabled = false;
|
||||
|
||||
if (isInternalStorageClass(s.name)) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled
|
||||
};
|
||||
}) || [];
|
||||
},
|
||||
@@ -259,7 +269,19 @@ export default {
|
||||
|
||||
rebuildStatus() {
|
||||
return this.value.longhornEngine?.status?.rebuildStatus;
|
||||
}
|
||||
},
|
||||
|
||||
isLHV2VolExpansionFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
||||
},
|
||||
|
||||
isResizeDisabled() {
|
||||
return (
|
||||
!this.isLHV2VolExpansionFeatureEnabled &&
|
||||
this.value?.isLonghornV2 &&
|
||||
this.isEdit
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -304,6 +326,10 @@ export default {
|
||||
return ['ReadWriteMany'];
|
||||
}
|
||||
|
||||
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
||||
return this.value.spec.accessModes;
|
||||
}
|
||||
|
||||
const storageClassName = this.value.spec.storageClassName;
|
||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||
|
||||
@@ -374,10 +400,12 @@ export default {
|
||||
:generate-yaml="generateYaml"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:namespaced="true"
|
||||
:name-required="false"
|
||||
:mode="mode"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
@@ -449,7 +477,7 @@ export default {
|
||||
:output-modifier="true"
|
||||
:increment="1024"
|
||||
:mode="mode"
|
||||
:disabled="value?.isLonghornV2 && isEdit"
|
||||
:disabled="isResizeDisabled"
|
||||
required
|
||||
class="mb-20"
|
||||
:suffix="GIBIBYTE"
|
||||
@@ -457,7 +485,7 @@ export default {
|
||||
/>
|
||||
|
||||
<Banner
|
||||
v-if="value?.isLonghornV2 && isEdit"
|
||||
v-if="isResizeDisabled"
|
||||
color="warning"
|
||||
>
|
||||
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
||||
@@ -567,6 +595,15 @@ export default {
|
||||
:label="t('nameNsDescription.name.label')"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="!isCreate"
|
||||
name="conditions"
|
||||
:label="t('harvester.volume.tabs.conditions')"
|
||||
class="bordered-table"
|
||||
:mode="mode"
|
||||
>
|
||||
<Conditions :value="value" />
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
@@ -64,6 +64,7 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
import { Banner } from '@components/Banner';
|
||||
import InfoBox from '@shell/components/InfoBox';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
export default {
|
||||
name: 'AccessControlList',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
Banner, InfoBox, LabeledSelect, LabeledInput
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const rows = (this.value || []).map((row) => {
|
||||
return {
|
||||
action: row.action || '',
|
||||
direction: row.direction || '',
|
||||
priority: row.priority || 0,
|
||||
match: row.match || '',
|
||||
};
|
||||
});
|
||||
|
||||
return { rows };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
actionOptions() {
|
||||
return [
|
||||
{ label: 'Allow', value: 'allow' },
|
||||
{ label: 'Drop', value: 'drop' },
|
||||
{ label: 'Pass', value: 'pass' },
|
||||
{ label: 'Reject', value: 'reject' },
|
||||
{ label: 'Allow-related', value: 'allow-related' },
|
||||
{ label: 'Allow-stateless', value: 'allow-stateless' },
|
||||
];
|
||||
},
|
||||
directionOptions() {
|
||||
return [
|
||||
{ label: 'To-lport', value: 'to-lport' },
|
||||
{ label: 'From-lport', value: 'from-lport' },
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.queueUpdate = debounce(this.update, 100);
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.rows.push({
|
||||
action: '',
|
||||
direction: '',
|
||||
priority: this.rows.length,
|
||||
match: '',
|
||||
});
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
removeRule(idx) {
|
||||
removeAt(this.rows, idx);
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.isView) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('update:value', this.rows);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Banner
|
||||
v-if="rows.length > 0"
|
||||
color="info"
|
||||
>
|
||||
<t
|
||||
k="harvester.subnet.acl.banner"
|
||||
:raw="true"
|
||||
/>
|
||||
</Banner>
|
||||
<div
|
||||
v-for="(row, idx) in rows"
|
||||
:key="idx"
|
||||
>
|
||||
<InfoBox class="box">
|
||||
<button
|
||||
type="button"
|
||||
class="role-link btn btn-sm removeBtn"
|
||||
:disabled="isView"
|
||||
@click="removeRule(idx)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
<div class="row">
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="row.action"
|
||||
:mode="mode"
|
||||
class="mt-5 ml-5"
|
||||
:options="actionOptions"
|
||||
:required="true"
|
||||
:label="t('harvester.subnet.acl.action.label')"
|
||||
:placeholder="t('harvester.subnet.acl.action.placeholder')"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="row.direction"
|
||||
:mode="mode"
|
||||
:options="directionOptions"
|
||||
class="mt-5"
|
||||
:required="true"
|
||||
:label="t('harvester.subnet.acl.direction.label')"
|
||||
:placeholder="t('harvester.subnet.acl.direction.placeholder')"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabeledInput
|
||||
v-model:value.number="row.priority"
|
||||
type="number"
|
||||
class="mb-20 mt-5"
|
||||
:max="32767"
|
||||
:min="0"
|
||||
:mode="mode"
|
||||
required
|
||||
label-key="harvester.subnet.acl.priority.label"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-11">
|
||||
<LabeledInput
|
||||
v-model:value="row.match"
|
||||
class="mb-5 ml-5"
|
||||
:mode="mode"
|
||||
required
|
||||
:placeholder="t('harvester.subnet.acl.match.placeholder')"
|
||||
label-key="harvester.subnet.acl.match.label"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoBox>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
:disabled="isView"
|
||||
@click="add()"
|
||||
>
|
||||
<t k="harvester.subnet.acl.addRule" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
position: relative;
|
||||
}
|
||||
.removeBtn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,399 @@
|
||||
<script>
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
||||
import { set } from '@shell/utils/object';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../../types';
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AccessControlList from './AccessControlList';
|
||||
|
||||
export default {
|
||||
name: 'EditSubnet',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
Banner,
|
||||
CruResource,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
NameNsDescription,
|
||||
Tab,
|
||||
RadioGroup,
|
||||
ArrayList,
|
||||
ResourceTabs,
|
||||
Loading,
|
||||
AccessControlList
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
created() {
|
||||
const vpc = this.$route.query.vpc || '';
|
||||
const enableDHCP = this.value?.spec?.enableDHCP || false;
|
||||
|
||||
set(this.value.spec, 'enableDHCP', enableDHCP);
|
||||
set(this.value, 'spec', this.value.spec || {
|
||||
cidrBlock: '',
|
||||
protocol: NETWORK_PROTOCOL.IPv4,
|
||||
provider: '',
|
||||
vpc,
|
||||
gatewayIP: '',
|
||||
excludeIps: [],
|
||||
private: false,
|
||||
enableDHCP,
|
||||
acls: []
|
||||
});
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
||||
};
|
||||
|
||||
await allHash(hash);
|
||||
},
|
||||
|
||||
computed: {
|
||||
showAllowSubnets() {
|
||||
return this.value?.spec?.private === true;
|
||||
},
|
||||
|
||||
doneLocationOverride() {
|
||||
return this.value.doneOverride;
|
||||
},
|
||||
|
||||
allowSubnetTooltip() {
|
||||
return this.t('harvester.subnet.allowSubnet.tooltip', null, true);
|
||||
},
|
||||
|
||||
excludeIPsTooltip() {
|
||||
return this.t('harvester.subnet.excludeIPs.tooltip', null, true);
|
||||
},
|
||||
|
||||
protocolOptions() {
|
||||
return Object.values(NETWORK_PROTOCOL);
|
||||
},
|
||||
provider: {
|
||||
get() {
|
||||
const raw = this.value.spec.provider;
|
||||
|
||||
if (!raw) {
|
||||
return '';
|
||||
}
|
||||
const vmNet = raw.split('.')[0] || '';
|
||||
const ns = raw.split('.')[1] || '';
|
||||
|
||||
return `${ ns }/${ vmNet }`;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
const ns = value.split('/')[0] || '';
|
||||
const vmNet = value.split('/')[1] || '';
|
||||
const provider = `${ vmNet }.${ ns }.ovn`;
|
||||
|
||||
set(this.value, 'spec.provider', provider);
|
||||
}
|
||||
},
|
||||
|
||||
providerOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vmNets = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT) || [];
|
||||
|
||||
return vmNets.filter((net) => net.vlanType === NETWORK_TYPE.OVERLAY).map((n) => ({
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
},
|
||||
|
||||
vpcOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
|
||||
|
||||
return vpcs.map((n) => ({
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.spec.enableDHCP': {
|
||||
handler(newValue) {
|
||||
if (newValue === false) {
|
||||
this.value.spec.dhcpV4Options = '';
|
||||
this.value.spec.dhcpV6Options = '';
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async saveSubnet(buttonCb) {
|
||||
const errors = [];
|
||||
const name = this.value?.metadata?.name;
|
||||
const hasEmptyAcls = this.value?.spec?.acls?.some((acl) => !acl.match || !acl.action || acl.priority === undefined || acl.priority === null);
|
||||
|
||||
try {
|
||||
if (!name) {
|
||||
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||
} else if (!this.value?.spec?.cidrBlock) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.cidrBlock.label') }, true));
|
||||
} else if (!this.value?.spec?.provider) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
|
||||
} else if (this.value.spec.excludeIps.includes('')) {
|
||||
errors.push(this.t('harvester.validation.subnet.excludeIps'));
|
||||
} else if (hasEmptyAcls) {
|
||||
errors.push(this.t('harvester.validation.subnet.aclEmptyError'));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
await this.value.save();
|
||||
buttonCb(true);
|
||||
this.done();
|
||||
} catch (e) {
|
||||
this.errors = [e];
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<CruResource
|
||||
v-else
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
:errors="errors"
|
||||
@finish="saveSubnet"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:namespaced="false"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
<ResourceTabs
|
||||
class="mt-15"
|
||||
:need-events="false"
|
||||
:need-related="false"
|
||||
:mode="mode"
|
||||
:side-tabs="true"
|
||||
>
|
||||
<Tab
|
||||
name="Basic"
|
||||
:label="t('generic.basic')"
|
||||
:weight="-1"
|
||||
class="bordered-table"
|
||||
>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.cidrBlock"
|
||||
class="mb-20"
|
||||
required
|
||||
:placeholder="t('harvester.subnet.cidrBlock.placeholder')"
|
||||
:label="t('harvester.subnet.cidrBlock.label')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.spec.protocol"
|
||||
:label="t('harvester.subnet.protocol.label')"
|
||||
:options="protocolOptions"
|
||||
required
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="provider"
|
||||
:label="t('harvester.subnet.provider.label')"
|
||||
:options="providerOptions"
|
||||
:tooltip="t('harvester.subnet.provider.tooltip')"
|
||||
required
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.spec.vpc"
|
||||
:label="t('harvester.subnet.vpc.label')"
|
||||
:options="vpcOptions"
|
||||
required
|
||||
:disabled="true"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.gateway"
|
||||
class="mb-20"
|
||||
:placeholder="t('harvester.subnet.gateway.placeholder')"
|
||||
:label="t('harvester.subnet.gateway.label')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model:value="value.spec.enableDHCP"
|
||||
name="enabled"
|
||||
:options="[true, false]"
|
||||
:label="t('harvester.subnet.dhcp.label')"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
:mode="mode"
|
||||
:tooltip="t('harvester.subnet.dhcp.tooltip')"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv4'"
|
||||
v-model:value="value.spec.dhcpV4Options"
|
||||
class="mb-20 mt-20"
|
||||
:placeholder="t('harvester.subnet.dhcp.placeholder')"
|
||||
:label="t('harvester.subnet.dhcp.v4Options')"
|
||||
:mode="mode"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv6'"
|
||||
v-model:value="value.spec.dhcpV6Options"
|
||||
:placeholder="t('harvester.subnet.dhcp.placeholder')"
|
||||
class="mb-20 mt-20"
|
||||
:label="t('harvester.subnet.dhcp.v6Options')"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Banner
|
||||
v-if="value.spec.enableDHCP"
|
||||
color="info"
|
||||
class="dhcpOption-banner"
|
||||
>
|
||||
<t
|
||||
k="harvester.subnet.dhcp.dhcpOptionBanner"
|
||||
:raw="true"
|
||||
/>
|
||||
</Banner>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model:value="value.spec.private"
|
||||
name="enabled"
|
||||
:options="[true, false]"
|
||||
:label="t('harvester.subnet.private.label')"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
:mode="mode"
|
||||
tooltip-key="harvester.subnet.private.tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ArrayList
|
||||
v-if="showAllowSubnets"
|
||||
v-model:value="value.spec.allowSubnets"
|
||||
:show-header="true"
|
||||
class="mt-20"
|
||||
:mode="mode"
|
||||
:add-label="t('harvester.subnet.allowSubnet.addSubnet')"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="box">
|
||||
<h3 class="key">
|
||||
{{ t('harvester.subnet.allowSubnet.label') }}
|
||||
<i
|
||||
v-clean-tooltip="{content: allowSubnetTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="allowSubnetTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #columns="scope">
|
||||
<div class="key">
|
||||
<input
|
||||
v-model="scope.row.value"
|
||||
:placeholder="t('harvester.subnet.allowSubnet.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
<ArrayList
|
||||
v-model:value="value.spec.excludeIps"
|
||||
:show-header="true"
|
||||
class="mt-20"
|
||||
:mode="mode"
|
||||
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="box">
|
||||
<h3 class="key">
|
||||
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||
<i
|
||||
v-clean-tooltip="{content: excludeIPsTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="excludeIPsTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #columns="scope">
|
||||
<div class="key">
|
||||
<input
|
||||
v-model="scope.row.value"
|
||||
:placeholder="t('harvester.subnet.excludeIPs.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="ACL"
|
||||
:label="t('harvester.subnet.acl.label')"
|
||||
:weight="-2"
|
||||
class="bordered-table"
|
||||
>
|
||||
<AccessControlList
|
||||
v-model:value="value.spec.acls"
|
||||
:mode="mode"
|
||||
/>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dhcpOption-banner {
|
||||
width: max-content;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { DOC } from '../../config/doc-links';
|
||||
import { docLink } from '../../utils/feature-flags';
|
||||
|
||||
export default {
|
||||
name: 'StaticRoutes',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: { Banner },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const rows = (this.value || []).map((row) => {
|
||||
return {
|
||||
cidr: row.cidr || '',
|
||||
nextHopIP: row.nextHopIP || '',
|
||||
};
|
||||
});
|
||||
|
||||
return { rows };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
showAdd() {
|
||||
return !this.isView;
|
||||
},
|
||||
|
||||
showRemove() {
|
||||
return !this.isView;
|
||||
},
|
||||
nextHopIPTooltip() {
|
||||
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
|
||||
},
|
||||
|
||||
vpcPeeringExamplesLink() {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
|
||||
return docLink(DOC.VPC_CONFIGURATION_EXAMPLES, version);
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.queueUpdate = debounce(this.update, 100);
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.rows.push({
|
||||
cidr: '',
|
||||
nextHopIP: '',
|
||||
});
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
remove(idx) {
|
||||
removeAt(this.rows, idx);
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.isView) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('update:value', this.rows);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Banner color="info">
|
||||
<t
|
||||
k="harvester.vpc.vpcPeerings.infoBanner"
|
||||
:raw="true"
|
||||
:url="vpcPeeringExamplesLink"
|
||||
/>
|
||||
</Banner>
|
||||
<div
|
||||
v-if="rows.length"
|
||||
class="static-route-row"
|
||||
>
|
||||
<div
|
||||
v-for="(row, idx) in rows"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="pool-headers cidr">
|
||||
<span class="pool-cidr">
|
||||
<t k="harvester.vpc.staticRoutes.cidr.label" />
|
||||
</span>
|
||||
<span class="pool-nextHopIP">
|
||||
<t k="harvester.vpc.staticRoutes.nextHopIP.label" />
|
||||
<i
|
||||
v-clean-tooltip="{content: nextHopIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="nextHopIPTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pool-row cidr">
|
||||
<div class="pool-cidr">
|
||||
<span v-if="isView">
|
||||
{{ row.cidr }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="row.cidr"
|
||||
type="text"
|
||||
:placeholder="t('harvester.vpc.staticRoutes.cidr.placeholder')"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="pool-nextHopIP">
|
||||
<span v-if="isView">
|
||||
{{ row.nextHopIP }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="row.nextHopIP"
|
||||
type="text"
|
||||
:placeholder="t('harvester.vpc.staticRoutes.nextHopIP.placeholder')"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="showRemove"
|
||||
type="button"
|
||||
class="btn role-link pl-0"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<t k="generic.remove" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="showAdd"
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
@click="add()"
|
||||
>
|
||||
<t k="generic.add" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pool-headers, .pool-row {
|
||||
display: grid;
|
||||
grid-column-gap: $column-gutter;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
|
||||
&.cidr {
|
||||
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
import { HCI } from '../../types';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'VpcPeerings',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: { LabeledSelect },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
vpc: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await this.$store.dispatch('harvester/findAll', { type: HCI.VPC });
|
||||
},
|
||||
|
||||
data() {
|
||||
const rows = (this.value || []).map((row) => {
|
||||
return {
|
||||
localConnectIP: row.localConnectIP || '',
|
||||
remoteVpc: row.remoteVpc || '',
|
||||
};
|
||||
});
|
||||
|
||||
return { rows };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.mode === _EDIT;
|
||||
},
|
||||
|
||||
showAdd() {
|
||||
return !this.isView;
|
||||
},
|
||||
|
||||
showRemove() {
|
||||
return !this.isView;
|
||||
},
|
||||
|
||||
remoteVpcOptions() {
|
||||
const allVpcs = this.$store.getters['harvester/all'](HCI.VPC) || [];
|
||||
|
||||
// filter self vpc if editing
|
||||
const vpcs = this.isEdit ? allVpcs.filter((v) => v.id !== this.vpc.id) : allVpcs;
|
||||
|
||||
return vpcs.map((n) => ({
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
},
|
||||
localConnectIPTooltip() {
|
||||
return this.t('harvester.vpc.vpcPeerings.localConnectIP.tooltip');
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.queueUpdate = debounce(this.update, 100);
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.rows.push({
|
||||
localConnectIP: '',
|
||||
remoteVpc: '',
|
||||
});
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
remove(idx) {
|
||||
removeAt(this.rows, idx);
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.isView) {
|
||||
return;
|
||||
}
|
||||
this.$emit('update:value', this.rows);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="rows.length"
|
||||
class="static-route-row"
|
||||
>
|
||||
<div
|
||||
v-for="(row, idx) in rows"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="pool-headers localConnectIP">
|
||||
<span class="pool-localConnectIP">
|
||||
<t k="harvester.vpc.vpcPeerings.localConnectIP.label" />
|
||||
<i
|
||||
v-clean-tooltip="{content: localConnectIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="localConnectPTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</span>
|
||||
<span class="pool-remoteVpc">
|
||||
<t k="harvester.vpc.vpcPeerings.remoteVpc.label" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="pool-row localConnectIP">
|
||||
<div class="pool-localConnectIP">
|
||||
<span v-if="isView">
|
||||
{{ row.localConnectIP }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="row.localConnectIP"
|
||||
type="text"
|
||||
:placeholder="t('harvester.vpc.vpcPeerings.localConnectIP.placeholder')"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="pool-remoteVpc">
|
||||
<span v-if="isView">
|
||||
{{ row.remoteVpc }}
|
||||
</span>
|
||||
<LabeledSelect
|
||||
v-model:value="row.remoteVpc"
|
||||
:options="remoteVpcOptions"
|
||||
:mode="mode"
|
||||
@update:value="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="showRemove"
|
||||
type="button"
|
||||
class="btn role-link pl-0"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<t k="generic.remove" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="showAdd"
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
@click="add()"
|
||||
>
|
||||
<t k="generic.add" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pool-headers, .pool-row {
|
||||
display: grid;
|
||||
grid-column-gap: $column-gutter;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
|
||||
&.localConnectIP {
|
||||
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script>
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||
import StaticRoutes from './StaticRoutes';
|
||||
import VpcPeerings from './VpcPeerings';
|
||||
import { set } from '@shell/utils/object';
|
||||
|
||||
export default {
|
||||
name: 'EditVPC',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
NameNsDescription,
|
||||
Tab,
|
||||
StaticRoutes,
|
||||
ResourceTabs,
|
||||
Loading,
|
||||
VpcPeerings
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
data() {
|
||||
set(this.value, 'spec', this.value.spec || {
|
||||
staticRoutes: [],
|
||||
vpcPeerings: [],
|
||||
});
|
||||
|
||||
return { staticRoutes: [] };
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveVpc(buttonCb) {
|
||||
const errors = [];
|
||||
|
||||
try {
|
||||
const name = this.value?.metadata?.name;
|
||||
|
||||
if (!name) {
|
||||
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
await this.value.save();
|
||||
buttonCb(true);
|
||||
this.done();
|
||||
} catch (e) {
|
||||
this.errors = [e];
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<CruResource
|
||||
v-else
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
:errors="errors"
|
||||
@finish="saveVpc"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:namespaced="false"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
<ResourceTabs
|
||||
class="mt-15"
|
||||
:mode="mode"
|
||||
:side-tabs="true"
|
||||
>
|
||||
<Tab
|
||||
name="staticRoutes"
|
||||
:label="t('harvester.vpc.staticRoutes.label')"
|
||||
:weight="-1"
|
||||
class="bordered-table"
|
||||
>
|
||||
<StaticRoutes
|
||||
v-model:value="value.spec.staticRoutes"
|
||||
class="col span-12"
|
||||
:mode="mode"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="vpcPeerings"
|
||||
:label="t('harvester.vpc.vpcPeerings.label')"
|
||||
:weight="-2"
|
||||
class="bordered-table"
|
||||
>
|
||||
<VpcPeerings
|
||||
v-model:value="value.spec.vpcPeerings"
|
||||
class="col span-12"
|
||||
:mode="mode"
|
||||
:vpc="value"
|
||||
/>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</CruResource>
|
||||
</template>
|
||||
+10
@@ -20,6 +20,10 @@ export default {
|
||||
type: String,
|
||||
default: 'create'
|
||||
},
|
||||
osType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -64,6 +68,12 @@ export default {
|
||||
},
|
||||
|
||||
watch: {
|
||||
osType(neu) {
|
||||
if (neu === 'windows') {
|
||||
this.id = '';
|
||||
}
|
||||
},
|
||||
|
||||
value(neu) {
|
||||
this.yamlScript = neu;
|
||||
},
|
||||
|
||||
@@ -28,6 +28,10 @@ export default {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
osType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
userScript: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -203,6 +207,7 @@ export default {
|
||||
<DataTemplate
|
||||
ref="userTemplate"
|
||||
:value="userScript"
|
||||
:os-type="osType"
|
||||
type="user"
|
||||
:view-code="viewCode"
|
||||
:mode="mode"
|
||||
@@ -219,6 +224,7 @@ export default {
|
||||
ref="networkTemplate"
|
||||
:value="networkScript"
|
||||
type="network"
|
||||
:os-type="osType"
|
||||
:view-code="viewCode"
|
||||
:mode="mode"
|
||||
:config-id="configNetworkId"
|
||||
|
||||
@@ -2,23 +2,44 @@
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { GIBIBYTE } from '../../utils/unit';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
import { HCI } from '../../types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI_SETTING } from '../../config/settings';
|
||||
|
||||
const DEFAULT_HOT_PLUG_TIMES = 4;
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditCpuMemory',
|
||||
|
||||
emits: ['updateCpuMemory'],
|
||||
|
||||
components: { UnitInput, InputOrDisplay },
|
||||
components: {
|
||||
UnitInput, InputOrDisplay, Checkbox
|
||||
},
|
||||
|
||||
props: {
|
||||
cpu: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
maxCpu: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
memory: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
maxMemory: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
enableHotPlug: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create',
|
||||
@@ -29,21 +50,59 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const res = await allHash({ settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) });
|
||||
|
||||
this.settings = res.settings || [];
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
localCpu: this.cpu,
|
||||
localMemory: this.memory
|
||||
localCpu: this.cpu,
|
||||
localMemory: this.memory,
|
||||
maxLocalCpu: this.maxCpu,
|
||||
maxLocalMemory: this.maxMemory,
|
||||
localEnableHotPlug: this.enableHotPlug,
|
||||
settings: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
cupDisplay() {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
cpuDisplay() {
|
||||
return `${ this.localCpu } C`;
|
||||
},
|
||||
|
||||
maxCpuDisplay() {
|
||||
return `${ this.maxLocalCpu } C`;
|
||||
},
|
||||
|
||||
memoryDisplay() {
|
||||
return `${ this.localMemory }`;
|
||||
},
|
||||
|
||||
maxMemoryDisplay() {
|
||||
return `${ this.maxLocalMemory }`;
|
||||
},
|
||||
|
||||
cpuMemoryHotplugTooltip() {
|
||||
return this.t('harvester.virtualMachine.hotplug.tooltip', { hotPlugTimes: this.maxHotplugRatio });
|
||||
},
|
||||
|
||||
isCPUMemoryHotPlugFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('cpuMemoryHotplug');
|
||||
},
|
||||
|
||||
maxHotplugRatio() {
|
||||
const maxHotPlugRatioSetting = this.settings.find((s) => s.id === HCI_SETTING.MAX_HOTPLUG_RATIO);
|
||||
const maxPlugRatio = maxHotPlugRatioSetting?.value ? parseInt(maxHotPlugRatioSetting.value, 10) : DEFAULT_HOT_PLUG_TIMES;
|
||||
|
||||
return maxPlugRatio;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -55,71 +114,161 @@ export default {
|
||||
if (neu && !neu.includes('null')) {
|
||||
this.localMemory = neu;
|
||||
}
|
||||
}
|
||||
},
|
||||
maxCpu(neu) {
|
||||
this.maxLocalCpu = neu;
|
||||
},
|
||||
maxMemory(neu) {
|
||||
if (neu && !neu.includes('null')) {
|
||||
this.maxLocalMemory = neu;
|
||||
}
|
||||
},
|
||||
enableHotPlug(neu) {
|
||||
this.localEnableHotPlug = neu;
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
change() {
|
||||
let memory = '';
|
||||
|
||||
if (String(this.localMemory).includes('Gi')) {
|
||||
memory = this.localMemory;
|
||||
hotPlugChanged(neu) {
|
||||
// If hot plug is enabled, we need to update the maxCpu and maxMemory values
|
||||
if (neu) {
|
||||
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
|
||||
} else {
|
||||
memory = `${ this.localMemory }${ GIBIBYTE }`;
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
|
||||
}
|
||||
if (memory.includes('null')) {
|
||||
memory = null;
|
||||
}
|
||||
this.$emit('updateCpuMemory', this.localCpu, memory);
|
||||
},
|
||||
|
||||
changeMemory() {
|
||||
if (this.localEnableHotPlug) {
|
||||
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||
}
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||
},
|
||||
changeCPU() {
|
||||
if (this.localEnableHotPlug) {
|
||||
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||
}
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||
},
|
||||
changeMaxCPUMemory() {
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
name="CPU"
|
||||
:value="cupDisplay"
|
||||
:mode="mode"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localCpu"
|
||||
label="CPU"
|
||||
suffix="C"
|
||||
:delay="0"
|
||||
required
|
||||
:disabled="disabled"
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
name="CPU"
|
||||
:value="cpuDisplay"
|
||||
:mode="mode"
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
class="mb-10"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localCpu"
|
||||
label="CPU"
|
||||
suffix="C"
|
||||
:delay="0"
|
||||
required
|
||||
:disabled="disabled"
|
||||
:mode="mode"
|
||||
class="mb-20"
|
||||
@update:value="changeCPU"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.memory')"
|
||||
:value="memoryDisplay"
|
||||
:mode="mode"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localMemory"
|
||||
:label="t('harvester.virtualMachine.input.memory')"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
required
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="changeMemory"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.memory')"
|
||||
:value="memoryDisplay"
|
||||
:mode="mode"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localMemory"
|
||||
:label="t('harvester.virtualMachine.input.memory')"
|
||||
<div
|
||||
v-if="isCPUMemoryHotPlugFeatureEnabled"
|
||||
class="row"
|
||||
>
|
||||
<Checkbox
|
||||
v-model:value="localEnableHotPlug"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.hotplug.title')"
|
||||
:disabled="isView"
|
||||
@update:value="hotPlugChanged"
|
||||
/>
|
||||
<i
|
||||
v-clean-tooltip="{content: cpuMemoryHotplugTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="cpuMemoryHotplugTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="localEnableHotPlug && isCPUMemoryHotPlugFeatureEnabled"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.maxCpu')"
|
||||
:value="maxCpuDisplay"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
required
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
class="mt-20"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="maxLocalCpu"
|
||||
:label="t('harvester.virtualMachine.input.maxCpu')"
|
||||
suffix="C"
|
||||
:delay="0"
|
||||
:disabled="disabled"
|
||||
:mode="mode"
|
||||
class="mt-20"
|
||||
@update:value="changeMaxCPUMemory"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.maxMemory')"
|
||||
:value="maxMemoryDisplay"
|
||||
:mode="mode"
|
||||
class="mt-20"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="maxLocalMemory"
|
||||
:label="t('harvester.virtualMachine.input.maxMemory')"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
:suffix="GIBIBYTE"
|
||||
class="mt-20"
|
||||
@update:value="changeMaxCPUMemory"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import InfoBox from '@shell/components/InfoBox';
|
||||
|
||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { clone } from '@shell/utils/object';
|
||||
@@ -14,6 +13,13 @@ export default {
|
||||
components: { InfoBox, Base },
|
||||
|
||||
props: {
|
||||
vm: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create'
|
||||
@@ -32,10 +38,15 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await this.fetchHotunplugData();
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
rows: this.addKeyId(clone(this.value)),
|
||||
nameIdx: 1
|
||||
nameIdx: 1,
|
||||
rows: this.addKeyId(clone(this.value)),
|
||||
hotunpluggableNics: new Set(),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -64,15 +75,57 @@ export default {
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
canCheckHotunplug() {
|
||||
return !!this.vm?.actions?.findHotunpluggableNics;
|
||||
},
|
||||
|
||||
vmState() {
|
||||
return this.vm?.stateDisplay;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(neu) {
|
||||
this.rows = neu;
|
||||
this.rows = this.mergeHotplugData(clone(neu));
|
||||
},
|
||||
|
||||
vmState(newState, oldState) {
|
||||
if (newState !== oldState) {
|
||||
this.fetchHotunplugData();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchHotunplugData() {
|
||||
if (!this.canCheckHotunplug) {
|
||||
this.rows = this.mergeHotplugData(clone(this.value));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await this.vm.doAction('findHotunpluggableNics');
|
||||
|
||||
this.hotunpluggableNics = new Set(resp?.interfaces || []);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to fetch hot-unpluggable NICs:', e);
|
||||
this.hotunpluggableNics = new Set();
|
||||
}
|
||||
|
||||
this.rows = this.mergeHotplugData(clone(this.value));
|
||||
},
|
||||
|
||||
mergeHotplugData(networks) {
|
||||
return (networks || []).map((network) => ({
|
||||
...network,
|
||||
isHotunpluggable: this.hotunpluggableNics.has(network.name),
|
||||
rowKeyId: network.rowKeyId || randomStr(10)
|
||||
}));
|
||||
},
|
||||
|
||||
add(type) {
|
||||
const name = this.generateName();
|
||||
|
||||
@@ -118,7 +171,11 @@ export default {
|
||||
|
||||
update() {
|
||||
this.$emit('update:value', this.rows);
|
||||
}
|
||||
},
|
||||
|
||||
unplugNIC(network) {
|
||||
this.vm.unplugNIC(network.name);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -129,17 +186,28 @@ export default {
|
||||
v-for="(row, i) in rows"
|
||||
:key="i"
|
||||
>
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="role-link remove-vol"
|
||||
@click="remove(row)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
|
||||
<h3> {{ t('harvester.virtualMachine.network.title') }} </h3>
|
||||
|
||||
<div class="box-title mb-10">
|
||||
<h3>
|
||||
{{ t('harvester.virtualMachine.network.title') }}
|
||||
</h3>
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
@click="remove(row)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
<button
|
||||
v-if="vm.hotplugNicFeatureEnabled && row.isHotunpluggable && isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
:disabled="!canCheckHotunplug"
|
||||
@click="unplugNIC(row)"
|
||||
>
|
||||
{{ t('harvester.virtualMachine.hotUnplug.detachNIC.actionLabel') }}
|
||||
</button>
|
||||
</div>
|
||||
<Base
|
||||
v-model:value="rows[i]"
|
||||
:rows="rows"
|
||||
@@ -162,16 +230,13 @@ export default {
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.infoBox{
|
||||
position: relative;
|
||||
}
|
||||
.box-title{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.remove-vol {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 16px;
|
||||
padding:0px;
|
||||
max-height: 28px;
|
||||
min-height: 28px;
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { SOURCE_TYPE } from '../../../config/harvester-map';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||
import { HCI } from '../../../types';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||
|
||||
export default {
|
||||
emits: ['update:value'],
|
||||
@@ -135,6 +136,10 @@ export default {
|
||||
pvcs() {
|
||||
return this.$store.getters['harvester/all'](PVC) || [];
|
||||
},
|
||||
|
||||
isLHV2VolExpansionFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -266,7 +271,17 @@ export default {
|
||||
|
||||
isLonghornV2(volume) {
|
||||
return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2;
|
||||
}
|
||||
},
|
||||
|
||||
isResizeDisabled(volume) {
|
||||
if (this.isLHV2VolExpansionFeatureEnabled) return false;
|
||||
if (this.isCreate || volume.newCreateId) return false;
|
||||
|
||||
const isStopped = this.vm.stateDisplay === OFF;
|
||||
const isLonghornV2 = this.isLonghornV2(volume);
|
||||
|
||||
return !isStopped || isLonghornV2;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -288,55 +303,57 @@ export default {
|
||||
v-for="(volume, i) in rows"
|
||||
:key="volume.id"
|
||||
>
|
||||
<InfoBox class="box">
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
@click="removeVolume(volume)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
<button
|
||||
v-if="volume.hotpluggable && isView"
|
||||
type="button"
|
||||
class="role-link btn remove"
|
||||
@click="unplugVolume(volume)"
|
||||
>
|
||||
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
|
||||
</button>
|
||||
<h3>
|
||||
<span
|
||||
v-if="volume.to && isVirtualType"
|
||||
class="title"
|
||||
>
|
||||
<router-link :to="volume.to">
|
||||
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
||||
</router-link>
|
||||
|
||||
<BadgeStateFormatter
|
||||
v-if="volume.pvc"
|
||||
class="ml-10 state"
|
||||
:arbitrary="true"
|
||||
:row="volume.pvc"
|
||||
:value="volume.pvc.state"
|
||||
/>
|
||||
<a
|
||||
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
||||
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
||||
class="ml-5 resource-external"
|
||||
rel="nofollow noopener noreferrer"
|
||||
target="_blank"
|
||||
:href="volume.pvc.resourceExternalLink.url"
|
||||
<InfoBox>
|
||||
<div class="box-title mb-10">
|
||||
<h3>
|
||||
<span
|
||||
v-if="volume.to && isVirtualType"
|
||||
class="title"
|
||||
>
|
||||
<i class="icon icon-external-link" />
|
||||
</a>
|
||||
</span>
|
||||
<router-link :to="volume.to">
|
||||
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
||||
</router-link>
|
||||
|
||||
<span v-else>
|
||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||
</span>
|
||||
</h3>
|
||||
<BadgeStateFormatter
|
||||
v-if="volume.pvc"
|
||||
class="ml-10 state"
|
||||
:arbitrary="true"
|
||||
:row="volume.pvc"
|
||||
:value="volume.pvc.state"
|
||||
/>
|
||||
<a
|
||||
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
||||
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
||||
class="ml-5 resource-external"
|
||||
rel="nofollow noopener noreferrer"
|
||||
target="_blank"
|
||||
:href="volume.pvc.resourceExternalLink.url"
|
||||
>
|
||||
<i class="icon icon-external-link" />
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||
</span>
|
||||
</h3>
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
@click="removeVolume(volume)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
<button
|
||||
v-if="volume.hotpluggable && isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
@click="unplugVolume(volume)"
|
||||
>
|
||||
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<component
|
||||
:is="componentFor(volume.source)"
|
||||
@@ -347,6 +364,7 @@ export default {
|
||||
:is-edit="isEdit"
|
||||
:is-view="isView"
|
||||
:is-virtual-type="isVirtualType"
|
||||
:is-resize-disabled="isResizeDisabled(rows[i])"
|
||||
:mode="mode"
|
||||
:idx="i"
|
||||
:validate-required="validateRequired"
|
||||
@@ -394,7 +412,7 @@ export default {
|
||||
:label="ucFirst(value.volumeBackups.error.message)"
|
||||
/>
|
||||
<Banner
|
||||
v-if="isLonghornV2(volume) && !isView"
|
||||
v-if="!isLHV2VolExpansionFeatureEnabled && isLonghornV2(volume) && !isView"
|
||||
color="warning"
|
||||
:label="t('harvester.volume.longhorn.disableResize')"
|
||||
/>
|
||||
@@ -479,25 +497,24 @@ export default {
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.box {
|
||||
position: relative;
|
||||
.box-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.state {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.bootOrder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+8
-8
@@ -99,7 +99,6 @@ export default {
|
||||
this.allPVCs
|
||||
.filter( (pvc) => {
|
||||
let isAvailable = true;
|
||||
let isBeingUsed = false;
|
||||
|
||||
this.rows.forEach( (O) => {
|
||||
if (O.volumeName === pvc.metadata.name) {
|
||||
@@ -111,17 +110,16 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
// already used as image volume
|
||||
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pvc.isGoldenImageVolume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
|
||||
isBeingUsed = false;
|
||||
} else if (pvc.attachVM) {
|
||||
isBeingUsed = true;
|
||||
}
|
||||
|
||||
return isAvailable && !isBeingUsed && pvc.isAvailable;
|
||||
return isAvailable && pvc.isAvailable;
|
||||
})
|
||||
.map((pvc) => {
|
||||
return {
|
||||
@@ -142,10 +140,12 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// update this.value with existing volume spec, then update to upstream component
|
||||
this.value.accessModes = pvcResource.spec.accessModes[0];
|
||||
this.value.size = pvcResource.spec.resources.requests.storage;
|
||||
this.value.storageClassName = pvcResource.spec.storageClassName;
|
||||
this.value.volumeMode = pvcResource.spec.volumeMode;
|
||||
this.update();
|
||||
},
|
||||
|
||||
'value.type'(neu) {
|
||||
|
||||
+26
-12
@@ -8,6 +8,7 @@ import { Banner } from '@components/Banner';
|
||||
import { PVC } from '@shell/config/types';
|
||||
import { formatSi, parseSi } from '@shell/utils/units';
|
||||
import { HCI } from '../../../../types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
@@ -63,13 +64,19 @@ export default {
|
||||
isVirtualType: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
isResizeDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
VOLUME_TYPE,
|
||||
HCI_ANNOTATIONS,
|
||||
InterfaceOption,
|
||||
loading: false,
|
||||
images: [],
|
||||
@@ -90,12 +97,23 @@ export default {
|
||||
},
|
||||
|
||||
imagesOption() {
|
||||
return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
|
||||
return {
|
||||
label: this.imageOptionLabel(I),
|
||||
value: I.id
|
||||
};
|
||||
});
|
||||
return this.images
|
||||
.filter((image) => {
|
||||
if (!image.isReady) return false;
|
||||
|
||||
// exclude internal images created during upgrade
|
||||
const isInternalCreatedImage =
|
||||
image.namespace === 'harvester-system' &&
|
||||
image.labels?.[HCI_ANNOTATIONS.UPGRADE];
|
||||
|
||||
return !isInternalCreatedImage;
|
||||
})
|
||||
.sort((a, b) => (a.creationTimestamp > b.creationTimestamp ? -1 : 1))
|
||||
.map((image) => ({
|
||||
label: this.imageOptionLabel(image),
|
||||
value: image.id,
|
||||
disabled: image.isImportedImage
|
||||
}));
|
||||
},
|
||||
|
||||
imageName() {
|
||||
@@ -122,10 +140,6 @@ export default {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
},
|
||||
|
||||
isLonghornV2() {
|
||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
||||
},
|
||||
|
||||
selectedImage() {
|
||||
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
||||
},
|
||||
@@ -337,7 +351,7 @@ export default {
|
||||
:label="t('harvester.fields.size')"
|
||||
:mode="mode"
|
||||
:required="validateRequired"
|
||||
:disable="isLonghornV2"
|
||||
:disabled="isResizeDisabled"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ucFirst } from '@shell/utils/string';
|
||||
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
||||
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
||||
import { GIBIBYTE } from '../../../../utils/unit';
|
||||
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
|
||||
export default {
|
||||
@@ -55,7 +56,12 @@ export default {
|
||||
isVirtualType: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
isResizeDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -118,19 +124,22 @@ export default {
|
||||
|
||||
storageClassOptions() {
|
||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let disabled = false;
|
||||
|
||||
if (isInternalStorageClass(s.name)) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled
|
||||
};
|
||||
}) || [];
|
||||
},
|
||||
|
||||
isLonghornV2() {
|
||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
||||
},
|
||||
|
||||
isLonghornStorageClass() {
|
||||
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
||||
|
||||
@@ -302,7 +311,7 @@ export default {
|
||||
:mode="mode"
|
||||
:required="validateRequired"
|
||||
:label="t('harvester.fields.size')"
|
||||
:disabled="isLonghornV2"
|
||||
:disabled="isResizeDisabled"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import { clone, set } from '@shell/utils/object';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
@@ -96,7 +96,6 @@ export default {
|
||||
templateVersionId: '',
|
||||
namePrefix: '',
|
||||
isSingle: true,
|
||||
useTemplate: false,
|
||||
isOpen: false,
|
||||
hostname,
|
||||
isRestartImmediately,
|
||||
@@ -114,13 +113,11 @@ export default {
|
||||
},
|
||||
|
||||
machineTypeOptions() {
|
||||
return [{
|
||||
label: 'None',
|
||||
value: ''
|
||||
}, {
|
||||
label: 'q35',
|
||||
value: 'q35'
|
||||
}];
|
||||
return this.machineTypes.map((type) => {
|
||||
if (!type) return { label: 'None', value: '' };
|
||||
|
||||
return { label: type, value: type };
|
||||
});
|
||||
},
|
||||
|
||||
templateOptions() {
|
||||
@@ -239,6 +236,9 @@ export default {
|
||||
}
|
||||
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION });
|
||||
const curVersion = versions.find( (V) => V.id === id);
|
||||
|
||||
if (!curVersion?.spec?.vm) return;
|
||||
|
||||
const cloneVersionVM = clone(curVersion.spec.vm);
|
||||
|
||||
delete cloneVersionVM.spec?.template?.spec?.accessCredentials;
|
||||
@@ -246,7 +246,6 @@ export default {
|
||||
delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS];
|
||||
|
||||
const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM);
|
||||
|
||||
const deleteDataSource = claimTemplate.map((volume) => {
|
||||
if (volume?.spec?.dataSource) {
|
||||
delete volume.spec.dataSource;
|
||||
@@ -255,8 +254,20 @@ export default {
|
||||
return volume;
|
||||
});
|
||||
|
||||
delete cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.MAC_ADDRESS];
|
||||
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||
|
||||
// Update instance labels, labels and annotations
|
||||
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels || {};
|
||||
this.value.metadata.labels = {
|
||||
...(curVersion.metadata?.labels || {}),
|
||||
...(cloneVersionVM.metadata?.labels || {})
|
||||
};
|
||||
this.value.metadata.annotations = {
|
||||
...(curVersion.metadata?.annotations || {}),
|
||||
...(cloneVersionVM.metadata?.annotations || {})
|
||||
};
|
||||
|
||||
this.getInitConfig({
|
||||
value: cloneVersionVM, existUserData: true, fromTemplate: true
|
||||
});
|
||||
@@ -327,6 +338,15 @@ export default {
|
||||
saveVM(buttonCb) {
|
||||
clear(this.errors);
|
||||
|
||||
this.validateCPUMemory();
|
||||
|
||||
// block create VM flow if has validation errors
|
||||
if (this.errors.length) {
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSingle) {
|
||||
this.saveSingle(buttonCb);
|
||||
} else {
|
||||
@@ -334,6 +354,18 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
validateCPUMemory() {
|
||||
const { cpu, memory } = this;
|
||||
|
||||
if ((!cpu)) {
|
||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
|
||||
}
|
||||
|
||||
if ((!memory)) {
|
||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
|
||||
}
|
||||
},
|
||||
|
||||
async saveSingle(buttonCb) {
|
||||
this.parseVM();
|
||||
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
||||
@@ -402,38 +434,44 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
_compareDisksIgnoreStorage(disksA = [], disksB = []) {
|
||||
if (disksA.length !== disksB.length) return false;
|
||||
|
||||
const sortedA = [...disksA].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
|
||||
const sortedB = [...disksB].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
|
||||
|
||||
for (let i = 0; i < sortedA.length; i++) {
|
||||
const cloneA = clone(sortedA[i]);
|
||||
const cloneB = clone(sortedB[i]);
|
||||
|
||||
// remove the storage field before comparison
|
||||
delete cloneA?.spec?.resources?.requests?.storage;
|
||||
delete cloneB?.spec?.resources?.requests?.storage;
|
||||
|
||||
if (!isEqual(cloneA, cloneB)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
_shouldPromptRestart(oldVM, newVM) {
|
||||
const specChanged = !isEqual(oldVM?.spec, newVM?.spec);
|
||||
|
||||
// check if the disk defined in annotations has changed, ignoring storage size
|
||||
const oldDisks = parseVolumeClaimTemplates(oldVM);
|
||||
const newDisks = parseVolumeClaimTemplates(newVM);
|
||||
const diskChanged = !this._compareDisksIgnoreStorage(oldDisks, newDisks);
|
||||
|
||||
return specChanged || diskChanged;
|
||||
},
|
||||
|
||||
restartVM() {
|
||||
if (this.mode !== 'edit') {
|
||||
return;
|
||||
}
|
||||
if (!this.value.isRunning) {
|
||||
return;
|
||||
}
|
||||
const cloneDeepNewVM = clone(this.value);
|
||||
if (this.mode !== 'edit' || !this.value.isRunning) return;
|
||||
|
||||
// new VM
|
||||
delete cloneDeepNewVM?.metadata;
|
||||
delete cloneDeepNewVM?.__clone;
|
||||
const needsRestart = this._shouldPromptRestart(this.cloneVM, this.value);
|
||||
|
||||
// old VM
|
||||
delete this.cloneVM?.metadata;
|
||||
delete this.cloneVM?.__clone;
|
||||
|
||||
// add empty hostDevices to old VM as CRD does not have it.
|
||||
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
|
||||
|
||||
if (devicesObj && devicesObj.hostDevices === undefined) {
|
||||
set(devicesObj, 'hostDevices', []);
|
||||
}
|
||||
|
||||
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
|
||||
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
|
||||
|
||||
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
|
||||
// only check VM is changed in form page.
|
||||
if (isEqual(oldVM, newVM)) {
|
||||
return;
|
||||
}
|
||||
// if no restart is needed (either no changes or only size changed)
|
||||
if (!needsRestart) return;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.isOpen = true;
|
||||
@@ -452,6 +490,7 @@ export default {
|
||||
if (this.isSingle) {
|
||||
if (!this.value.spec.template.spec.hostname) {
|
||||
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||
this.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +558,7 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="saveVM"
|
||||
@cancel="cancelAction"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<RadioGroup
|
||||
v-if="isCreate"
|
||||
@@ -591,9 +631,13 @@ export default {
|
||||
:label="t('harvester.virtualMachine.detail.tabs.basics')"
|
||||
>
|
||||
<CpuMemory
|
||||
class="mb-20"
|
||||
:cpu="cpu"
|
||||
:max-cpu="maxCpu"
|
||||
:memory="memory"
|
||||
:max-memory="maxMemory"
|
||||
:mode="mode"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
@updateCpuMemory="updateCpuMemory"
|
||||
/>
|
||||
|
||||
@@ -761,10 +805,31 @@ export default {
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="annotations"
|
||||
:label="t('harvester.tab.annotations')"
|
||||
:weight="-11"
|
||||
>
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.annotations.banner" />
|
||||
</Banner>
|
||||
<KeyValue
|
||||
key="annotations"
|
||||
:value="value.annotations"
|
||||
:protected-keys="value.systemAnnotations || []"
|
||||
:toggle-filter="toggler"
|
||||
:add-label="t('labels.addAnnotation')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@update:value="value.setAnnotations($event)"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="advanced"
|
||||
:label="t('harvester.tab.advanced')"
|
||||
:weight="-11"
|
||||
:weight="-12"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
@@ -834,7 +899,10 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<div
|
||||
v-if="!value.vmMachineTypeAutoFeatureEnabled"
|
||||
class="col span-6"
|
||||
>
|
||||
<LabeledSelect
|
||||
v-model:value="machineType"
|
||||
label-key="harvester.virtualMachine.input.MachineType"
|
||||
@@ -862,6 +930,7 @@ export default {
|
||||
ref="yamlEditor"
|
||||
:user-script="userScript"
|
||||
:mode="mode"
|
||||
:os-type="osType"
|
||||
:view-code="isWindows"
|
||||
:namespace="value.metadata.namespace"
|
||||
:network-script="networkScript"
|
||||
|
||||
@@ -39,6 +39,10 @@ export default {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
disableAdd() {
|
||||
return this.isStandaloneHarvester && this.rows.some((row) => row.namespace === '*');
|
||||
},
|
||||
|
||||
showAdd() {
|
||||
return !this.isView;
|
||||
},
|
||||
@@ -141,6 +145,7 @@ export default {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="disableAdd"
|
||||
class="btn role-tertiary add"
|
||||
@click="add()"
|
||||
>
|
||||
|
||||
@@ -66,17 +66,26 @@ export default {
|
||||
},
|
||||
|
||||
namespaceOptions() {
|
||||
const out = (this.filteredNamespaces || []).map((namespace) => {
|
||||
return {
|
||||
label: namespace.metadata.name,
|
||||
value: namespace.id,
|
||||
};
|
||||
});
|
||||
const namespaces = this.filteredNamespaces;
|
||||
const selected = this.rows.map((r) => r?.namespace);
|
||||
|
||||
if (this.isStandaloneHarvester) {
|
||||
return [
|
||||
{ label: this.t('generic.all'), value: '*' },
|
||||
...namespaces.map((ns) => ({
|
||||
label: ns.metadata.name,
|
||||
value: ns.id,
|
||||
disabled: selected.includes(ns.id) && ns.id !== this.row.namespace
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
return [{
|
||||
label: this.t('generic.all'),
|
||||
value: '*',
|
||||
}, ...out];
|
||||
},
|
||||
...namespaces.map((ns) => ({ label: ns.metadata.name, value: ns.id }))
|
||||
];
|
||||
},
|
||||
|
||||
guestClusterOptions() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user