Compare commits

...

39 Commits

Author SHA1 Message Date
mergify[bot]
b887e5ee01
ci: replace pull_request_target with two-step workflows (backport #841) (#854)
* ci: replace pull_request_target with two-step workflows (#841)

* ci: update PR auto assign workflows

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

* ci: update backport label workflow

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

* ci: update backport PR via mergify workflow

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

* ci: update add PR label workflow

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

* refactor: file name

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

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

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

---------

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

* ci: remove brackets in PR Management

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

---------

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



* refactor: update based on copilot feedback



* refactor: update AGENTS.md



* refactor: update based on AI suggestion



* refactor: based on comments



* refactor: some files



* refactor: boundaries.md



---------


(cherry picked from commit 67bb6dfbd5d7f3632a138626ade5040188b76394)

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

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

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

* ci: update node version to 24

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

* ci: update build catalog yaml

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

* fix: nav items order

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

* refactor: remove unneeded weightType

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

---------

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

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

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

---------

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



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




* refactor: based on feedback



---------



(cherry picked from commit c4d10183883b4e70e883733aae777c005978093e)

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

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



* refactor: extract to utils/auth.js



---------


(cherry picked from commit 6fdd1e3954ca730a082c72e558fd79a54b65084f)

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

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

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



* refactor: based on copilot review



---------


(cherry picked from commit c5b4f6cd1ee6a23d14f251b6d45d13e7c2274b2b)

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

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

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

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



* feat: add data migration action on volume page



* refactor: use show advanced options link instead of checkbox



* feat: add feature flag



* feat: add feature flag for dataMigration action



---------




(cherry picked from commit 566e79eda5c08e84e431d5e048e4bb581e7d9aaf)

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

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

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

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

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

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

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

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



* fix: network payload



---------


(cherry picked from commit d1949641a7b6f5055a0eba210196d5e83db08f1a)

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


(cherry picked from commit 9c9f59c939706edac551692694ea4ab88ec192d0)

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

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

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

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

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

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

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



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



* refactor: update with conditionally rendering



---------


(cherry picked from commit 23344e0c0759e970bbdc88bc463774053448e0a1)

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



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



---------


(cherry picked from commit a2486a7d389ff86760f8456a73ef2202ed06ca02)

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

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

View File

@ -4,7 +4,8 @@ description: Setup node and install dependencies
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- name: Setup Nodejs with yarn caching
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'

View File

@ -1,6 +0,0 @@
addAssignees: author
addReviewers: true
numberOfReviewers: 0
reviewers:
- a110605
- houhoucoop

View File

@ -34,12 +34,14 @@
{
"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"]
}

View File

@ -0,0 +1,39 @@
name: "PR Management Add PR Label Collect Data"
on:
pull_request:
types: [opened, reopened, edited]
branches:
- main
- 'release-harvester-v*'
jobs:
collect:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
permissions:
contents: read
actions: write
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: Extract PR label
run: |
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
echo "PR_LABEL=$PR_LABEL"
{
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
echo "PR_LABEL=$PR_LABEL"
} > pr-add-label-data.env
- name: Upload PR data artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pr-add-label-data
path: pr-add-label-data.env

33
.github/workflows/add-pr-label.yaml vendored Normal file
View File

@ -0,0 +1,33 @@
name: "PR Management Add PR Label"
on:
workflow_run:
workflows:
- "PR Management Add PR Label Collect Data"
types: [completed]
jobs:
auto-assign-pr-label:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
actions: read
pull-requests: write
steps:
- name: Download PR data artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: pr-add-label-data
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Load PR data
run: |
cat pr-add-label-data.env >> $GITHUB_ENV
- name: Set PR label
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "PR_LABEL = $PR_LABEL"
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$PR_LABEL"

View File

@ -0,0 +1,28 @@
name: "PR Management Auto Assign Collect Data"
on:
pull_request:
types: [opened, ready_for_review]
branches:
- main
- 'release-harvester-v*'
jobs:
collect:
runs-on: ubuntu-latest
permissions:
contents: read
actions: write
steps:
- name: Save PR data to artifact
run: |
{
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
echo "PR_AUTHOR=${{ github.event.pull_request.user.login }}"
} > pr-auto-assign-data.env
- name: Upload PR data artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pr-auto-assign-data
path: pr-auto-assign-data.env

View File

@ -1,17 +0,0 @@
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@main
with:
configuration-path: .github/auto-assign-config.yaml

35
.github/workflows/auto-assign.yaml vendored Normal file
View File

@ -0,0 +1,35 @@
name: "PR Management Auto Assign"
on:
workflow_run:
workflows:
- "PR Management Auto Assign Collect Data"
types: [completed]
jobs:
auto-assign:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
env:
REVIEWERS: "a110605,houhoucoop"
permissions:
actions: read
pull-requests: write
steps:
- name: Download PR data artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: pr-auto-assign-data
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Load PR data
run: |
cat pr-auto-assign-data.env >> $GITHUB_ENV
- name: Auto assign PR author
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "Assigning PR author: $PR_AUTHOR"
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-assignee "$PR_AUTHOR" --add-reviewer "$REVIEWERS"

View File

@ -0,0 +1,30 @@
name: "PR Management Add Labels Collect Data"
on:
pull_request:
types: [opened, reopened]
branches:
- main
- 'release-harvester-v*'
jobs:
collect:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
permissions:
contents: read
actions: write
steps:
- name: Save PR data to artifact
run: |
{
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
echo "PR_BASE_REF=${{ github.event.pull_request.base.ref }}"
echo "PR_USER_LOGIN=${{ github.event.pull_request.user.login }}"
} > pr-backport-label-data.env
- name: Upload PR data artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pr-backport-label-data
path: pr-backport-label-data.env

View File

@ -1,40 +1,44 @@
name: "[PR Management] Add Labels"
name: "PR Management Add Backport Labels"
on:
pull_request_target:
types: [opened, reopened]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
workflow_run:
workflows:
- "PR Management Add Labels Collect Data"
types: [completed]
jobs:
add-require-backport-label:
if: github.event.pull_request.draft == false &&
github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
actions: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download PR data artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
ref: ${{ github.base_ref }}
name: pr-backport-label-data
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Fetch release branches and PR labels
id: fetch_info
- name: Load PR data
run: |
cat pr-backport-label-data.env >> $GITHUB_ENV
- name: Add require-backport label (main branch PRs)
if: env.PR_BASE_REF == 'main'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
repo="${{ github.repository }}"
pr_number=${{ github.event.pull_request.number }}
pr_number="$PR_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"
echo "No release branches found, skipping."
exit 0
fi
@ -44,51 +48,36 @@ jobs:
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
if echo "$tags" | grep -Fxq "$release_tag"; then
echo "should_label=false" >> "$GITHUB_OUTPUT"
echo "Release $release_tag already published, skipping."
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)
if echo "$pr_labels" | grep -Fxq "$label"; then
echo "Label '$label' already present, skipping."
exit 0
fi
echo "Adding label: $label"
gh pr edit "$pr_number" --repo "$repo" --add-label "$label"
- name: Add backport label (release branch PRs opened by Mergify)
if: startsWith(env.PR_BASE_REF, 'release-harvester-v')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ 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 }}"
set -euo pipefail
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 }}
IS_MERGIFY=$(echo "$PR_USER_LOGIN" | grep -iq 'mergify' && echo true || echo false)
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
if [[ "$IS_MERGIFY" != "true" ]]; then
echo "PR author is not Mergify, skipping."
exit 0
fi
- 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}"
version="${PR_BASE_REF#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"
echo "Adding label: $label"
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$label"

View File

@ -0,0 +1,28 @@
name: "PR Management Request Backport via Mergify Collect Data"
on:
pull_request:
types: [closed]
branches: [main]
jobs:
collect:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: read
actions: write
steps:
- name: Save PR data to artifact
run: |
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
{
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
echo "PR_LABELS=$(echo "$labels_json" | jq -r '[.[]] | join(",")')"
} > pr-backport-request-data.env
- name: Upload PR data artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pr-backport-request-data
path: pr-backport-request-data.env

View File

@ -1,41 +1,47 @@
name: "[PR Management] Request Backport via Mergify"
name: "PR Management Request Backport via Mergify"
on:
pull_request_target:
types: [closed]
branches: [main]
permissions:
pull-requests: write
workflow_run:
workflows:
- "PR Management Request Backport via Mergify Collect Data"
types: [completed]
jobs:
comment-backport:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
actions: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download PR data artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
ref: ${{ github.base_ref }}
name: pr-backport-request-data
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Load PR data
run: |
cat pr-backport-request-data.env >> $GITHUB_ENV
- name: Post Mergify backport command
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
pr_number=${{ github.event.pull_request.number }}
pr_number="$PR_NUMBER"
repo="${{ github.repository }}"
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
labels=$(echo "$labels_json" | jq -r '.[] // empty')
labels=$(echo "$PR_LABELS" | tr ',' '\n')
echo "Labels on PR: $labels"
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
if [[ -z "$matches" ]]; then
echo "No backport labels found — skipping."
echo "No back-port labels found — skipping."
exit 0
fi

View File

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version
env:

View File

@ -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}}

View File

@ -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,24 +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
with:
node-version: '20'
cache: yarn
- name: Install dependencies
run: yarn
- name: Setup Nodejs with yarn install
uses: ./.github/actions/setup
- name: Build and push UI image
run: |

View File

@ -13,7 +13,7 @@ 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
@ -44,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
@ -62,7 +62,13 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Node from .nvmrc
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Setup environment
run: |
@ -70,7 +76,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
@ -79,7 +85,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
@ -94,7 +100,7 @@ jobs:
contents: write
steps:
- name: Checkout release branch
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: '${{ github.ref_name }}'
@ -105,7 +111,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 }}'
@ -121,7 +127,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
@ -132,7 +138,7 @@ jobs:
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
- name: Run Helm chart releaser
uses: helm/chart-releaser-action@v1.7.0
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
with:
charts_dir: ./charts
env:

View File

@ -17,7 +17,7 @@ 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
@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version
env:
TAG_VERSION: ${{ github.event.release.tag_name }}
@ -43,7 +43,7 @@ jobs:
needs:
- setup-release-tag
- check-version
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@3b26a36bad555e5e2b8634b24823be29732f287c # master
permissions:
actions: write
contents: write

View File

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version
env:
TAG_VERSION: ${{github.ref_name}}

View File

@ -20,13 +20,13 @@ jobs:
# 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@main
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@main
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

View File

@ -1,30 +0,0 @@
name: "[PR Management] Add PR Label"
on:
pull_request_target:
types: [opened, reopened, edited]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
auto-assign-pr-label:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Setup Nodejs and yarn install
uses: ./.github/actions/setup
- name: Set PR label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
echo "PR_LABEL = $PR_LABEL"
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"

View File

@ -16,7 +16,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # Need full history for commit-lint

2
.nvmrc
View File

@ -1 +1 @@
20
24

261
AGENTS.md Normal file
View File

@ -0,0 +1,261 @@
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
# Project Overview
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the 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.
```
# Personas
## Software Developer
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
- **Focus**: Writing clean, maintainable, and performant code.
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.
# Agents
## Boundaries
- ✅ **Always:**
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
- Make commits in a new branch (for a PR).
- Run `yarn lint:fix` before commits.
- Use conventional commit format:
```
<type>:
<description>
```
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
- ⚠️ **Ask first:**
- Adding dependencies
- Upgrading dependencies
- 🚫 **Never:**
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
- Edit `node_modules/`.
- Commit directly to `main` (use PRs).
- Skip git hooks with `--no-verify` flag.
## Tools
### Prerequisites
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
- **Package manager**: `yarn` (v1 classic)
### Common Commands
| Command | Description |
|---------|-------------|
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
| `yarn build-pkg harvester` | Build the Harvester extension package |
| `yarn serve-pkgs` | Serve the locally built extension for testing |
| `yarn lint` | Run ESLint (zero warnings allowed) |
| `yarn lint:fix` | Run ESLint with auto-fix |
| `yarn clean` | Clean build artifacts |
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
### Development
- **Start dev server**:
```bash
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
```
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
- The dashboard will be available at `https://127.0.0.1:8005`.
### Building
- **Build extension package**:
```bash
yarn build-pkg harvester
```
- **Serve locally built extension** (for integration testing with a Rancher instance):
```bash
yarn serve-pkgs
```
### Linting
- **Check** (must pass with zero warnings):
```bash
yarn lint
```
- **Auto-fix**:
```bash
yarn lint:fix
```
- ESLint covers `.js`, `.ts`, and `.vue` files.
- Always run `yarn lint:fix` before committing.
### Commit Conventions
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
### Agent Documentation
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
- After editing any source file, regenerate the root `AGENTS.md`:
```bash
yarn agents:generate
```
# Contributors Guide
## Getting Started
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
To get started, follow the `Development Setup` section.
## Project Information
- **Tech Stack:**
- `Vue.js`: Framework
- `Linting`: ESLint
- `CSS`: SCSS should be used
- `TypeScript`: Primary language for logic.
- **Code Style and Standards:**
- `Language`: TypeScript is preferred for new code.
- `Vue.js`:
- Composition API components are preferred over Options API.
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
- Place source tag above template above style.
- style tag should contain `lang='scss' scoped`.
- `Linting`: Follow the ESLint configuration in the root.
- **File Structure:**
- `.github/`: CI/CD workflows and Renovate config.
- `docs/`: Documentation source for `AGENTS.md` generation.
- `scripts/`: Bash scripts for build, CI and doc generation.
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
- `l10n/`: Localization (`en-us.yaml`).
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
- `styles/`: Global SCSS files.
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
## Harvester UI Extension Development Guide
1. Backward Compatibility
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
2. Implementation Steps for New Features
To add a feature in a new release, follow these steps:
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
Check: Use the following getter to verify if the feature is enabled for the current version:
```
computed: {
newFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
},
},
```
Render: Use the result of the check to conditionally render the UI components.
## E2E Tests (Cypress)
See https://github.com/harvester/harvester-ui-tests
## Node Dependencies
Dependencies are managed via `package.json` and `yarn`
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
For the Renovate config, see `.github/renovate.json`.
## Milestone guidance
- All issues must first be resolved in the `main` branch
- If backports are needed they can be made via the backport bot
- pull requests
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
- All backported pull requests must link to a backported issue
## Creating a branch
### To resolve an issue
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
- `main` for the latest unreleased minor version
- `release-harvester-v.X` for release minor versions
- `release-harvester-v1.6`
- `release-harvester-v1.7`
- `release-harvester-v1.8`
- Ensure you have the latest of that branch `git pull --rebase`
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
## Creating a commit
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 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
## Creating a Pull Request
- Pull requests must come from forks
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
- A Pull Request will only be merged once
- ALL CI gates have passed
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR

2
CODEOWNERS Normal file
View File

@ -0,0 +1,2 @@
@a110605
@houhoucoop

View File

@ -7,7 +7,7 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
## 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).
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
## Development Setup

63
docs/agents.md/README Normal file
View File

@ -0,0 +1,63 @@
# Agents.md
This folder contains files used to construct a single root `AGENTS.md`
We do this because
- The single commonality between almost all AI tools is that they look for a root `AGENTS.md`
- Having the information in a single place for all AI tools is better than duplicating and maintaining the information in multiple files for each AI tool
## Process
1. Add files to or update files in `./docs/agents.md/agents`, `./docs/agents.md/personas`, `./docs/agents.md/contributors`.
2. Each file must
- Start markdown headers at the `##` level
- End in a new line
- Order contents in AGENTS.md by giving an optional number prefix
3. Run `yarn agents:generate`.
- Take the contents of `./docs/agents.md/template_agents.md`
- Insert the contents of the agents, personas and contributors files
- Output to `./AGENTS.md`
4. Commit all changes, including root `./AGENTS.md` and any files that link to it
## Guidance
### Examples
When referencing code or configuration it's good to include an example
## Future Tasks
- Add real world examples when referencing code
- Create a GH workflow to monitor `agents.md` folder files in a PR, if changed run generate script and add new AGENTS.md to PR
- Discuss
- Three multi AI tool related questions
- Questions
- how AI specific patterns, like co-pilot instructions, can be expressed in a way to be adopted by or reduce duplication with the root agents.md
- how AI tool specific metadata code can be included
- multiple agents vs personas approach works
- Proposal
- Generic information applicable to all AI tools is added to the root `agents.md`
- Agent, instruction or other AI specific files or patterns can be used in files specific to that AI tool. They must not contain duplicate information to the root `agents.md`
- The root `agents.md` continues to be AI tool agnostic
- How can folder specific agents be used
- Proposal
- `agents.md` are added to folders as long as most LLMs can make use of them and they do not duplicate information in the root `agents.md`
## Test Prompts
To validate LLMs are picking up information consider running the following prompts.
### Information
Can they provide correct information?
- `what's this project about?`
- `can you tell me the process for fixing a github issue in this repo?`
- `what makes a good software developer?`
- `what should never be done in this project?`
- `what are the folders in this project for?`
- `what are the env vars i could use when running e2e tests?`
### Actions
Can they use the information in their actions?
Pending

View File

@ -0,0 +1,23 @@
## Boundaries
- ✅ **Always:**
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
- Make commits in a new branch (for a PR).
- Run `yarn lint:fix` before commits.
- Use conventional commit format:
```
<type>:
<description>
```
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
- ⚠️ **Ask first:**
- Adding dependencies
- Upgrading dependencies
- 🚫 **Never:**
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
- Edit `node_modules/`.
- Commit directly to `main` (use PRs).
- Skip git hooks with `--no-verify` flag.

View File

@ -0,0 +1,65 @@
## Tools
### Prerequisites
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
- **Package manager**: `yarn` (v1 classic)
### Common Commands
| Command | Description |
|---------|-------------|
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
| `yarn build-pkg harvester` | Build the Harvester extension package |
| `yarn serve-pkgs` | Serve the locally built extension for testing |
| `yarn lint` | Run ESLint (zero warnings allowed) |
| `yarn lint:fix` | Run ESLint with auto-fix |
| `yarn clean` | Clean build artifacts |
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
### Development
- **Start dev server**:
```bash
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
```
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
- The dashboard will be available at `https://127.0.0.1:8005`.
### Building
- **Build extension package**:
```bash
yarn build-pkg harvester
```
- **Serve locally built extension** (for integration testing with a Rancher instance):
```bash
yarn serve-pkgs
```
### Linting
- **Check** (must pass with zero warnings):
```bash
yarn lint
```
- **Auto-fix**:
```bash
yarn lint:fix
```
- ESLint covers `.js`, `.ts`, and `.vue` files.
- Always run `yarn lint:fix` before committing.
### Commit Conventions
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
### Agent Documentation
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
- After editing any source file, regenerate the root `AGENTS.md`:
```bash
yarn agents:generate
```

View File

@ -0,0 +1,70 @@
## Getting Started
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
To get started, follow the `Development Setup` section.
## Project Information
- **Tech Stack:**
- `Vue.js`: Framework
- `Linting`: ESLint
- `CSS`: SCSS should be used
- `TypeScript`: Primary language for logic.
- **Code Style and Standards:**
- `Language`: TypeScript is preferred for new code.
- `Vue.js`:
- Composition API components are preferred over Options API.
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
- Place source tag above template above style.
- style tag should contain `lang='scss' scoped`.
- `Linting`: Follow the ESLint configuration in the root.
- **File Structure:**
- `.github/`: CI/CD workflows and Renovate config.
- `docs/`: Documentation source for `AGENTS.md` generation.
- `scripts/`: Bash scripts for build, CI and doc generation.
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
- `l10n/`: Localization (`en-us.yaml`).
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
- `styles/`: Global SCSS files.
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
## Harvester UI Extension Development Guide
1. Backward Compatibility
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
2. Implementation Steps for New Features
To add a feature in a new release, follow these steps:
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
Check: Use the following getter to verify if the feature is enabled for the current version:
```
computed: {
newFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
},
},
```
Render: Use the result of the check to conditionally render the UI components.

View File

@ -0,0 +1,3 @@
## E2E Tests (Cypress)
See https://github.com/harvester/harvester-ui-tests

View File

@ -0,0 +1,12 @@
## Node Dependencies
Dependencies are managed via `package.json` and `yarn`
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
For the Renovate config, see `.github/renovate.json`.

View File

@ -0,0 +1,58 @@
## Milestone guidance
- All issues must first be resolved in the `main` branch
- If backports are needed they can be made via the backport bot
- pull requests
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
- All backported pull requests must link to a backported issue
## Creating a branch
### To resolve an issue
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
- `main` for the latest unreleased minor version
- `release-harvester-v.X` for release minor versions
- `release-harvester-v1.6`
- `release-harvester-v1.7`
- `release-harvester-v1.8`
- Ensure you have the latest of that branch `git pull --rebase`
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
## Creating a commit
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 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
## Creating a Pull Request
- Pull requests must come from forks
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
- A Pull Request will only be merged once
- ALL CI gates have passed
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR

View File

@ -0,0 +1,5 @@
## Software Developer
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
- **Focus**: Writing clean, maintainable, and performant code.
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.

View File

@ -0,0 +1,22 @@
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
# Project Overview
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the 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.
```
# Personas
`<personas>`
# Agents
`<agents>`
# Contributors Guide
`<contributors>`

View File

@ -1,13 +1,13 @@
{
"name": "harvester-ui-extension",
"version": "1.8.0-dev",
"version": "1.8.1-dev",
"private": false,
"engines": {
"node": ">=20.0.0"
"node": ">=24.0.0"
},
"dependencies": {
"@babel/plugin-transform-class-static-block": "7.28.6",
"@rancher/shell": "3.0.9-rc.6",
"@rancher/shell": "3.0.12-rc.1",
"@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5",
@ -21,19 +21,19 @@
"yaml": "^2.5.1"
},
"resolutions": {
"@types/node": "~20.19.0",
"@types/node": "25.6.0",
"cronstrue": "2.59.0",
"d3-color": "3.1.0",
"ejs": "3.1.10",
"follow-redirects": "1.15.11",
"follow-redirects": "1.16.0",
"glob": "7.2.3",
"glob-parent": "6.0.2",
"json5": "2.2.3",
"@types/lodash": "4.17.24",
"merge": "2.1.1",
"node-forge": "1.3.3",
"node-forge": "1.4.0",
"nth-check": "2.1.1",
"qs": "6.15.0",
"qs": "6.15.1",
"roarr": "7.21.4",
"semver": "7.7.4",
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
@ -49,7 +49,8 @@
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
"commitlint": "commitlint --edit",
"prepare": "husky"
"prepare": "husky",
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
},
"devDependencies": {
"@commitlint/load": "^19.8.1",

View File

@ -7,8 +7,7 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
## 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).
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
## Development Setup

View File

@ -0,0 +1,104 @@
<script>
import UnitInput from '@shell/components/form/UnitInput';
import { Banner } from '@components/Banner';
export default {
name: 'HarvesterInstanceManagerResources',
components: {
UnitInput,
Banner,
},
props: {
value: {
type: Object,
default: () => ({
value: '',
default: '{}'
})
},
},
data() {
const resources = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
return {
resources,
parseError: null,
};
},
methods: {
parseJSON(string) {
try {
return JSON.parse(string);
} catch (e) {
this.parseError = this.t('harvester.setting.instanceManagerResources.parseError', { error: e.message });
return null;
}
},
update() {
if (!this.value) return;
const cpu = { ...this.resources?.cpu };
if (cpu.v1 !== null && cpu.v1 !== undefined) cpu.v1 = String(cpu.v1);
if (cpu.v2 !== null && cpu.v2 !== undefined) cpu.v2 = String(cpu.v2);
this.value.value = JSON.stringify({ ...this.resources, cpu });
},
useDefault() {
if (this.value?.default) {
this.resources = this.parseJSON(this.value.default) || {};
this.update();
}
},
},
};
</script>
<template>
<div>
<Banner
v-if="parseError"
color="error"
>
{{ parseError }}
</Banner>
<div class="row">
<div class="col span-12">
<UnitInput
v-model:value="resources.cpu.v1"
:label="t('harvester.setting.instanceManagerResources.v1')"
suffix="%"
:delay="0"
type="number"
min="0"
max="100"
required
:mode="mode"
class="mb-20"
@update:value="update"
/>
<UnitInput
v-model:value="resources.cpu.v2"
:label="t('harvester.setting.instanceManagerResources.v2')"
suffix="%"
:delay="0"
type="number"
min="0"
max="100"
required
:mode="mode"
class="mb-20"
@update:value="update"
/>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,371 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { RadioGroup } from '@components/Form/Radio';
import ArrayList from '@shell/components/form/ArrayList';
import { isValidCIDR } from '@shell/utils/validators/cidr';
import { _EDIT } from '@shell/config/query-params';
import { Banner } from '@components/Banner';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types';
import { NETWORK_TYPE } from '../../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
const SHARE_STORAGE_NETWORK = 'share-storage-network';
const NETWORK = 'network';
const DEFAULT_DEDICATED_NETWORK = {
vlan: '',
clusterNetwork: '',
range: '',
exclude: [],
};
export default {
name: 'RwxNetworkSetting',
components: {
RadioGroup,
Banner,
ArrayList,
LabeledInput,
LabeledSelect,
},
props: {
registerBeforeHook: {
type: Function,
required: true,
},
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => {
return {};
},
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
});
},
data() {
let enabled = false; // enabled / disabled options
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
let networkType = L2VLAN;
let exclude = [];
try {
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
if (parsedValue && typeof parsedValue === 'object') {
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
dedicatedNetwork = {
vlan: parsedNetwork.vlan || '',
clusterNetwork: parsedNetwork.clusterNetwork || '',
range: parsedNetwork.range || '',
};
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
}
} catch (error) {
enabled = false;
shareStorageNetwork = false;
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
}
return {
enabled,
shareStorageNetwork,
dedicatedNetwork,
networkType,
exclude,
defaultAddValue: '',
};
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
computed: {
showDedicatedNetworkConfig() {
return this.enabled && !this.shareStorageNetwork;
},
showVlan() {
return this.networkType === L2VLAN;
},
networkTypes() {
return [L2VLAN, UNTAGGED];
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
return clusterNetworksOptions.map((network) => {
const disabled = !network.isReadyForStorageNetwork;
return {
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
value: network.id,
disabled,
};
});
},
},
methods: {
onUpdateEnabled() {
if (!this.enabled) {
this.shareStorageNetwork = false;
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
}
this.update();
},
onUpdateNetworkType() {
if (this.shareStorageNetwork) {
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
}
this.update();
},
onUpdateDedicatedType(neu) {
this.dedicatedNetwork.clusterNetwork = '';
if (neu === L2VLAN) {
this.dedicatedNetwork.vlan = '';
} else {
delete this.dedicatedNetwork.vlan;
}
this.update();
},
inputVlan(neu) {
if (neu === '') {
this.dedicatedNetwork.vlan = '';
this.update();
return;
}
const newValue = Number(neu);
if (newValue > 4094) {
this.dedicatedNetwork.vlan = 4094;
} else if (newValue < 1) {
this.dedicatedNetwork.vlan = 1;
} else {
this.dedicatedNetwork.vlan = newValue;
}
this.update();
},
useDefault() {
this.enabled = false;
this.shareStorageNetwork = false;
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
this.update();
},
update() {
const value = { [SHARE_STORAGE_NETWORK]: false };
if (this.enabled && this.shareStorageNetwork) {
value[SHARE_STORAGE_NETWORK] = true;
}
if (this.showDedicatedNetworkConfig) {
value[NETWORK] = {};
if (this.showVlan) {
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
}
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
value[NETWORK].range = this.dedicatedNetwork.range;
const excludeList = this.exclude.filter((ip) => ip);
if (Array.isArray(excludeList) && excludeList.length > 0) {
value[NETWORK].exclude = excludeList;
}
}
this.value.value = JSON.stringify(value);
},
willSave() {
this.update();
if (!this.showDedicatedNetworkConfig) {
return Promise.resolve();
}
const errors = [];
if (this.showVlan && !this.dedicatedNetwork.vlan) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
}
if (!this.dedicatedNetwork.clusterNetwork) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
}
if (!this.dedicatedNetwork.range) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
}
if (this.exclude) {
const hasInvalidCIDR = this.exclude.find((cidr) => {
return cidr && !isValidCIDR(cidr);
});
if (hasInvalidCIDR) {
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
}
}
if (errors.length > 0) {
return Promise.reject(errors);
}
return Promise.resolve();
},
},
};
</script>
<template>
<div :class="mode">
<Banner color="warning">
<t
k="harvester.setting.rwxNetwork.warning"
:raw="true"
/>
</Banner>
<RadioGroup
v-model:value="enabled"
class="mb-20"
name="rwx-network-enable"
:options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="onUpdateEnabled"
/>
<RadioGroup
v-if="enabled"
v-model:value="shareStorageNetwork"
class="mb-20"
name="rwx-network-type"
:options="[true,false]"
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
@update:value="onUpdateNetworkType"
/>
<Banner
v-if="shareStorageNetwork"
class="mb-20"
color="warning"
>
<t
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
:raw="true"
/>
</Banner>
<template v-if="showDedicatedNetworkConfig">
<LabeledSelect
v-model:value="networkType"
class="mb-20"
:options="networkTypes"
:mode="mode"
:label="t('harvester.fields.type')"
required
@update:value="onUpdateDedicatedType"
/>
<LabeledInput
v-if="showVlan"
v-model:value.number="dedicatedNetwork.vlan"
type="number"
class="mb-20"
:mode="mode"
required
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan"
/>
<LabeledSelect
v-model:value="dedicatedNetwork.clusterNetwork"
label-key="harvester.setting.storageNetwork.clusterNetwork"
class="mb-20"
required
:options="clusterNetworkOptions"
@update:value="update"
/>
<LabeledInput
v-model:value="dedicatedNetwork.range"
class="mb-5"
:mode="mode"
required
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
label-key="harvester.setting.storageNetwork.range.label"
@update:value="update"
/>
<ArrayList
v-model:value="exclude"
:show-header="true"
:default-add-value="defaultAddValue"
:mode="mode"
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
class="mt-20"
@update:value="update"
>
<template #column-headers>
<div class="box mb-10">
<div class="key">
{{ t('harvester.setting.storageNetwork.exclude.label') }}
</div>
</div>
</template>
<template #columns="scope">
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
@update:value="update"
/>
</div>
</template>
</ArrayList>
</template>
</div>
</template>

View File

@ -61,7 +61,11 @@ const FEATURE_FLAGS = {
'v1.8.0': [
'hotplugCdRom',
'supportBundleFileNameSetting',
'clusterRegistrationTLSVerify'
'clusterRegistrationTLSVerify',
'vGPUAsPCIDevice',
'instanceManagerResourcesSetting',
'rwxNetworkSetting',
'createPVCWithDataVolume'
],
};

View File

@ -81,7 +81,6 @@ export function init($plugin, store) {
configureType,
virtualType,
weightGroup,
weightType,
} = $plugin.DSL(store, PRODUCT_NAME);
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
@ -168,7 +167,7 @@ export function init($plugin, store) {
group: 'Root',
name: HCI.HOST,
namespaced: true,
weight: 399,
weight: 499,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.HOST }
@ -200,7 +199,7 @@ export function init($plugin, store) {
group: 'root',
name: HCI.VM,
namespaced: true,
weight: 299,
weight: 498,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VM }
@ -361,7 +360,7 @@ export function init($plugin, store) {
ifHaveType: PVC,
name: HCI.VOLUME,
namespaced: true,
weight: 199,
weight: 497,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VOLUME }
@ -387,7 +386,7 @@ export function init($plugin, store) {
group: 'root',
name: HCI.IMAGE,
namespaced: true,
weight: 198,
weight: 496,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IMAGE }
@ -402,7 +401,7 @@ export function init($plugin, store) {
group: 'root',
namespaced: true,
name: 'projects-namespaces',
weight: 98,
weight: 495,
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
exact: true,
});
@ -414,7 +413,7 @@ export function init($plugin, store) {
labelKey: 'harvester.namespace.label',
name: NAMESPACE,
namespaced: true,
weight: 89,
weight: 495,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: NAMESPACE }
@ -592,9 +591,8 @@ export function init($plugin, store) {
'backupAndSnapshot'
);
weightGroup('networks', 300, true);
weightType(NAMESPACE, 299, true);
weightGroup('backupAndSnapshot', 289, true);
weightGroup('networks', 494, true);
weightGroup('backupAndSnapshot', 493, true);
basicType(
[

View File

@ -28,6 +28,7 @@ export const HCI = {
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
IMAGE_NAME: 'harvesterhci.io/image-name',
@ -79,4 +80,5 @@ export const HCI = {
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
MAC_ADDRESS: 'harvesterhci.io/mac-address',
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
};

View File

@ -20,6 +20,7 @@ export const HCI_SETTING = {
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
STORAGE_NETWORK: 'storage-network',
RWX_NETWORK: 'rwx-network',
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
SSL_CERTIFICATES: 'ssl-certificates',
SSL_PARAMETERS: 'ssl-parameters',
@ -39,7 +40,8 @@ export const HCI_SETTING = {
VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
KUBEVIRT_MIGRATION: 'kubevirt-migration'
KUBEVIRT_MIGRATION: 'kubevirt-migration',
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
};
export const HCI_ALLOWED_SETTINGS = {
@ -80,6 +82,9 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.STORAGE_NETWORK]: {
kind: 'custom', from: 'import', canReset: true
},
[HCI_SETTING.RWX_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
},
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: {
@ -122,6 +127,9 @@ export const HCI_ALLOWED_SETTINGS = {
},
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
},
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
}
};

View File

@ -41,3 +41,8 @@ export const VMIMPORT_SOURCE_KINDS = {
OPENSTACK: 'OpenstackSource',
OVA: 'OvaSource',
};
export const CDI_POPULATOR_KIND = {
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
};

View File

@ -4,6 +4,7 @@ import { Card } from '@components/Card';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types';
import { getHarvesterUserName } from '../utils/auth';
export default {
name: 'HarvesterEnablePciPassthrough',
@ -34,16 +35,7 @@ export default {
},
async save(buttonCb) {
// isSingleProduct == this is a standalone Harvester cluster
const isSingleProduct = this.$store.getters['isSingleProduct'];
let userName = 'admin';
// if this is imported Harvester, there may be users other than 'admin
if (!isSingleProduct) {
const user = this.$store.getters['auth/v3User'];
userName = user?.username || user?.id;
}
const userName = getHarvesterUserName(this.$store.getters);
for (let i = 0; i < this.resources.length; i++) {
const actionResource = this.resources[i];

View File

@ -4,6 +4,7 @@ import { Card } from '@components/Card';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types';
import { getHarvesterUserName } from '../utils/auth';
export default {
name: 'HarvesterEnableUSBPassthrough',
@ -34,16 +35,7 @@ export default {
},
async save(buttonCb) {
// isSingleProduct == this is a standalone Harvester cluster
const isSingleProduct = this.$store.getters['isSingleProduct'];
let userName = 'admin';
// if this is imported Harvester, there may be users other than 'admin
if (!isSingleProduct) {
const user = this.$store.getters['auth/v3User'];
userName = user?.username || user?.id;
}
const userName = getHarvesterUserName(this.$store.getters);
for (let i = 0; i < this.resources.length; i++) {
const actionResource = this.resources[i];

View File

@ -0,0 +1,183 @@
<script>
import { mapGetters } from 'vuex';
import { STORAGE_CLASS } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { sortBy } from '@shell/utils/sort';
import { isInternalStorageClass } from '../utils/storage-class';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput';
import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterDataMigrationDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, LabeledInput, LabeledSelect
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
this.storageClasses = await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
},
data() {
return {
targetVolumeName: '',
targetStorageClassName: '',
errors: [],
storageClasses: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
storageClassOptions() {
return sortBy(
this.storageClasses
.filter((sc) => !isInternalStorageClass(sc.metadata?.name))
.map((sc) => ({
label: sc.metadata?.name,
value: sc.metadata?.name
})),
'label'
);
},
disableSave() {
return !this.targetVolumeName || !this.targetStorageClassName;
},
},
methods: {
close() {
this.targetVolumeName = '';
this.targetStorageClassName = '';
this.errors = [];
this.$emit('close');
},
async apply(buttonDone) {
if (!this.actionResource) {
buttonDone(false);
return;
}
if (!this.targetVolumeName) {
const name = this.t('harvester.modal.dataMigration.fields.targetVolumeName.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
if (!this.targetStorageClassName) {
const name = this.t('harvester.modal.dataMigration.fields.targetStorageClassName.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
try {
await this.actionResource.doAction('dataMigration', {
targetVolumeName: this.targetVolumeName,
targetStorageClassName: this.targetStorageClassName
}, {}, false);
buttonDone(true);
this.close();
} catch (err) {
const error = err?.data || err;
this['errors'] = exceptionToErrorsArray(error);
buttonDone(false);
}
},
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.dataMigration.title') }}
</template>
<template #body>
<LabeledInput
v-model:value="targetVolumeName"
:label="t('harvester.modal.dataMigration.fields.targetVolumeName.label')"
:placeholder="t('harvester.modal.dataMigration.fields.targetVolumeName.placeholder')"
class="mb-20"
required
/>
<LabeledSelect
v-model:value="targetStorageClassName"
:label="t('harvester.modal.dataMigration.fields.targetStorageClassName.label')"
:placeholder="t('harvester.modal.dataMigration.fields.targetStorageClassName.placeholder')"
:options="storageClassOptions"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
#actions
class="actions"
>
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="disableSave"
@click="apply"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>

View File

@ -0,0 +1,239 @@
<script>
import { mapGetters } from 'vuex';
import { PVC } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { sortBy } from '@shell/utils/sort';
import { HCI } from '../types';
import { parseVolumeClaimTemplates } from '@pkg/harvester/utils/vm';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterStorageMigrationDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, LabeledSelect
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
},
data() {
return {
sourceVolume: '',
targetVolume: '',
errors: [],
allPVCs: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
sourceVolumeOptions() {
const volumes = this.actionResource.spec?.template?.spec?.volumes || [];
return sortBy(
volumes
.map((v) => v.persistentVolumeClaim?.claimName)
.filter((name) => !!name)
.map((name) => ({
label: name,
value: name
})),
'label'
);
},
namespacePVCs() {
return this.allPVCs.filter((pvc) => pvc.metadata.namespace === this.actionResource.metadata.namespace);
},
vmUsedVolumeNames() {
const allVMs = this.$store.getters['harvester/all'](HCI.VM) || [];
const names = new Set();
allVMs.forEach((vm) => {
// Collect volume names from spec.template.spec.volumes (both PVC and DataVolume references)
const volumes = vm.spec?.template?.spec?.volumes || [];
volumes.forEach((v) => {
const name = v.persistentVolumeClaim?.claimName || v.dataVolume?.name;
if (name) {
names.add(`${ vm.metadata.namespace }/${ name }`);
}
});
// Collect volume names from volumeClaimTemplates annotation
const templates = parseVolumeClaimTemplates(vm);
templates.forEach((t) => {
if (t.metadata?.name) {
names.add(`${ vm.metadata.namespace }/${ t.metadata.name }`);
}
});
});
return names;
},
targetVolumeOptions() {
return sortBy(
this.namespacePVCs
.filter((pvc) => {
// Exclude volumes used by any VM (via spec.volumes or volumeClaimTemplates)
if (this.vmUsedVolumeNames.has(`${ pvc.metadata.namespace }/${ pvc.metadata.name }`)) {
return false;
}
return true;
})
.map((pvc) => ({
label: pvc.metadata.name,
value: pvc.metadata.name
})),
'label'
);
},
disableSave() {
return !this.sourceVolume || !this.targetVolume;
},
},
methods: {
close() {
this.sourceVolume = '';
this.targetVolume = '';
this.errors = [];
this.$emit('close');
},
async apply(buttonDone) {
if (!this.actionResource) {
buttonDone(false);
return;
}
if (!this.sourceVolume) {
const name = this.t('harvester.modal.storageMigration.fields.sourceVolume.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
if (!this.targetVolume) {
const name = this.t('harvester.modal.storageMigration.fields.targetVolume.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
try {
await this.actionResource.doAction('storageMigration', {
sourceVolume: this.sourceVolume,
targetVolume: this.targetVolume
}, {}, false);
buttonDone(true);
this.close();
} catch (err) {
const error = err?.data || err;
this['errors'] = exceptionToErrorsArray(error);
buttonDone(false);
}
},
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.storageMigration.title') }}
</template>
<template #body>
<LabeledSelect
v-model:value="sourceVolume"
:label="t('harvester.modal.storageMigration.fields.sourceVolume.label')"
:placeholder="t('harvester.modal.storageMigration.fields.sourceVolume.placeholder')"
:options="sourceVolumeOptions"
class="mb-20"
required
/>
<LabeledSelect
v-model:value="targetVolume"
:label="t('harvester.modal.storageMigration.fields.targetVolume.label')"
:placeholder="t('harvester.modal.storageMigration.fields.targetVolume.placeholder')"
:options="targetVolumeOptions"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
#actions
class="actions"
>
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="disableSave"
@click="apply"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>

View File

@ -71,21 +71,38 @@ export default {
},
data() {
let provisioner = `${ this.value.provisioner || LONGHORN_DRIVER }`;
if (provisioner === LONGHORN_DRIVER) {
provisioner = `${ provisioner }_${ this.value.provisionerVersion || LONGHORN_VERSION_V1 }`;
}
return {
provisioner,
volumeGroupDialog: null,
randomStr: randomStr(10).toLowerCase(),
isOpen: false
isOpen: false,
};
},
computed: {
provisioner: {
get() {
let provisioner = `${ this.value?.provisioner || LONGHORN_DRIVER }`;
if (provisioner === LONGHORN_DRIVER) {
provisioner = `${ provisioner }_${ this.value?.provisionerVersion || LONGHORN_VERSION_V1 }`;
}
return provisioner;
},
set(value) {
this.randomStr = randomStr(10).toLowerCase();
const [provisioner, provisionerVersion] = (value || '').split('_');
this.value.provisioner = provisioner;
if (provisioner === LONGHORN_DRIVER) {
this.value.provisionerVersion = provisionerVersion || LONGHORN_VERSION_V1;
} else {
this.value.provisionerVersion = undefined;
}
},
},
provisioners() {
const out = [];
@ -283,20 +300,6 @@ export default {
},
watch: {
provisioner(value) {
this.randomStr = randomStr(10).toLowerCase();
const [provisioner, provisionerVersion] = value?.split('_');
this.value.provisioner = provisioner;
if (provisioner === LONGHORN_DRIVER) {
this.value.provisionerVersion = provisionerVersion || LONGHORN_VERSION_V1;
} else {
this.value.provisionerVersion = undefined;
}
},
'value.lvmVolumeGroup'(neu) {
if (neu === _NEW) {
this.value.lvmVolumeGroup = null;

View File

@ -9,8 +9,11 @@ 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 { Checkbox } from '@components/Form/Checkbox';
import jsyaml from 'js-yaml';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { allHash } from '@shell/utils/promise';
import { get } from '@shell/utils/object';
import { clone, get } from '@shell/utils/object';
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
import { sortBy } from '@shell/utils/sort';
import { saferDump } from '@shell/utils/create-yaml';
@ -33,6 +36,7 @@ export default {
components: {
Banner,
Checkbox,
Tab,
UnitInput,
CruResource,
@ -90,6 +94,8 @@ export default {
source,
storage,
imageId,
showAdvanced: false,
createWithDataVolume: false,
snapshots: [],
images: [],
GIBIBYTE
@ -98,6 +104,24 @@ export default {
created() {
this.registerBeforeHook(this.willSave, 'willSave');
if (this.mode === _CREATE) {
const originalSaveYaml = this.value.saveYaml?.bind(this.value);
this.value.saveYaml = async(yaml) => {
if (this.createWithDataVolume && this.isBlank) {
const parsed = jsyaml.load(yaml);
const dvObj = { ...parsed, type: 'cdi.kubevirt.io.datavolume' };
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
await dataVolume.save();
return dataVolume;
}
return originalSaveYaml(yaml);
};
}
},
computed: {
@ -135,6 +159,10 @@ export default {
return Object.values(VOLUME_MODE);
},
accessModeOptions() {
return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
},
imageOption() {
return sortBy(
this.images
@ -275,6 +303,10 @@ export default {
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
},
isCreatePVCWithDataVolumeFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
},
isResizeDisabled() {
return (
!this.isLHV2VolExpansionFeatureEnabled &&
@ -341,6 +373,58 @@ export default {
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
},
buildDataVolumeObj() {
const storage = {
storageClassName: this.value.spec.storageClassName,
resources: { requests: { storage: this.storage } },
};
if (this.showAdvanced && this.value.spec.accessModes?.length > 0) {
storage.accessModes = this.value.spec.accessModes;
}
if (this.showAdvanced && this.value.spec.volumeMode) {
storage.volumeMode = this.value.spec.volumeMode;
}
return {
type: 'cdi.kubevirt.io.datavolume',
apiVersion: 'cdi.kubevirt.io/v1beta1',
kind: 'DataVolume',
metadata: {
name: this.value.metadata.name,
namespace: this.value.metadata.namespace,
annotations: this.value.metadata.annotations || {},
labels: this.value.metadata.labels || {},
},
spec: {
source: { blank: {} },
storage,
}
};
},
async save(buttonDone) {
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
try {
this.update();
const dvObj = this.buildDataVolumeObj();
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
await dataVolume.save();
buttonDone(true);
this.done();
} catch (err) {
const error = err?.data || err;
this['errors'] = exceptionToErrorsArray(error);
buttonDone(false);
}
} else {
await CreateEditView.methods.save.call(this, buttonDone);
}
},
willSave() {
this.update();
},
@ -383,9 +467,17 @@ export default {
this.update();
},
generateYaml() {
const out = saferDump(this.value);
this.update();
return out;
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
return saferDump(this.buildDataVolumeObj());
}
const plain = clone(this.value);
delete plain.saveYaml;
return saferDump(plain);
},
}
};
@ -458,18 +550,6 @@ export default {
@update:value="update"
/>
<LabeledSelect
v-if="showVolumeMode"
v-model:value="value.spec.volumeMode"
:label="t('harvester.volume.volumeMode')"
:options="volumeModeOptions"
required
:disabled="!isCreate"
:mode="mode"
class="mb-20"
@update:value="update"
/>
<UnitInput
v-model:value="storage"
:label="t('harvester.volume.size')"
@ -490,6 +570,44 @@ export default {
>
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
</Banner>
<div class="row mb-20">
<Checkbox
v-if="isCreate && isBlank && isCreatePVCWithDataVolumeFeatureEnabled"
v-model:value="createWithDataVolume"
:label="t('harvester.volume.createWithDataVolume')"
tooltip-key="harvester.volume.createWithDataVolumeTooltip"
/>
</div>
<a
v-if="isCreate && isCreatePVCWithDataVolumeFeatureEnabled"
role="button"
class="hand"
@click="showAdvanced = !showAdvanced"
>
{{ showAdvanced ? t('harvester.volume.hideAdvanced') : t('harvester.volume.showAdvanced') }}
</a>
<LabeledSelect
v-if="showAdvanced"
v-model:value="value.spec.accessModes"
:label="t('harvester.volume.accessModes')"
:options="accessModeOptions"
:multiple="true"
:mode="mode"
class="mb-20 mt-20"
@update:value="update"
/>
<LabeledSelect
v-if="showAdvanced"
v-model:value="value.spec.volumeMode"
:label="t('harvester.volume.volumeMode')"
:options="volumeModeOptions"
:mode="mode"
class="mb-20"
@update:value="update"
/>
</Tab>
<Tab
v-if="!isCreate"

View File

@ -31,6 +31,7 @@ export default {
const _hash = {
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
srigpuovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE }),
};
await allHash(_hash);
@ -106,19 +107,32 @@ export default {
},
computed: {
parentSriovOptions() {
allSriovs() {
const inStore = this.$store.getters['currentProduct'].inStore;
const allSriovs = this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
return allSriovs.map((sriov) => {
return sriov.id;
});
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
},
allSriovGPUs() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
},
parentSriovOptions() {
return this.allSriovs.map((sriov) => sriov.id);
},
parentSriovGPUOptions() {
return this.allSriovGPUs.map((sriovgpu) => sriovgpu.id);
},
parentSriovLabel() {
return HCI_ANNOTATIONS.PARENT_SRIOV;
}
},
parentSriovGPULabel() {
return HCI_ANNOTATIONS.PARENT_SRIOV_GPU;
},
vGPUAsPCIDeviceEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
},
},
methods: {
enableGroup(rows = []) {
const row = rows[0];
@ -206,6 +220,15 @@ export default {
:rows="rows"
@change-rows="changeRows"
/>
<FilterBySriov
v-if="vGPUAsPCIDeviceEnabled"
ref="filterByParentSRIOVGPU"
:parent-sriov-options="parentSriovGPUOptions"
:parent-sriov-label="parentSriovGPULabel"
:label="t('harvester.sriov.parentSriovGPU')"
:rows="rows"
@change-rows="changeRows"
/>
</template>
</ResourceTable>
</template>

View File

@ -8,6 +8,7 @@ import { set } from '@shell/utils/object';
import { HCI } from '../../../types';
import DeviceList from './DeviceList';
import CompatibilityMatrix from '../CompatibilityMatrix';
import MessageLink from '@shell/components/MessageLink';
export default {
name: 'VirtualMachinePCIDevices',
@ -15,7 +16,8 @@ export default {
LabeledSelect,
DeviceList,
CompatibilityMatrix,
Banner
Banner,
MessageLink
},
props: {
mode: {
@ -138,6 +140,13 @@ export default {
return inUse;
},
toVGpuDevicesPage() {
return {
name: 'harvester-c-cluster-resource',
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
};
},
devicesByNode() {
return this.enabledDevices?.reduce((acc, device) => {
const nodeName = device.status?.nodeName;
@ -232,7 +241,12 @@ export default {
<div class="row">
<div class="col span-12">
<Banner color="info">
<t k="harvester.pci.howToUseDevice" />
<MessageLink
:to="toVGpuDevicesPage"
prefix-label="harvester.pci.howToUseDeviceInVMCreation.prefix"
middle-label="harvester.pci.howToUseDeviceInVMCreation.middle"
suffix-label="harvester.pci.howToUseDeviceInVMCreation.suffix"
/>
</Banner>
<Banner
v-if="selectedDevices.length > 0"

View File

@ -211,6 +211,10 @@ export default {
return false;
},
vGPUAsPCIDeviceEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
},
usbPassthroughEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
},
@ -740,7 +744,7 @@ export default {
</Tab>
<Tab
v-if="enabledSriovgpu"
v-if="enabledSriovgpu && !vGPUAsPCIDeviceEnabled"
:label="t('harvester.tab.vGpuDevices')"
name="vGpuDevices"
:weight="-6"

View File

@ -1,6 +1,7 @@
<script>
import { NORMAN } from '@shell/config/types';
import { HCI } from '../types';
import { getHarvesterUser } from '../utils/auth';
export default {
props: {
@ -25,7 +26,7 @@ export default {
},
data() {
const user = this.$store.getters['auth/v3User'];
const user = getHarvesterUser(this.$store.getters);
return {
harvesterSettings: [],

View File

@ -123,6 +123,24 @@ harvester:
namespace: Namespace
message:
success: 'Image { name } created successfully.'
storageMigration:
title: Storage Migration
fields:
sourceVolume:
label: Source Volume
placeholder: Select a source volume
targetVolume:
label: Target Volume
placeholder: Select a target volume
dataMigration:
title: Data Migration
fields:
targetVolumeName:
label: Target Volume Name
placeholder: Enter a target volume name
targetStorageClassName:
label: Target Storage Class
placeholder: Select a storage class
migration:
failedMessage: Latest migration failed!
title: Migration
@ -232,6 +250,9 @@ harvester:
migrate: Migrate
cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration
storageMigration: Storage Migration
cancelStorageMigration: Cancel Storage Migration
dataMigration: Data Migration
createTemplate: Generate Template
enableMaintenance: Enable Maintenance Mode
disableMaintenance: Disable Maintenance Mode
@ -355,7 +376,14 @@ harvester:
available: Available Devices
compatibleNodes: Compatible Nodes
impossibleSelection: 'There are no hosts with all of the selected devices.'
howToUseDevice: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.'
howToUseDeviceInVMCreation:
prefix: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine. <br>For vGPU devices, please enable them on the'
middle: vGPU Devices
suffix: page first.
howToUseDevice:
prefix: 'Select the device in the table to enable PCI passthrough. <br>For vGPU devices, please enable them on the'
middle: vGPU Devices
suffix: page.
deviceInTheSameHost: 'You can only select devices on the same host.'
oldFormatDevices:
help: |-
@ -425,7 +453,7 @@ harvester:
volume:
upperType: Volume name
lowerType: volume name
needImageOrExisting: 'At least an image volume or an existing root-disk volume is required!'
needAtLeastOneBootable: 'At least one bootable volume is required!'
image:
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
@ -898,6 +926,11 @@ harvester:
conditions: Conditions
size: Size
volumeMode: Volume Mode
accessModes: Access Modes
createWithDataVolume: Create with DataVolume
createWithDataVolumeTooltip: Create Volume with Kubevirt/Containerized Data Importer way. It can fill accessMode/volumeMode automatically.
showAdvanced: Show Advanced Options
hideAdvanced: Hide Advanced Options
source: Source
kind: Kind
sourceOptions:
@ -1266,6 +1299,11 @@ harvester:
addIp: Add Exclude IP
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
rwxNetwork:
warning: 'WARNING: <br/> Any change to rwx-network requires longhorn RWX volumes detached before applying this setting.<br/>Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
shareStorageNetwork: Share Storage Network
dedicatedRwxNetwork: Dedicated RWX Network
shareStorageNetworkWarning: The rwx-network is governed by storage-network, and changes here won't take effect until share-storage-network is set to false.
vmForceDeletionPolicy:
period: Period
vmMigrationTimeout: VM Migration Timeout
@ -1354,13 +1392,17 @@ harvester:
url: URL
insecureSkipTLSVerify: Insecure Skip TLS Verify
tip:
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure SKip TLS Verify", you must provide custom CA certificates using the
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure Skip TLS Verify", you must provide custom CA certificates using the
middle: 'additional-ca'
suffix: setting.
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
ntpServers:
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
isDuplicate: There are duplicate NTP server configurations.
instanceManagerResources:
parseError: "Failed to parse configuration: {error}"
v1: "V1 Data Engine"
v2: "V2 Data Engine"
kubevirtMigration:
parseError: "Failed to parse configuration: {error}"
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
@ -1832,7 +1874,8 @@ harvester:
numVFs: Number Of Virtual Functions
vfAddresses: Virtual Functions Addresses
showMore: Show More
parentSriov: Filter By Parent SR-IOV
parentSriov: Filter By Parent SR-IOV Netork Device
parentSriovGPU: Filter By Parent SR-IOV GPU Device
sriovgpu:
label: SR-IOV GPU Devices
@ -1979,6 +2022,7 @@ advancedSettings:
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
'harv-storage-network': 'Longhorn storage-network setting.'
'harv-rwx-network': 'Configure RWX network behavior for shared or dedicated storage network usage.'
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
@ -1994,6 +2038,7 @@ advancedSettings:
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
typeLabel:
kubevirt.io.virtualmachine: |-

View File

@ -60,6 +60,17 @@ export default {
return schema;
},
toVGpuDevicesPage() {
return {
name: 'harvester-c-cluster-resource',
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
};
},
vGPUAsPCIDeviceEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
},
rows() {
const inStore = this.$store.getters['currentProduct'].inStore;
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
@ -85,11 +96,23 @@ export default {
{{ t('harvester.pci.noPCIPermission') }}
</Banner>
</div>
<div v-else-if="hasSchema && enabledPCI">
<Banner
v-if="vGPUAsPCIDeviceEnabled"
color="info"
>
<MessageLink
:to="toVGpuDevicesPage"
prefix-label="harvester.pci.howToUseDevice.prefix"
middle-label="harvester.pci.howToUseDevice.middle"
suffix-label="harvester.pci.howToUseDevice.suffix"
/>
</Banner>
<DeviceList
v-else-if="hasSchema && enabledPCI"
:devices="rows"
:schema="schema"
/>
</div>
<div v-else>
<Banner color="warning">
<MessageLink

View File

@ -70,8 +70,13 @@ export default {
return schema;
},
filterRows() {
// we only show the non golden image PVCs in the list
return this.rows.filter((pvc) => !pvc?.isGoldenImageVolume);
return this.rows.filter((pvc) => {
if (pvc?.isGoldenImageVolume || pvc?.isCDIPopulatorVolume) {
return false;
}
return true;
});
},
headers() {
return [

View File

@ -707,18 +707,22 @@ export default {
}
},
needVolume(R) {
if (R.image === EMPTY_IMAGE) {
return false;
needVolumeRelatedInfo(R) {
// return [needVolume, needVolumeClaimTemplate]
if (R.source === SOURCE_TYPE.CONTAINER) {
return [true, false];
}
return true;
if (R.source === SOURCE_TYPE.IMAGE && R.image === EMPTY_IMAGE) {
return [false, false];
}
return [true, true];
},
parseDiskRows(disk) {
const disks = [];
const volumes = [];
const diskNameLabels = [];
const volumeClaimTemplates = [];
disk.forEach( (R, index) => {
@ -726,14 +730,18 @@ export default {
disks.push(_disk);
if (this.needVolume(R)) {
const prefixName = this.value.metadata?.name || '';
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
const [needVolume, needVolumeClaimTemplate] = this.needVolumeRelatedInfo(R);
if (needVolume) {
const _volume = this.parseVolume(R, dataVolumeName);
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
volumes.push(_volume);
diskNameLabels.push(dataVolumeName);
}
if (needVolumeClaimTemplate) {
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
volumeClaimTemplates.push(_dataVolumeTemplate);
}
});

View File

@ -1,6 +1,8 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { getHarvesterUserName } from '../utils/auth';
const STATUS_DISPLAY = {
enabled: {
@ -32,7 +34,7 @@ export default class PCIDevice extends SteveModel {
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.isEnabling,
enabled: !this.isEnabling && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
@ -41,7 +43,7 @@ export default class PCIDevice extends SteveModel {
},
{
action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe,
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough',
bulkable: true,
@ -52,6 +54,14 @@ export default class PCIDevice extends SteveModel {
return out;
}
get isvGPUDevice() {
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
return false;
}
return !!this.metadata?.labels?.[HCI_ANNOTATIONS.PARENT_SRIOV_GPU];
}
get canYaml() {
return false;
}
@ -87,15 +97,8 @@ export default class PCIDevice extends SteveModel {
if (!this.passthroughClaim) {
return false;
}
const isSingleProduct = this.$rootGetters['isSingleProduct'];
let userName = 'admin';
// if this is imported Harvester, there may be users other than admin
if (!isSingleProduct) {
const user = this.$rootGetters['auth/v3User'];
userName = user?.username || user?.id;
}
const userName = getHarvesterUserName(this.$rootGetters);
return this.claimedBy === userName;
}
@ -176,6 +179,10 @@ export default class PCIDevice extends SteveModel {
return this.status?.description;
}
get vGPUAsPCIDeviceFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
}
showDetachWarning() {
this.$dispatch('growl/warning', {
title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'),

View File

@ -1,6 +1,7 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types';
import { getHarvesterUserName } from '../utils/auth';
const STATUS_DISPLAY = {
enabled: {
@ -87,15 +88,8 @@ export default class USBDevice extends SteveModel {
if (!this.passthroughClaim) {
return false;
}
const isSingleProduct = this.$rootGetters['isSingleProduct'];
let userName = 'admin';
// if this is imported Harvester, there may be users other than admin
if (!isSingleProduct) {
const user = this.$rootGetters['auth/v3User'];
userName = user?.username || user?.id;
}
const userName = getHarvesterUserName(this.$rootGetters);
return this.claimedBy === userName;
}

View File

@ -9,6 +9,7 @@ import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { HCI, VOLUME_SNAPSHOT } from '../../types';
import HarvesterResource from '../harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
import { CDI_POPULATOR_KIND } from '../../config/types';
import { LVM_DRIVER } from './storage.k8s.io.storageclass';
const DEGRADED_ERRORS = ['replica scheduling failed', 'precheck new replica failed'];
@ -44,7 +45,7 @@ export default class HciPv extends HarvesterResource {
const exportImageAction = {
action: 'exportImage',
enabled: this.hasAction('export') && !this.isEncrypted,
icon: 'icon icon-copy',
icon: 'icon icon-external-link',
label: this.t('harvester.action.exportImage')
};
const takeSnapshotAction = {
@ -77,10 +78,23 @@ export default class HciPv extends HarvesterResource {
icon: 'icon icon-backup',
label: this.t('harvester.action.cancelExpand')
},
{
action: 'dataMigration',
enabled: this.hasAction('dataMigration') && this.createPVCWithDataVolumeFeatureEnabled,
icon: 'icon icon-copy',
label: this.t('harvester.action.dataMigration')
},
...out
];
}
dataMigration(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterDataMigrationDialog'
});
}
exportImage(resources = this) {
this.$dispatch('promptModal', {
resources,
@ -339,10 +353,20 @@ export default class HciPv extends HarvesterResource {
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true';
}
get isCDIPopulatorVolume() {
const kind = this?.metadata?.annotations?.[HCI_ANNOTATIONS.CDI_POPULATOR_KIND];
return kind === CDI_POPULATOR_KIND.VOLUME_IMPORT_SOURCE || kind === CDI_POPULATOR_KIND.VOLUME_CLONE_SOURCE;
}
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
get createPVCWithDataVolumeFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
}
get resourceExternalLink() {
const host = window.location.host;
const { params } = this.currentRoute();

View File

@ -199,6 +199,18 @@ export default class VirtVm extends HarvesterResource {
icon: 'icon icon-close',
label: this.t('harvester.action.abortMigration')
},
{
action: 'storageMigration',
enabled: !!this.actions?.storageMigration,
icon: 'icon icon-copy',
label: this.t('harvester.action.storageMigration')
},
{
action: 'cancelStorageMigration',
enabled: !!this.actions?.cancelStorageMigration,
icon: 'icon icon-close',
label: this.t('harvester.action.cancelStorageMigration')
},
{
action: 'addHotplugVolume',
enabled: !!this.actions?.addVolume,
@ -368,6 +380,13 @@ export default class VirtVm extends HarvesterResource {
});
}
storageMigration(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterStorageMigrationDialog'
});
}
backupVM(resources = this) {
this.$dispatch('promptModal', {
resources,
@ -520,6 +539,10 @@ export default class VirtVm extends HarvesterResource {
this.doActionGrowl('abortMigration', {});
}
cancelStorageMigration() {
this.doActionGrowl('cancelStorageMigration', {});
}
createTemplate(resources = this) {
this.$dispatch('promptModal', {
resources,
@ -770,11 +793,11 @@ export default class VirtVm extends HarvesterResource {
}
get isPending() {
if (this &&
if ((this &&
!this.isVMExpectedRunning &&
this.isVMCreated &&
this.vmi?.status?.phase === VMIPhase.Pending
) {
) || (this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS] === 'cloning')) {
return { status: VMIPhase.Pending };
}

View File

@ -1,7 +1,7 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.8.0-dev",
"version": "1.8.1-dev",
"private": false,
"rancher": {
"annotations": {
@ -17,12 +17,12 @@
"nuxt": "./node_modules/.bin/nuxt"
},
"engines": {
"node": ">=20.0.0"
"node": ">=24.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.0"
"@vue/cli-plugin-babel": "~5.0.9",
"@vue/cli-service": "~5.0.9",
"@vue/cli-plugin-typescript": "~5.0.9"
},
"browserslist": [
"> 1%",

View File

@ -0,0 +1,31 @@
/**
* Resolve the Harvester username from Vuex getters.
*
* Works with both `this.$store.getters` (in components) and
* `this.$rootGetters` (in Steve models).
*
* - In single-product (standalone Harvester) mode, always returns the
* default username (`admin`).
* - Otherwise, falls back to the authenticated user's `username` or `id`.
*/
export function getHarvesterUserName(getters, defaultUserName = 'admin') {
const isSingleProduct = getters?.['isSingleProduct'];
if (isSingleProduct) {
return defaultUserName;
}
const user = getHarvesterUser(getters);
return user?.username || user?.id || defaultUserName;
}
/**
* Return the authenticated user object from Vuex getters.
*
* Works with both `this.$store.getters` (in components) and
* `this.$rootGetters` (in Steve models).
*/
export function getHarvesterUser(getters) {
return getters?.['auth/user'];
}

View File

@ -69,7 +69,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
});
let requiredVolume = false;
let hasBootableVolume = false;
_volumes.forEach((V, idx) => {
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
@ -77,7 +77,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
const prefix = V.name || idx + 1;
if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) {
requiredVolume = true;
hasBootableVolume = true;
}
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
@ -137,10 +137,10 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
});
/**
* At least one volume must be create. (Verify only when create.)
* At least one bootable volume must be provided. (Verify only when create.)
*/
if ((!requiredVolume || _volumes.length === 0) && !value.links) {
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needImageOrExisting'));
if (!hasBootableVolume && !value.links) {
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needAtLeastOneBootable'));
}
return errors;

40
scripts/bump_version.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
set -e
# Usage: update package.json and pkg/harvester/package.json to desired version
# Example: ./scripts/bump_version.sh v1.8.0-rc3
VERSION="$1"
if [ -z "$VERSION" ]; then
echo "Usage: $0 <version>"
echo "Example: $0 v1.8.0-rc3"
exit 1
fi
# Strip leading 'v' if present
VERSION="${VERSION#v}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
FILES=(
"$ROOT_DIR/package.json"
"$ROOT_DIR/pkg/harvester/package.json"
)
for FILE in "${FILES[@]}"; do
if [ ! -f "$FILE" ]; then
echo "File not found: $FILE"
exit 1
fi
# Use node to update version in-place while preserving formatting
node -e "
const fs = require('fs');
const raw = fs.readFileSync('$FILE', 'utf8');
const updated = raw.replace(/\"version\":\s*\"[^\"]*\"/, '\"version\": \"$VERSION\"');
fs.writeFileSync('$FILE', updated);
"
echo "Updated $FILE -> $VERSION"
done

View File

@ -0,0 +1,44 @@
#!/bin/bash
set -e
# Get the directory of the script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Project root is one level up from scripts/
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# Source files
TEMPLATE_FILE="${PROJECT_ROOT}/docs/agents.md/template_agents.md"
AGENTS_DIR="${PROJECT_ROOT}/docs/agents.md/agents"
CONTRIBUTORS_DIR="${PROJECT_ROOT}/docs/agents.md/contributors"
PERSONAS_DIR="${PROJECT_ROOT}/docs/agents.md/personas"
# Destination file
OUTPUT_FILE="${PROJECT_ROOT}/AGENTS.md"
insert_directory_contents() {
local dir="$1"
if [ -d "$dir" ]; then
for file in "$dir"/*; do
if [ -f "$file" ]; then
cat "$file"
echo ""
fi
done
fi
}
echo "Generating ${OUTPUT_FILE}..."
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" == *"<agents>"* ]]; then
insert_directory_contents "${AGENTS_DIR}"
elif [[ "$line" == *"<contributors>"* ]]; then
insert_directory_contents "${CONTRIBUTORS_DIR}"
elif [[ "$line" == *"<personas>"* ]]; then
insert_directory_contents "${PERSONAS_DIR}"
else
echo "$line"
fi
done < "${TEMPLATE_FILE}" > "${OUTPUT_FILE}"
echo "Done."

4168
yarn.lock

File diff suppressed because it is too large Load Diff