Compare commits

..
Author SHA1 Message Date
mergify[bot]andTim Liou bbc415a2b4 chore: bump version to 1.8.0-rc4 (#778) (#779)
(cherry picked from commit 9d698b1230)

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
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 566e79eda5)

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]andAndy Lee dc91be11f9 chore: bump version to 1.8.0-rc3 (#774) (#775)
(cherry picked from commit 42ddcfc1fe)

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]andAndy Lee e5b72499b5 fix: use default value got undefined value in payload (#772) (#773)
(cherry picked from commit ad3decf71f)

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]andAndy Lee e66037b007 ci: remove the single quota for use commit hash (#767) (#771)
(cherry picked from commit 8083a41df0)

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]andfreeze c676cad057 chore: pin GH Actions to commit sha (#765) (#766)
(cherry picked from commit 62801b3b13)

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

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]andAndy Lee ff1e6e6e7f docs: update install instruction doc link (#753) (#754)
(cherry picked from commit 97e93dba0b)

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]andAndy Lee 622242e4a5 refactor: change rwxNetwork setting to kind json (#751) (#752)
(cherry picked from commit 9a8a709e56)

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]andAndy Lee d8ea9be174 feat: introduce rwxNetwork setting (#746) (#750)
* feat: add rwxNetwork setting



* fix: network payload



---------


(cherry picked from commit d1949641a7)

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]andfreeze f8a479cfcf feat: add storage migration operation (#724) (#749)
- add storage migration
    - add cancel storage migration


(cherry picked from commit 9c9f59c939)

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]andAndy Lee 6d5e584113 chore: bump to v1.8.0-rc2 (#747) (#748)
(cherry picked from commit ccc14c7fb9)

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]andAndy Lee a9eb04195e feat: introduce instance-manager-resources setting (#744) (#745)
(cherry picked from commit 2ba471907e)

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]andAndy Lee 0441c1f6e3 refactor: remove using resourceName to determine is vGPU device (#741) (#743)
(cherry picked from commit 5aea476f64)

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

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]andTim Liou b8111f0ad7 fix: reword the error message to focus on bootable volume (#736) (#738)
(cherry picked from commit a9c392c13f)

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]andTim Liou 6d627f82e9 fix: container disks don't need volumeClaimTemplates but volumes (#735) (#737)
(cherry picked from commit 888ec7a50f)

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]andAndy Lee 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 23344e0c07)

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]andPo Han Huang 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 a2486a7d38)

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]andAndy Lee 9ecc372009 chore: bump to v1.8.0-rc1 (#731) (#733)
(cherry picked from commit df3d249923)

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
103 changed files with 4623 additions and 6265 deletions
+1 -2
View File
@@ -4,8 +4,7 @@ description: Setup node and install dependencies
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
- name: Setup Nodejs with yarn caching - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
+6
View File
@@ -0,0 +1,6 @@
addAssignees: author
addReviewers: true
numberOfReviewers: 0
reviewers:
- a110605
- houhoucoop
+4 -15
View File
@@ -34,25 +34,14 @@
{ {
"matchUpdateTypes": ["minor"], "matchUpdateTypes": ["minor"],
"groupName": "minor dependencies", "groupName": "minor dependencies",
"minimumReleaseAge": "7 days",
"labels": ["minor-update"], "labels": ["minor-update"],
"reviewers": ["a110605", "houhoucoop"] "reviewers": ["a110605", "houhoucoop"]
}, },
{ {
"matchUpdateTypes": ["patch"], "matchUpdateTypes": ["patch", "digest"],
"automerge": false, "automerge": true,
"minimumReleaseAge": "7 days", "groupName": "patch digest dependencies",
"groupName": "patch dependencies", "labels": ["patch-update", "automerge"]
"labels": ["patch-update"],
"reviewers": ["a110605", "houhoucoop"]
},
{
"matchUpdateTypes": ["digest", "pinDigest"],
"automerge": false,
"groupName": "digest dependencies",
"labels": ["digest-update"],
"schedule": ["on the first day of the month"],
"reviewers": ["a110605", "houhoucoop"]
} }
] ]
} }
-39
View File
@@ -1,39 +0,0 @@
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
View File
@@ -1,33 +0,0 @@
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"
-28
View File
@@ -1,28 +0,0 @@
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
@@ -0,0 +1,17 @@
name: "[PR Management] Auto Assign Reviewer & Assignee"
on:
pull_request_target:
types: [opened, ready_for_review]
permissions:
pull-requests: write
jobs:
auto-assign:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: rancher/gh-issue-mgr/auto-assign-action@b70f0bdf12a03e5e3f33e4f92ccb6c89deb3ebd9 # main
with:
configuration-path: .github/auto-assign-config.yaml
-35
View File
@@ -1,35 +0,0 @@
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"
@@ -1,30 +0,0 @@
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
+55 -44
View File
@@ -1,44 +1,40 @@
name: "PR Management Add Backport Labels" name: "[PR Management] Add Labels"
on: on:
workflow_run: pull_request_target:
workflows: types: [opened, reopened]
- "PR Management Add Labels Collect Data" branches:
types: [completed] - main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs: jobs:
add-require-backport-label: add-require-backport-label:
if: github.event.pull_request.draft == false &&
github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
actions: read
pull-requests: write
steps: steps:
- name: Download PR data artifact - name: Checkout repository
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
name: pr-backport-label-data ref: ${{ github.base_ref }}
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Load PR data - name: Fetch release branches and PR labels
run: | id: fetch_info
cat pr-backport-label-data.env >> $GITHUB_ENV
- name: Add require-backport label (main branch PRs)
if: env.PR_BASE_REF == 'main'
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
repo="${{ github.repository }}" repo="${{ github.repository }}"
pr_number="$PR_NUMBER" pr_number=${{ github.event.pull_request.number }}
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true) 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 if [[ -z "$release_branches" ]]; then
echo "No release branches found, skipping." echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
@@ -48,36 +44,51 @@ jobs:
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name') tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
if echo "$tags" | grep -Fxq "$release_tag"; then if echo "$tags" | grep -Fxq "$release_tag"; then
echo "Release $release_tag already published, skipping." echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
label="require backport/v${version}" 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=$(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"
if echo "$pr_labels" | grep -Fxq "$label"; then - name: Add label if needed
echo "Label '$label' already present, skipping." if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
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: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail 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 }}"
IS_MERGIFY=$(echo "$PR_USER_LOGIN" | grep -iq 'mergify' && echo true || echo false) 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 }}
if [[ "$IS_MERGIFY" != "true" ]]; then echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
echo "PR author is not Mergify, skipping." echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
exit 0
fi
version="${PR_BASE_REF#release-harvester-v}" - name: Add label if needed
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
version="${TARGET_BRANCH#release-harvester-v}"
label="backport/v${version}" label="backport/v${version}"
echo "Adding label: $label" echo "Adding label $label"
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$label" gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "$label"
@@ -1,28 +0,0 @@
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
+16 -22
View File
@@ -1,47 +1,41 @@
name: "PR Management Request Backport via Mergify" name: "[PR Management] Request Backport via Mergify"
on: on:
workflow_run: pull_request_target:
workflows: types: [closed]
- "PR Management Request Backport via Mergify Collect Data" branches: [main]
types: [completed]
permissions:
pull-requests: write
jobs: jobs:
comment-backport: comment-backport:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
actions: read
pull-requests: write
steps: steps:
- name: Download PR data artifact - name: Checkout repository
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
name: pr-backport-request-data ref: ${{ github.base_ref }}
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 - name: Post Mergify backport command
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
pr_number="$PR_NUMBER" pr_number=${{ github.event.pull_request.number }}
repo="${{ github.repository }}" repo="${{ github.repository }}"
labels=$(echo "$PR_LABELS" | tr ',' '\n') labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
labels=$(echo "$labels_json" | jq -r '.[] // empty')
echo "Labels on PR: $labels" echo "Labels on PR: $labels"
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true) matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
if [[ -z "$matches" ]]; then if [[ -z "$matches" ]]; then
echo "No back-port labels found — skipping." echo "No back‑port labels found — skipping."
exit 0 exit 0
fi fi
@@ -45,7 +45,7 @@ jobs:
run: ./scripts/build-upload-gate run: ./scripts/build-upload-gate
- name: Get gcs auth - name: Get gcs auth
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3 uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with: with:
secrets: | secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ; secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
@@ -90,7 +90,7 @@ jobs:
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
- name: Get gcs auth - name: Get gcs auth
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3 uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with: with:
secrets: | secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ; secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
@@ -134,7 +134,7 @@ jobs:
run: ./scripts/build-upload-gate run: ./scripts/build-upload-gate
- name: Get gcs auth - name: Get gcs auth
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3 uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with: with:
secrets: | secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ; secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
@@ -27,7 +27,7 @@ jobs:
build-status: ${{ job.status }} build-status: ${{ job.status }}
steps: steps:
- name: Read Secrets - name: Read Secrets
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3 uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with: with:
secrets: | secrets: |
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ; secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
@@ -55,8 +55,14 @@ jobs:
with: with:
version: v3.8.0 version: v3.8.0
- name: Setup Nodejs with yarn install - name: Setup Nodejs with yarn caching
uses: ./.github/actions/setup uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: yarn
- name: Install dependencies
run: yarn
- name: Build and push UI image - name: Build and push UI image
run: | run: |
@@ -64,12 +64,6 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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 - name: Setup environment
run: | run: |
corepack enable corepack enable
@@ -43,7 +43,7 @@ jobs:
needs: needs:
- setup-release-tag - setup-release-tag
- check-version - check-version
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@3b26a36bad555e5e2b8634b24823be29732f287c # master uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@9eb70a732e9be146722e1dbab431338366c2afc6 # creators-pkg-v3.0.10
permissions: permissions:
actions: write actions: write
contents: write contents: write
+2 -2
View File
@@ -20,13 +20,13 @@ jobs:
# The FOSSA token is shared between all repos in Harvester's GH org. It can # 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. # be used directly and there is no need to request specific access to EIO.
- name: Read FOSSA token - name: Read FOSSA token
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3 uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with: with:
secrets: | secrets: |
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
- name: FOSSA scan - name: FOSSA scan
uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.0 uses: fossas/fossa-action@c414b9ad82eaad041e47a7cf62a4f02411f427a0 # v1.8.0
with: with:
api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }} api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }}
# Only runs the scan and do not provide/returns any results back to the # Only runs the scan and do not provide/returns any results back to the
+30
View File
@@ -0,0 +1,30 @@
name: "[PR Management] Add PR Label"
on:
pull_request_target:
types: [opened, reopened, edited]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
auto-assign-pr-label:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.base_ref }}
- name: Setup Nodejs and yarn install
uses: ./.github/actions/setup
- name: Set PR label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
echo "PR_LABEL = $PR_LABEL"
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
+1 -1
View File
@@ -1 +1 @@
24 20
-261
View File
@@ -1,261 +0,0 @@
> 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
+1 -1
View File
@@ -33,7 +33,7 @@ module.exports = {
'subject-full-stop': [2, 'never', '.'], 'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [0, 'never'], 'subject-max-length': [0, 'never'],
'body-leading-blank': [2, 'always'], 'body-leading-blank': [2, 'always'],
'body-max-line-length': [0, 'always', 100], 'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [2, 'always'], 'footer-leading-blank': [2, 'always'],
'footer-max-line-length': [2, 'always', 100], 'footer-max-line-length': [2, 'always', 100],
}, },
-63
View File
@@ -1,63 +0,0 @@
# 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
-23
View File
@@ -1,23 +0,0 @@
## 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.
-65
View File
@@ -1,65 +0,0 @@
## 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
```
@@ -1,70 +0,0 @@
## 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.
@@ -1,3 +0,0 @@
## E2E Tests (Cypress)
See https://github.com/harvester/harvester-ui-tests
@@ -1,12 +0,0 @@
## 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`.
@@ -1,58 +0,0 @@
## 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
@@ -1,5 +0,0 @@
## 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.
-22
View File
@@ -1,22 +0,0 @@
> 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>`
+11 -12
View File
@@ -1,13 +1,13 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.9.0-dev", "version": "1.8.0-rc4",
"private": false, "private": false,
"engines": { "engines": {
"node": ">=24.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-transform-class-static-block": "7.29.7", "@babel/plugin-transform-class-static-block": "7.28.6",
"@rancher/shell": "3.0.12-rc.3", "@rancher/shell": "3.0.9-rc.6",
"@vue-flow/background": "^1.3.0", "@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1", "@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5", "@vue-flow/core": "^1.33.5",
@@ -21,21 +21,21 @@
"yaml": "^2.5.1" "yaml": "^2.5.1"
}, },
"resolutions": { "resolutions": {
"@types/node": "25.9.2", "@types/node": "~20.19.0",
"cronstrue": "2.59.0", "cronstrue": "2.59.0",
"d3-color": "3.1.0", "d3-color": "3.1.0",
"ejs": "3.1.10", "ejs": "3.1.10",
"follow-redirects": "1.16.0", "follow-redirects": "1.15.11",
"glob": "7.2.3", "glob": "7.2.3",
"glob-parent": "6.0.2", "glob-parent": "6.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"@types/lodash": "4.17.24", "@types/lodash": "4.17.24",
"merge": "2.1.1", "merge": "2.1.1",
"node-forge": "1.4.0", "node-forge": "1.3.3",
"nth-check": "2.1.1", "nth-check": "2.1.1",
"qs": "6.15.2", "qs": "6.15.0",
"roarr": "7.21.5", "roarr": "7.21.4",
"semver": "7.8.2", "semver": "7.7.4",
"@vue/cli-service/html-webpack-plugin": "^5.0.0" "@vue/cli-service/html-webpack-plugin": "^5.0.0"
}, },
"scripts": { "scripts": {
@@ -49,8 +49,7 @@
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish", "publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name", "parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
"commitlint": "commitlint --edit", "commitlint": "commitlint --edit",
"prepare": "husky", "prepare": "husky"
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/load": "^19.8.1", "@commitlint/load": "^19.8.1",
@@ -1,186 +0,0 @@
<script>
import { Banner } from '@components/Banner';
import MatchExpressions from '@shell/components/form/MatchExpressions';
import ResourceTable from '@shell/components/ResourceTable';
import { _EDIT } from '@shell/config/query-params';
import { convert, simplify, matching as selectorMatching } from '@shell/utils/selector';
import throttle from 'lodash/throttle';
import { NODE } from '@shell/config/types';
import { NAME, AGE } from '@shell/config/table-headers';
export default {
name: 'HarvesterNodeSelector',
components: {
Banner,
MatchExpressions,
ResourceTable,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
required: true,
},
},
async fetch() {
this.updateMatchingResources();
},
data() {
const inStore = this.$store.getters['currentProduct'].inStore;
return {
matchingResources: {
matched: 0,
matches: [],
none: true,
sample: null,
total: 0,
},
tableHeaders: [
NAME,
{
name: 'host-ip',
labelKey: 'tableHeaders.hostIp',
search: ['internalIp'],
value: 'internalIp',
sort: ['internalIp'],
align: 'center',
},
{
name: 'cpuManager',
labelKey: 'harvester.tableHeaders.cpuManager',
value: 'id',
formatter: 'HarvesterCPUPinning',
width: 150,
align: 'center',
},
{
name: 'diskState',
labelKey: 'tableHeaders.diskState',
value: 'diskState',
formatter: 'HarvesterDiskState',
width: 130,
},
AGE,
],
inStore,
};
},
watch: {
value: {
handler: 'updateMatchingResources',
deep: true,
},
allResourcesInScope() {
this.updateMatchingResources();
},
},
computed: {
schema() {
return this.$store.getters[`${ this.inStore }/schemaFor`](NODE);
},
selectorExpressions: {
get() {
return convert(
this.value.matchLabels || {},
this.value.matchExpressions || []
);
},
set(selectorExpressions) {
const { matchLabels, matchExpressions } = simplify(selectorExpressions);
this.value['matchLabels'] = matchLabels;
this.value['matchExpressions'] = matchExpressions;
this.updateMatchingResources();
},
},
allNodes() {
return this.$store.getters[`${ this.inStore }/all`](NODE) || [];
},
allResourcesInScope() {
return this.allNodes.length;
},
},
methods: {
updateMatchingResources: throttle(function() {
const expressions = this.selectorExpressions;
const allNodes = this.allNodes;
// Empty expressions with no key = no match
const hasValidExpression = expressions.length > 0 && expressions.every((e) => !!e.key);
if (!hasValidExpression) {
this.matchingResources = {
matched: 0,
matches: [],
none: true,
sample: null,
total: allNodes.length,
};
return;
}
const matches = selectorMatching(allNodes, expressions, 'metadata.labels');
this.matchingResources = {
matched: matches.length,
matches,
none: matches.length === 0,
sample: matches[0]?.nameDisplay || null,
total: allNodes.length,
};
}, 100, { trailing: true })
},
};
</script>
<template>
<div>
<div class="row">
<div class="col span-12">
<MatchExpressions
v-model:value="selectorExpressions"
:mode="mode"
:show-remove="false"
:type="'node'"
:target-resources="allResourcesInScope"
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<Banner :color="(matchingResources.none ? 'warning' : 'success')">
<span v-clean-html="t('generic.selectors.matchingResources.matchesSome', matchingResources)" />
</Banner>
</div>
</div>
<div class="row">
<div class="col span-12">
<ResourceTable
:rows="matchingResources.matches"
:headers="tableHeaders"
key-field="id"
:table-actions="false"
:row-actions="false"
:schema="schema"
:groupable="false"
:search="false"
/>
</div>
</div>
</div>
</template>
+1 -1
View File
@@ -122,7 +122,7 @@ export default {
return this.$store.getters['currentCluster'].isLocal; return this.$store.getters['currentCluster'].isLocal;
}, },
canEditClusterMembers() { canEditClusterMembers() {
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post'); return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
}, },
}, },
}; };
+5 -8
View File
@@ -58,14 +58,11 @@ export default {
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`; const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
// Defer so v-select can finish closing the dropdown before the popup steals focus window.open(
this.$nextTick(() => { url,
window.open( '_blank',
url, `toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
'_blank', );
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
);
});
}, },
isEmpty(o) { isEmpty(o) {
@@ -1,196 +0,0 @@
<script>
import { RadioGroup } from '@components/Form/Radio';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view';
export default {
name: 'HarvesterClusterPodSecurityStandard',
components: {
RadioGroup,
LabeledSelect,
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => ({}),
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
},
data() {
let enabled = false;
let whitelistedNamespaces = [];
let privilegedNamespaces = [];
let restrictedNamespaces = [];
try {
const parsed = JSON.parse(this.value.value || this.value.default || '{}');
enabled = !!parsed.enabled;
whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
} catch (e) {
enabled = false;
whitelistedNamespaces = [];
privilegedNamespaces = [];
restrictedNamespaces = [];
}
return {
enabled,
whitelistedNamespaces,
privilegedNamespaces,
restrictedNamespaces,
};
},
computed: {
enabledOptions() {
return [
{ label: this.t('generic.enabled'), value: true },
{ label: this.t('generic.disabled'), value: false },
];
},
allNamespaces() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](NAMESPACE).filter((ns) => !ns.isSystem).map((ns) => ns.id);
},
whitelistedOptions() {
const excluded = new Set([...this.privilegedNamespaces, ...this.restrictedNamespaces]);
return this.allNamespaces.filter((ns) => !excluded.has(ns));
},
privilegedOptions() {
const excluded = new Set([...this.whitelistedNamespaces, ...this.restrictedNamespaces]);
return this.allNamespaces.filter((ns) => !excluded.has(ns));
},
restrictedOptions() {
const excluded = new Set([...this.whitelistedNamespaces, ...this.privilegedNamespaces]);
return this.allNamespaces.filter((ns) => !excluded.has(ns));
},
},
methods: {
useDefault() {
try {
const parsed = JSON.parse(this.value.default || '{}');
this.enabled = !!parsed.enabled;
this.whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
this.privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
this.restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
} catch (e) {
this.enabled = false;
this.whitelistedNamespaces = [];
this.privilegedNamespaces = [];
this.restrictedNamespaces = [];
}
this.save();
},
onUpdateEnabled() {
if (!this.enabled) {
this.whitelistedNamespaces = [];
this.privilegedNamespaces = [];
this.restrictedNamespaces = [];
}
this.save();
},
updateWhitelisted(selected) {
this.whitelistedNamespaces = selected;
this.save();
},
updatePrivileged(selected) {
this.privilegedNamespaces = selected;
this.save();
},
updateRestricted(selected) {
this.restrictedNamespaces = selected;
this.save();
},
save() {
this.value.value = JSON.stringify({
enabled: this.enabled,
whitelistedNamespacesList: this.whitelistedNamespaces.join(','),
privilegedNamespacesList: this.privilegedNamespaces.join(','),
restrictedNamespacesList: this.restrictedNamespaces.join(','),
});
},
},
};
</script>
<template>
<div>
<div class="row mb-20">
<div class="col span-6">
<RadioGroup
v-model:value="enabled"
name="enabled"
:options="enabledOptions"
@update:value="onUpdateEnabled"
/>
</div>
</div>
<template v-if="enabled">
<div class="row mb-20">
<div class="col span-12">
<LabeledSelect
v-model:value="whitelistedNamespaces"
:label="t('harvester.setting.clusterPodSecurityStandard.whitelistedNamespaces.label')"
:options="whitelistedOptions"
:multiple="true"
:mode="mode"
@update:value="updateWhitelisted"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<LabeledSelect
v-model:value="privilegedNamespaces"
:label="t('harvester.setting.clusterPodSecurityStandard.privilegedNamespaces.label')"
:options="privilegedOptions"
:multiple="true"
:mode="mode"
@update:value="updatePrivileged"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<LabeledSelect
v-model:value="restrictedNamespaces"
:label="t('harvester.setting.clusterPodSecurityStandard.restrictedNamespaces.label')"
:options="restrictedOptions"
:multiple="true"
:mode="mode"
@update:value="updateRestricted"
/>
</div>
</div>
</template>
</div>
</template>
@@ -64,9 +64,6 @@ export default {
try { try {
parsedDefaultValue = JSON.parse(this.value.value); parsedDefaultValue = JSON.parse(this.value.value);
if (typeof parsedDefaultValue.exclusiveVlan !== 'boolean') {
parsedDefaultValue.exclusiveVlan = false;
}
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
openVlan = true; openVlan = true;
} catch (error) { } catch (error) {
@@ -75,8 +72,7 @@ export default {
vlan: '', vlan: '',
clusterNetwork: '', clusterNetwork: '',
range: '', range: '',
exclude: [], exclude: []
exclusiveVlan: false
}; };
} }
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || []; const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
@@ -98,10 +94,6 @@ export default {
}, },
computed: { computed: {
showExclusiveVlan() {
return this.networkType === L2VLAN &&
Number(this.parsedDefaultValue.vlan) !== 1;
},
showVlan() { showVlan() {
return this.networkType === L2VLAN; return this.networkType === L2VLAN;
}, },
@@ -140,18 +132,6 @@ export default {
}; };
}); });
}, },
exclusiveVlanOptions() {
return [
{
label: this.t('generic.enabled'),
value: true
},
{
label: this.t('generic.disabled'),
value: false
}
];
},
}, },
watch: { watch: {
@@ -194,8 +174,7 @@ export default {
vlan: '', vlan: '',
clusterNetwork: '', clusterNetwork: '',
range: '', range: '',
exclude: [], exclude: []
exclusiveVlan: false
}; };
}, },
@@ -301,15 +280,7 @@ export default {
label-key="harvester.setting.storageNetwork.vlan" label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan" @update:value="inputVlan"
/> />
<LabeledSelect
v-if="showExclusiveVlan"
v-model:value="parsedDefaultValue.exclusiveVlan"
class="mb-20"
:options="exclusiveVlanOptions"
:mode="mode"
label-key="harvester.setting.storageNetwork.exclusiveVlan"
@update:value="update"
/>
<LabeledSelect <LabeledSelect
v-model:value="parsedDefaultValue.clusterNetwork" v-model:value="parsedDefaultValue.clusterNetwork"
label-key="harvester.setting.storageNetwork.clusterNetwork" label-key="harvester.setting.storageNetwork.clusterNetwork"
+1 -8
View File
@@ -65,14 +65,7 @@ const FEATURE_FLAGS = {
'vGPUAsPCIDevice', 'vGPUAsPCIDevice',
'instanceManagerResourcesSetting', 'instanceManagerResourcesSetting',
'rwxNetworkSetting', 'rwxNetworkSetting',
'createPVCWithDataVolume', 'createPVCWithDataVolume'
'clusterPodSecurityStandardSetting',
],
'v1.8.1': [],
'v1.9.0': [
'supportFilesystem',
'disableResourcePooling',
'expandOnlineEncryptedVolume'
], ],
}; };
+11 -38
View File
@@ -81,6 +81,7 @@ export function init($plugin, store) {
configureType, configureType,
virtualType, virtualType,
weightGroup, weightGroup,
weightType,
} = $plugin.DSL(store, PRODUCT_NAME); } = $plugin.DSL(store, PRODUCT_NAME);
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME; const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
@@ -167,7 +168,7 @@ export function init($plugin, store) {
group: 'Root', group: 'Root',
name: HCI.HOST, name: HCI.HOST,
namespaced: true, namespaced: true,
weight: 499, weight: 399,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.HOST } params: { resource: HCI.HOST }
@@ -199,7 +200,7 @@ export function init($plugin, store) {
group: 'root', group: 'root',
name: HCI.VM, name: HCI.VM,
namespaced: true, namespaced: true,
weight: 498, weight: 299,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VM } params: { resource: HCI.VM }
@@ -237,7 +238,6 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimport', labelKey: 'harvester.addons.vmImport.labels.vmimport',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT } params: { resource: HCI.VMIMPORT }
@@ -267,7 +267,6 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare', labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_V,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V } params: { resource: HCI.VMIMPORT_SOURCE_V }
@@ -297,7 +296,6 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack', labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_O,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O } params: { resource: HCI.VMIMPORT_SOURCE_O }
@@ -326,7 +324,6 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA', labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA } params: { resource: HCI.VMIMPORT_SOURCE_OVA }
@@ -364,7 +361,7 @@ export function init($plugin, store) {
ifHaveType: PVC, ifHaveType: PVC,
name: HCI.VOLUME, name: HCI.VOLUME,
namespaced: true, namespaced: true,
weight: 497, weight: 199,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VOLUME } params: { resource: HCI.VOLUME }
@@ -390,7 +387,7 @@ export function init($plugin, store) {
group: 'root', group: 'root',
name: HCI.IMAGE, name: HCI.IMAGE,
namespaced: true, namespaced: true,
weight: 496, weight: 198,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IMAGE } params: { resource: HCI.IMAGE }
@@ -405,7 +402,7 @@ export function init($plugin, store) {
group: 'root', group: 'root',
namespaced: true, namespaced: true,
name: 'projects-namespaces', name: 'projects-namespaces',
weight: 495, weight: 98,
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` }, route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
exact: true, exact: true,
}); });
@@ -417,7 +414,7 @@ export function init($plugin, store) {
labelKey: 'harvester.namespace.label', labelKey: 'harvester.namespace.label',
name: NAMESPACE, name: NAMESPACE,
namespaced: true, namespaced: true,
weight: 495, weight: 89,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: NAMESPACE } params: { resource: NAMESPACE }
@@ -488,7 +485,6 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.CLUSTER_FLOW,
labelKey: 'harvester.logging.clusterFlow.label', labelKey: 'harvester.logging.clusterFlow.label',
name: HCI.CLUSTER_FLOW, name: HCI.CLUSTER_FLOW,
namespaced: true, namespaced: true,
@@ -512,7 +508,6 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.CLUSTER_OUTPUT,
labelKey: 'harvester.logging.clusterOutput.label', labelKey: 'harvester.logging.clusterOutput.label',
name: HCI.CLUSTER_OUTPUT, name: HCI.CLUSTER_OUTPUT,
namespaced: true, namespaced: true,
@@ -536,7 +531,6 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.FLOW,
labelKey: 'harvester.logging.flow.label', labelKey: 'harvester.logging.flow.label',
name: HCI.FLOW, name: HCI.FLOW,
namespaced: true, namespaced: true,
@@ -560,7 +554,6 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.OUTPUT,
labelKey: 'harvester.logging.output.label', labelKey: 'harvester.logging.output.label',
name: HCI.OUTPUT, name: HCI.OUTPUT,
namespaced: true, namespaced: true,
@@ -581,7 +574,6 @@ export function init($plugin, store) {
[ [
HCI.CLUSTER_NETWORK, HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT, HCI.NETWORK_ATTACHMENT,
HCI.HOST_NETWORK_CONFIG,
HCI.VPC, HCI.VPC,
NETWORK_POLICY, NETWORK_POLICY,
HCI.LB, HCI.LB,
@@ -600,8 +592,9 @@ export function init($plugin, store) {
'backupAndSnapshot' 'backupAndSnapshot'
); );
weightGroup('networks', 494, true); weightGroup('networks', 300, true);
weightGroup('backupAndSnapshot', 493, true); weightType(NAMESPACE, 299, true);
weightGroup('backupAndSnapshot', 289, true);
basicType( basicType(
[ [
@@ -695,7 +688,7 @@ export function init($plugin, store) {
}, },
resource: NETWORK_ATTACHMENT, resource: NETWORK_ATTACHMENT,
resourceDetail: HCI.NETWORK_ATTACHMENT, resourceDetail: HCI.NETWORK_ATTACHMENT,
resourceEdit: HCI.NETWORK_ATTACHMENT, resourceEdit: HCI.NETWORK_ATTACHMENT
}); });
virtualType({ virtualType({
@@ -1142,24 +1135,4 @@ export function init($plugin, store) {
ifHaveType: HCI.IP_POOL, ifHaveType: HCI.IP_POOL,
}); });
headers(HCI.IP_POOL, IP_POOL_HEADERS); headers(HCI.IP_POOL, IP_POOL_HEADERS);
configureType(HCI.HOST_NETWORK_CONFIG, {
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.HOST_NETWORK_CONFIG }
},
canYaml: false,
});
virtualType({
labelKey: 'harvester.hostNetworkConfig.label',
name: HCI.HOST_NETWORK_CONFIG,
namespaced: false,
weight: 183,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.HOST_NETWORK_CONFIG }
},
exact: false,
ifHaveType: HCI.HOST_NETWORK_CONFIG,
});
} }
-2
View File
@@ -88,8 +88,6 @@ export const CSI_SECRETS = {
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace', CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name', CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace', CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
CSI_NODE_EXPAND_SECRET_NAME: 'csi.storage.k8s.io/node-expand-secret-name',
CSI_NODE_EXPAND_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-expand-secret-namespace'
}; };
// Some harvester CRD type is not equal to model file name, define the mapping here // Some harvester CRD type is not equal to model file name, define the mapping here
@@ -19,7 +19,6 @@ export const HCI = {
NETWORK_TYPE: 'network.harvesterhci.io/type', NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName', VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix', VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory', VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status', MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name', HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
+1 -5
View File
@@ -41,8 +41,7 @@ export const HCI_SETTING = {
RANCHER_CLUSTER: 'rancher-cluster', RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio', MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
KUBEVIRT_MIGRATION: 'kubevirt-migration', KUBEVIRT_MIGRATION: 'kubevirt-migration',
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources', INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
}; };
export const HCI_ALLOWED_SETTINGS = { export const HCI_ALLOWED_SETTINGS = {
@@ -131,9 +130,6 @@ export const HCI_ALLOWED_SETTINGS = {
}, },
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: { [HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting' kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
},
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
} }
}; };
-6
View File
@@ -46,9 +46,3 @@ export const CDI_POPULATOR_KIND = {
VOLUME_IMPORT_SOURCE: 'VolumeImportSource', VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
VOLUME_CLONE_SOURCE: 'VolumeCloneSource', VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
}; };
export const FILESYSTEM_SOURCE_TYPE = {
CONFIGMAP: 'configmap',
SECRET: 'secret',
SERVICEACCOUNT: 'serviceaccount',
};
@@ -27,11 +27,15 @@ export default {
await allHash({ await allHash({
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }), vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }), vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
vmims: this.$store.dispatch('harvester/findAll', { type: HCI.VMIM }), allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
}); });
}, },
computed: { computed: {
allClusterNetwork() {
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
},
rows() { rows() {
const vms = this.$store.getters['harvester/all'](HCI.VM); const vms = this.$store.getters['harvester/all'](HCI.VM);
@@ -104,6 +108,7 @@ export default {
<HarvesterVmState <HarvesterVmState
class="vmstate" class="vmstate"
:row="scope.row" :row="scope.row"
:all-cluster-network="allClusterNetwork"
/> />
</div> </div>
</template> </template>
@@ -98,9 +98,9 @@ export default {
methods: { methods: {
escapeHtml, escapeHtml,
close(data) { close() {
this.errors = []; this.errors = [];
this.$emit('close', data); this.$emit('close');
}, },
async apply(buttonDone) { async apply(buttonDone) {
@@ -109,7 +109,7 @@ export default {
await resource.doActionGrowl(this.modalData.action, {}); await resource.doActionGrowl(this.modalData.action, {});
} }
buttonDone(true); buttonDone(true);
this.close({ performCallback: true, clearTableSelection: true }); this.close();
} catch (e) { } catch (e) {
this.errors = exceptionToErrorsArray(e); this.errors = exceptionToErrorsArray(e);
buttonDone(false); buttonDone(false);
+16 -31
View File
@@ -1,12 +1,9 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string'; import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types'; import { HCI } from '../types';
import { getHarvesterUserName } from '../utils/auth';
export default { export default {
name: 'HarvesterEnablePciPassthrough', name: 'HarvesterEnablePciPassthrough',
@@ -15,9 +12,7 @@ export default {
components: { components: {
AsyncButton, AsyncButton,
Banner,
Card, Card,
Checkbox,
}, },
props: { props: {
@@ -28,16 +23,10 @@ export default {
}, },
data() { data() {
return { disableResourcePooling: false }; return {};
}, },
computed: { computed: { ...mapGetters({ t: 'i18n/t' }) },
...mapGetters({ t: 'i18n/t' }),
disableResourcePoolingEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('disableResourcePooling');
},
},
methods: { methods: {
close() { close() {
@@ -45,7 +34,16 @@ export default {
}, },
async save(buttonCb) { async save(buttonCb) {
const userName = getHarvesterUserName(this.$store.getters); // 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;
}
for (let i = 0; i < this.resources.length; i++) { for (let i = 0; i < this.resources.length; i++) {
const actionResource = this.resources[i]; const actionResource = this.resources[i];
@@ -62,10 +60,9 @@ export default {
}] }]
}, },
spec: { spec: {
address: actionResource.status.address, address: actionResource.status.address,
nodeName: actionResource.status.nodeName, nodeName: actionResource.status.nodeName,
userName, userName
disableResourcePooling: this.disableResourcePooling,
} }
} ); } );
@@ -96,19 +93,7 @@ export default {
</template> </template>
<template #body> <template #body>
<p class="mb-20"> {{ t('harvester.pci.enablePassthroughWarning') }}
{{ t('harvester.pci.enablePassthroughWarning') }}
</p>
<template v-if="disableResourcePoolingEnabled">
<Checkbox
v-model:value="disableResourcePooling"
label-key="harvester.pci.disableResourcePooling"
/>
<Banner
color="info"
:label="t('harvester.pci.disableResourcePoolingDescription')"
/>
</template>
</template> </template>
<template #actions> <template #actions>
+10 -2
View File
@@ -4,7 +4,6 @@ import { Card } from '@components/Card';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string'; import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types'; import { HCI } from '../types';
import { getHarvesterUserName } from '../utils/auth';
export default { export default {
name: 'HarvesterEnableUSBPassthrough', name: 'HarvesterEnableUSBPassthrough',
@@ -35,7 +34,16 @@ export default {
}, },
async save(buttonCb) { async save(buttonCb) {
const userName = getHarvesterUserName(this.$store.getters); // 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;
}
for (let i = 0; i < this.resources.length; i++) { for (let i = 0; i < this.resources.length; i++) {
const actionResource = this.resources[i]; const actionResource = this.resources[i];
@@ -1,154 +0,0 @@
<script>
import merge from 'lodash/merge';
import jsyaml from 'js-yaml';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { LabeledInput } from '@components/Form/LabeledInput';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
export default {
name: 'HarvesterEnableNvidiaDriverToolkit',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
const addon = this.resources[0];
let valuesContentJson;
try {
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(addon.spec.valuesContent));
} catch (e) {
valuesContentJson = { ...DEFAULT_VALUE };
}
return { valuesContentJson };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
buttonDisabled() {
const { image, driverLocation } = this.valuesContentJson;
return !(image?.repository || '').trim() || !(image?.tag || '').trim() || !(driverLocation || '').trim();
}
},
methods: {
close() {
this.$emit('close');
},
async enable(buttonCb) {
const addon = this.resources[0];
try {
addon.spec.valuesContent = jsyaml.dump(this.valuesContentJson);
addon.spec.enabled = true;
await addon.save();
buttonCb(true);
this.close();
} catch (err) {
addon.spec.enabled = false;
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(addon.metadata.name) }),
err,
}, { root: true });
buttonCb(false);
}
}
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
<h4
v-clean-html="t('harvester.addons.nvidiaDriverToolkit.enable.title')"
class="text-default-text"
/>
</template>
<template #body>
<div class="body">
<div class="row mb-15">
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.image.repository"
:required="true"
:label="t('harvester.addons.nvidiaDriverToolkit.image.repository')"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.image.tag"
:required="true"
:label="t('harvester.addons.nvidiaDriverToolkit.image.tag')"
/>
</div>
</div>
<div class="row mb-15">
<div class="col span-12">
<LabeledInput
v-model:value="valuesContentJson.driverLocation"
:required="true"
:label="t('harvester.addons.nvidiaDriverToolkit.driver.location')"
/>
</div>
</div>
</div>
</template>
<template #actions>
<div class="buttons actions">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="enable"
:disabled="buttonDisabled"
@click="enable"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.body {
display: flex;
flex-direction: column;
min-width: 400px;
}
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -1,12 +1,15 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { NODE } from '@shell/config/types'; import { NODE } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI } from '../types';
export default { export default {
emits: ['close'], emits: ['close'],
@@ -59,46 +62,28 @@ export default {
return this.resources[0]; return this.resources[0];
}, },
anyCpuPinning() { vmi() {
return this.resources.some((r) => r.isCpuPinning); const inStore = this.$store.getters['currentProduct'].inStore;
}, const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
vmsByNode() { return resource;
const groups = {};
for (const r of this.resources) {
const node = r.nodeName || '';
const name = r.nameDisplay || r.name || r.id;
if (!groups[node]) {
groups[node] = [];
}
groups[node].push(name);
}
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
}, },
cpuPinningAlertMessage() { cpuPinningAlertMessage() {
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage'); return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
}, },
allVmsOnTargetNode() {
if (!this.nodeName) {
return false;
}
return this.resources.every((r) => r.nodeName === this.nodeName);
},
nodeNameList() { nodeNameList() {
const nodes = this.$store.getters['harvester/all'](NODE); const nodes = this.$store.getters['harvester/all'](NODE);
return nodes.filter((n) => { return nodes.filter((n) => {
const isNotSelfNode = !!this.availableNodes.includes(n.id);
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes. const isCpuPinning = this.actionResource?.isCpuPinning;
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
return isNotWitnessNode && matchingCpuManagerConfig; return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
}).map((n) => { }).map((n) => {
let label = n?.metadata?.name; let label = n?.metadata?.name;
const value = n?.metadata?.name; const value = n?.metadata?.name;
@@ -117,10 +102,10 @@ export default {
}, },
methods: { methods: {
close(data) { close() {
this.nodeName = ''; this.nodeName = '';
this.errors = []; this.errors = [];
this.$emit('close', data); this.$emit('close');
}, },
async apply(buttonDone) { async apply(buttonDone) {
@@ -141,32 +126,10 @@ export default {
} }
try { try {
// Filter out VMs already running on the selected node await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
const failedMigrations = results
.map((result, index) => ({ resource: toMigrate[index], result }))
.filter(({ result }) => result.status === 'rejected');
if (failedMigrations.length) {
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
const error = result.reason?.data || result.reason;
const messages = exceptionToErrorsArray(error);
return messages.map((message) => `${ vmName }: ${ message }`);
});
buttonDone(false);
return;
}
buttonDone(true); buttonDone(true);
this.close({ performCallback: true, clearTableSelection: true }); this.close();
} catch (err) { } catch (err) {
const error = err?.data || err; const error = err?.data || err;
const message = exceptionToErrorsArray(error); const message = exceptionToErrorsArray(error);
@@ -183,35 +146,17 @@ export default {
<template> <template>
<Card :show-highlight-border="false"> <Card :show-highlight-border="false">
<template #title> <template #title>
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }} {{ t('harvester.modal.migration.title') }}
</template> </template>
<template #body> <template #body>
<Banner <Banner
v-if="anyCpuPinning" v-if="actionResource?.isCpuPinning"
color="warning" color="warning"
:label="cpuPinningAlertMessage" :label="cpuPinningAlertMessage"
/> />
<p>
{{ t('harvester.modal.migration.selectedVMs') }}
</p>
<ul class="vm-list">
<li
v-for="group in vmsByNode"
:key="group.node"
>
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
<span
v-if="nodeName && group.node === nodeName"
class="already-on-target"
>
({{ t('harvester.modal.migration.alreadyOnTarget') }})
</span>
</li>
</ul>
<LabeledSelect <LabeledSelect
v-model:value="nodeName" v-model:value="nodeName"
class="mt-15"
:label="t('harvester.modal.migration.fields.nodeName.label')" :label="t('harvester.modal.migration.fields.nodeName.label')"
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')" :placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
:options="nodeNameList" :options="nodeNameList"
@@ -238,7 +183,7 @@ export default {
<AsyncButton <AsyncButton
mode="apply" mode="apply"
:disabled="!nodeName || allVmsOnTargetNode" :disabled="!nodeName"
@click="apply" @click="apply"
/> />
</div> </div>
@@ -256,16 +201,4 @@ export default {
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
} }
.already-on-target {
color: var(--warning);
font-style: italic;
}
.vm-list {
list-style: disc;
padding-left: 1.5em;
margin-bottom: 10px;
margin-top: 10px;
}
</style> </style>
@@ -71,38 +71,21 @@ export default {
}, },
data() { data() {
let provisioner = `${ this.value.provisioner || LONGHORN_DRIVER }`;
if (provisioner === LONGHORN_DRIVER) {
provisioner = `${ provisioner }_${ this.value.provisionerVersion || LONGHORN_VERSION_V1 }`;
}
return { return {
provisioner,
volumeGroupDialog: null, volumeGroupDialog: null,
randomStr: randomStr(10).toLowerCase(), randomStr: randomStr(10).toLowerCase(),
isOpen: false, isOpen: false
}; };
}, },
computed: { 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() { provisioners() {
const out = []; const out = [];
@@ -300,6 +283,20 @@ export default {
}, },
watch: { 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) { 'value.lvmVolumeGroup'(neu) {
if (neu === _NEW) { if (neu === _NEW) {
this.value.lvmVolumeGroup = null; this.value.lvmVolumeGroup = null;
+2 -10
View File
@@ -8,7 +8,6 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { getLoginAwareErrors } from '../utils/error';
export default { export default {
name: 'HarvesterEditKeypair', name: 'HarvesterEditKeypair',
@@ -64,14 +63,6 @@ export default {
} }
}, },
computed: {
normalizedErrors() {
const message = this.t('harvester.virtualMachine.genericLoginError');
return getLoginAwareErrors(this.errors, message);
}
},
methods: { onKeySelected: createOnSelected('publicKey') }, methods: { onKeySelected: createOnSelected('publicKey') },
}; };
</script> </script>
@@ -81,9 +72,10 @@ export default {
:done-route="doneRoute" :done-route="doneRoute"
:resource="value" :resource="value"
:mode="mode" :mode="mode"
:errors="normalizedErrors" :errors="errors"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<div class="header mb-20"> <div class="header mb-20">
<FileSelector <FileSelector
@@ -3,9 +3,8 @@ import KeyValue from '@shell/components/form/KeyValue';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import RadioGroup from '@components/Form/Radio/RadioGroup'; import RadioGroup from '@components/Form/Radio/RadioGroup';
import Checkbox from '@components/Form/Checkbox/Checkbox';
import { SECRET, LONGHORN } from '@shell/config/types'; import { SECRET, LONGHORN } from '@shell/config/types';
import { _CREATE, _VIEW, _EDIT } from '@shell/config/query-params'; import { _CREATE, _VIEW } from '@shell/config/query-params';
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map'; import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
import { clone } from '@shell/utils/object'; import { clone } from '@shell/utils/object';
import { uniq } from '@shell/utils/array'; import { uniq } from '@shell/utils/array';
@@ -17,9 +16,7 @@ const {
CSI_NODE_PUBLISH_SECRET_NAME, CSI_NODE_PUBLISH_SECRET_NAME,
CSI_NODE_PUBLISH_SECRET_NAMESPACE, CSI_NODE_PUBLISH_SECRET_NAMESPACE,
CSI_NODE_STAGE_SECRET_NAME, CSI_NODE_STAGE_SECRET_NAME,
CSI_NODE_STAGE_SECRET_NAMESPACE, CSI_NODE_STAGE_SECRET_NAMESPACE
CSI_NODE_EXPAND_SECRET_NAME,
CSI_NODE_EXPAND_SECRET_NAMESPACE
} = CSI_SECRETS; } = CSI_SECRETS;
export default { export default {
@@ -30,7 +27,6 @@ export default {
LabeledSelect, LabeledSelect,
LabeledInput, LabeledInput,
RadioGroup, RadioGroup,
Checkbox,
}, },
props: { props: {
@@ -61,10 +57,7 @@ export default {
}; };
} }
const hasExpandSecret = !!(this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAME] && this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAMESPACE]); return { };
const volumeExpansionCheckBoxEnabled = this.realMode === _CREATE ? true : hasExpandSecret;
return { volumeExpansionCheckBoxEnabled };
}, },
computed: { computed: {
@@ -105,11 +98,8 @@ export default {
}, []); }, []);
}, },
isEdit() {
return this.realMode === _EDIT;
},
isView() { isView() {
return this.realMode === _VIEW; return this.mode === _VIEW;
}, },
migratableOptions() { migratableOptions() {
@@ -162,10 +152,6 @@ export default {
} }
}, },
enableOnlineExpansionVolumeEncryption() {
return this.value.expandOnlineEncryptedVolumeFeatureEnabled;
},
volumeEncryption: { volumeEncryption: {
set(neu) { set(neu) {
this.value['parameters'] = { this.value['parameters'] = {
@@ -194,11 +180,6 @@ export default {
set(selectedSecret) { set(selectedSecret) {
const [namespace, name] = selectedSecret.split('/'); const [namespace, name] = selectedSecret.split('/');
const expandSecretParams = (this.enableOnlineExpansionVolumeEncryption && this.volumeExpansionCheckBoxEnabled) ? {
[CSI_NODE_EXPAND_SECRET_NAME]: name,
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
} : {};
this.value['parameters'] = { this.value['parameters'] = {
...this.value.parameters, ...this.value.parameters,
[CSI_PROVISIONER_SECRET_NAME]: name, [CSI_PROVISIONER_SECRET_NAME]: name,
@@ -206,8 +187,7 @@ export default {
[CSI_NODE_STAGE_SECRET_NAME]: name, [CSI_NODE_STAGE_SECRET_NAME]: name,
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace, [CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace, [CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace, [CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace
...expandSecretParams,
}; };
} }
}, },
@@ -260,32 +240,6 @@ export default {
} }
}, },
}, },
watch: {
volumeExpansionCheckBoxEnabled(enabled) {
const currentSecret = this.secret;
if (!currentSecret) {
return;
}
const [namespace, name] = currentSecret.split('/');
if (enabled && this.enableOnlineExpansionVolumeEncryption) {
this.value['parameters'] = {
...this.value.parameters,
[CSI_NODE_EXPAND_SECRET_NAME]: name,
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
};
} else {
const params = { ...this.value.parameters };
delete params[CSI_NODE_EXPAND_SECRET_NAME];
delete params[CSI_NODE_EXPAND_SECRET_NAMESPACE];
this.value['parameters'] = params;
}
},
},
}; };
</script> </script>
<template> <template>
@@ -383,16 +337,6 @@ export default {
:mode="mode" :mode="mode"
/> />
</div> </div>
<div
v-if="enableOnlineExpansionVolumeEncryption"
class="col span-6 flex items-center mt-20"
>
<Checkbox
v-model:value="volumeExpansionCheckBoxEnabled"
:label="t('harvester.storage.volumeExpansionCheckbox')"
:disabled="isEdit || isView"
/>
</div>
</div> </div>
</template> </template>
<KeyValue <KeyValue
@@ -254,24 +254,15 @@ export default {
v-if="restoreNewVm" v-if="restoreNewVm"
v-model:value="restoreResource.spec.keepMacAddress" v-model:value="restoreResource.spec.keepMacAddress"
type="checkbox" type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.keepMacAddress')" :label="t('harvester.backup.restore.keepMacAddress')"
/> />
<LabeledSelect <LabeledSelect
v-if="!restoreNewVm" v-if="!restoreNewVm"
v-model:value="deletionPolicy" v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')" :label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption" :options="deletionPolicyOption"
/> />
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div> </div>
<Footer <Footer
@@ -26,7 +26,6 @@ import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel'; import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig'; import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey'; import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
import Filesystem from './kubevirt.io.virtualmachine/VirtualMachineFilesystem';
export default { export default {
name: 'HarvesterEditVMTemplate', name: 'HarvesterEditVMTemplate',
@@ -52,7 +51,6 @@ export default {
UnitInput, UnitInput,
Banner, Banner,
KeyValue, KeyValue,
Filesystem,
}, },
mixins: [CreateEditView, VM_MIXIN], mixins: [CreateEditView, VM_MIXIN],
@@ -97,10 +95,6 @@ export default {
secretNamePrefix() { secretNamePrefix() {
return this.templateValue?.metadata?.name; return this.templateValue?.metadata?.name;
}, },
filesystemEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
},
}, },
watch: { watch: {
@@ -160,7 +154,6 @@ export default {
mounted() { mounted() {
this.imageId = this.diskRows[0]?.image || ''; this.imageId = this.diskRows[0]?.image || '';
this['filesystemRows'] = this.getFilesystemRows(this.value.spec.vm);
}, },
methods: { methods: {
@@ -356,19 +349,6 @@ export default {
</template> </template>
</Tab> </Tab>
<Tab
v-if="filesystemEnabled"
name="filesystem"
:label="t('harvester.tab.filesystem')"
:weight="-8"
>
<Filesystem
v-model:value="filesystemRows"
:mode="mode"
:namespace="templateValue.metadata.namespace"
/>
</Tab>
<Tab <Tab
name="labels" name="labels"
:label="t('generic.labels')" :label="t('generic.labels')"
@@ -2,7 +2,6 @@
import Footer from '@shell/components/form/Footer'; import Footer from '@shell/components/form/Footer';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import Checkbox from '@components/Form/Checkbox/Checkbox';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
@@ -33,7 +32,6 @@ const createObject = {
export default { export default {
name: 'CreateRestore', name: 'CreateRestore',
components: { components: {
Checkbox,
Footer, Footer,
RadioGroup, RadioGroup,
LabeledInput, LabeledInput,
@@ -251,17 +249,9 @@ export default {
<LabeledSelect <LabeledSelect
v-if="!restoreNewVm" v-if="!restoreNewVm"
v-model:value="deletionPolicy" v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')" :label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption" :options="deletionPolicyOption"
/> />
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div> </div>
<Footer <Footer
+7 -27
View File
@@ -43,20 +43,18 @@ export default {
created() { created() {
const vpc = this.$route.query.vpc || ''; const vpc = this.$route.query.vpc || '';
const enableDHCP = this.value?.spec?.enableDHCP || false; const enableDHCP = this.value?.spec?.enableDHCP || false;
const natOutgoing = this.value?.spec?.natOutgoing || false;
set(this.value.spec, 'enableDHCP', enableDHCP); set(this.value.spec, 'enableDHCP', enableDHCP);
set(this.value, 'spec', this.value.spec || { set(this.value, 'spec', this.value.spec || {
cidrBlock: '', cidrBlock: '',
protocol: NETWORK_PROTOCOL.IPv4, protocol: NETWORK_PROTOCOL.IPv4,
provider: '', provider: '',
vpc, vpc,
gateway: '', gatewayIP: '',
excludeIps: [], excludeIps: [],
private: false, private: false,
enableDHCP, enableDHCP,
natOutgoing, acls: []
acls: []
}); });
}, },
@@ -131,10 +129,6 @@ export default {
label: n.id, label: n.id,
value: n.id, value: n.id,
})); }));
},
natOutgoingDisabled() {
// Disable the NAT Outgoing option when the subnet belongs to the ovn-cluster VPC and its name is join or ovn-default.
return this.value?.spec?.vpc === 'ovn-cluster' && ['join', 'ovn-default'].includes(this.value?.metadata?.name);
} }
}, },
@@ -310,20 +304,6 @@ export default {
</Banner> </Banner>
</div> </div>
</div> </div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
v-model:value="value.spec.natOutgoing"
name="enableExternalConnectivity"
:disabled="natOutgoingDisabled"
:options="[true, false]"
:label="t('harvester.subnet.externalConnectivity.label')"
:labels="[t('generic.enabled'), t('generic.disabled')]"
:mode="mode"
:tooltip="t('harvester.subnet.externalConnectivity.tooltip')"
/>
</div>
</div>
<div class="row mt-20"> <div class="row mt-20">
<div class="col span-6"> <div class="col span-6">
<RadioGroup <RadioGroup
@@ -1,310 +0,0 @@
<script>
import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import { CONFIG_MAP, SECRET, SERVICE_ACCOUNT } from '@shell/config/types';
import { _VIEW } from '@shell/config/query-params';
import CopyToClipboard from '@shell/components/CopyToClipboard';
import MessageLink from '@shell/components/MessageLink';
import { FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
const MAX_FILESYSTEMS = 3;
const { CONFIGMAP: FS_TYPE_CONFIGMAP, SECRET: FS_TYPE_SECRET, SERVICEACCOUNT: FS_TYPE_SERVICEACCOUNT } = FILESYSTEM_SOURCE_TYPE;
const DEFAULT_VOLUME_NAMES = {
[FS_TYPE_CONFIGMAP]: 'appconfigfs',
[FS_TYPE_SECRET]: 'appsecretfs',
[FS_TYPE_SERVICEACCOUNT]: 'appserviceaccountfs',
};
const FS_TYPE_OPTIONS = [
{ label: 'ConfigMap', value: FS_TYPE_CONFIGMAP },
{ label: 'Secret', value: FS_TYPE_SECRET },
{ label: 'ServiceAccount', value: FS_TYPE_SERVICEACCOUNT },
];
function emptyRow() {
return {
fsType: FS_TYPE_CONFIGMAP,
volumeName: DEFAULT_VOLUME_NAMES[FS_TYPE_CONFIGMAP],
resourceName: '',
};
}
export default {
name: 'VirtualMachineFilesystem',
emits: ['update:value'],
components: {
Banner,
LabeledSelect,
LabeledInput,
CopyToClipboard,
MessageLink,
},
props: {
mode: {
type: String,
default: 'create',
},
namespace: {
type: String,
default: '',
},
value: {
type: Array,
default: () => [],
},
},
data() {
return { rows: this.value.length > 0 ? this.value.map((r) => ({ ...r })) : [emptyRow()] };
},
watch: {
value(newVal) {
if (newVal) {
const incoming = JSON.stringify(newVal);
const current = JSON.stringify(this.rows);
if (incoming !== current) {
this.rows = newVal.map((r) => ({ ...r }));
}
}
},
rows: {
deep: true,
handler(val) {
this.$emit('update:value', val.map((r) => ({ ...r })));
},
},
},
computed: {
...mapGetters({ t: 'i18n/t' }),
inStore() {
return this.$store.getters['currentProduct'].inStore;
},
configMaps() {
return this.$store.getters[`${ this.inStore }/all`](CONFIG_MAP)
.filter((cm) => !this.namespace || cm.metadata.namespace === this.namespace)
.map((cm) => ({ label: cm.metadata.name, value: cm.metadata.name }));
},
secrets() {
return this.$store.getters[`${ this.inStore }/all`](SECRET)
.filter((s) => !this.namespace || s.metadata.namespace === this.namespace)
.map((s) => ({ label: s.metadata.name, value: s.metadata.name }));
},
serviceAccounts() {
return this.$store.getters[`${ this.inStore }/all`](SERVICE_ACCOUNT)
.filter((sa) => !this.namespace || sa.metadata.namespace === this.namespace)
.map((sa) => ({ label: sa.metadata.name, value: sa.metadata.name }));
},
canAddRow() {
return this.rows.length < MAX_FILESYSTEMS;
},
isView() {
return this.mode === _VIEW;
},
completedRows() {
return this.rows.filter((r) => r.fsType && r.volumeName && r.resourceName);
},
allMountCommands() {
return this.completedRows.map((r) => this.mountCommands(r)).join('\n');
},
},
methods: {
fsTypeOptions(currentIndex) {
const usedTypes = this.rows
.filter((_, i) => i !== currentIndex)
.map((r) => r.fsType);
return FS_TYPE_OPTIONS.filter((opt) => !usedTypes.includes(opt.value));
},
resourceOptions(fsType) {
if (fsType === FS_TYPE_CONFIGMAP) return this.configMaps;
if (fsType === FS_TYPE_SECRET) return this.secrets;
if (fsType === FS_TYPE_SERVICEACCOUNT) return this.serviceAccounts;
return [];
},
onFsTypeChange(row, newType) {
row.fsType = newType;
row.volumeName = DEFAULT_VOLUME_NAMES[newType] || '';
row.resourceName = '';
},
addRow() {
if (this.canAddRow) {
const usedTypes = this.rows.map((r) => r.fsType);
const nextType = FS_TYPE_OPTIONS.find((opt) => !usedTypes.includes(opt.value))?.value || FS_TYPE_CONFIGMAP;
this.rows.push({
fsType: nextType,
volumeName: DEFAULT_VOLUME_NAMES[nextType] || '',
resourceName: '',
});
}
},
removeRow(index) {
this.rows.splice(index, 1);
},
mountCommands(row) {
const vol = row.volumeName || '<volume-name>';
return `- mkdir -p /mnt/${ vol }\n- mount -t virtiofs ${ vol } /mnt/${ vol }`;
},
},
};
</script>
<template>
<div class="vm-filesystem">
<p class="mb-20">
{{ t('harvester.virtualMachine.filesystem.description') }}
</p>
<div
v-for="(row, index) in rows"
:key="index"
class="filesystem-row mb-15"
>
<div class="row">
<div class="col span-3">
<LabeledSelect
:value="row.fsType"
:label="t('harvester.virtualMachine.filesystem.type')"
:options="fsTypeOptions(index)"
:mode="mode"
required
@update:value="onFsTypeChange(row, $event)"
/>
</div>
<div class="col span-3">
<LabeledInput
v-model:value="row.volumeName"
:label="t('harvester.virtualMachine.filesystem.volume')"
:mode="mode"
required
/>
</div>
<div class="col span-5">
<LabeledSelect
v-model:value="row.resourceName"
:label="t('harvester.virtualMachine.filesystem.resource')"
:options="resourceOptions(row.fsType)"
:mode="mode"
required
/>
</div>
<div
v-if="!isView"
class="col span-1 remove-col"
>
<button
type="button"
class="btn role-link remove-btn"
@click="removeRow(index)"
>
{{ t('generic.remove') }}
</button>
</div>
</div>
</div>
<Banner
v-if="completedRows.length > 0 && mode === 'create'"
color="warning"
class="mt-10"
>
<div>
<MessageLink
:to="{ hash: '#advanced' }"
prefix-label="harvester.virtualMachine.filesystem.mountBannerHint"
middle-label="harvester.virtualMachine.filesystem.mountBannerHintLink"
suffix-label="harvester.virtualMachine.filesystem.mountBannerHintSuffix"
/>
<div class="pre-wrapper mt-10">
<pre class="mt-5 mb-0">{{ allMountCommands }}</pre>
<CopyToClipboard
:text="allMountCommands"
:show-label="false"
class="icon-btn"
action-color="bg-transparent"
/>
</div>
</div>
</Banner>
<button
v-if="!isView && canAddRow"
type="button"
class="btn role-tertiary add"
@click="addRow"
>
{{ t('harvester.virtualMachine.filesystem.add') }}
</button>
</div>
</template>
<style lang="scss" scoped>
.vm-filesystem {
padding: 10px 0;
}
.filesystem-row {
border-bottom: 1px solid var(--border);
padding-bottom: 15px;
}
.remove-col {
display: flex;
align-items: center;
justify-content: center;
}
.remove-btn {
padding: 0;
}
.pre-wrapper {
position: relative;
pre {
padding-right: 36px;
}
.icon-btn {
position: absolute;
top: 0px;
right: 5px;
padding: 2px 4px;
line-height: 1;
&:active {
background-color: var(--success) !important;
}
}
}
</style>
@@ -152,10 +152,6 @@ export default {
return !rows.find((device) => !device.passthroughClaim); return !rows.find((device) => !device.passthroughClaim);
}, },
canManageGroup(rows = []) {
return rows.length > 0 && rows.every((row) => row.canUpdate === true);
},
changeRows(filterRows, parentSriov) { changeRows(filterRows, parentSriov) {
this['filterRows'] = filterRows; this['filterRows'] = filterRows;
this['parentSriov'] = parentSriov; this['parentSriov'] = parentSriov;
@@ -189,27 +185,22 @@ export default {
v-trim-whitespace v-trim-whitespace
class="group-tab" class="group-tab"
> >
<div <button
v-if="canManageGroup(group.rows)" v-if="groupIsAllEnabled(group.rows)"
class="group-actions" type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{disableGroup(group.rows); e.target.blur()}"
> >
<button {{ t('harvester.pci.disableGroup') }}
v-if="groupIsAllEnabled(group.rows)" </button>
type="button" <button
class="btn btn-sm role-secondary mr-5" v-else
@click="e=>{disableGroup(group.rows); e.target.blur()}" type="button"
> class="btn btn-sm role-secondary mr-5"
{{ t('harvester.pci.disableGroup') }} @click="e=>{enableGroup(group.rows); e.target.blur()}"
</button> >
<button {{ t('harvester.pci.enableGroup') }}
v-else </button>
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{enableGroup(group.rows); e.target.blur()}"
>
{{ t('harvester.pci.enableGroup') }}
</button>
</div>
<span v-clean-html="group.key" /> <span v-clean-html="group.key" />
</div> </div>
</template> </template>
@@ -241,9 +232,3 @@ export default {
</template> </template>
</ResourceTable> </ResourceTable>
</template> </template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>
@@ -52,21 +52,31 @@ export default {
} }
const selectedDevices = []; const selectedDevices = [];
const oldFormatDevices = [];
const vmDevices = this.value?.domain?.devices?.hostDevices || []; const vmDevices = this.value?.domain?.devices?.hostDevices || [];
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
const vmDeviceNames = vmDevices.map(({ name }) => name); const vmDeviceNames = vmDevices.map(({ name }) => name);
this.pciDevices.forEach((row) => { this.pciDevices.forEach((row) => {
row.allowDisable = !vmDeviceNames.includes(row.metadata.name); row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
}); });
vmDevices.forEach(({ name }) => { vmDevices.forEach(({ name, deviceName }) => {
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) { const checkName = (deviceName || '').split('/')?.[1];
if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
oldFormatDevices.push(name);
} else if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
selectedDevices.push(name); selectedDevices.push(name);
} }
}); });
this.selectedDevices = selectedDevices; if (oldFormatDevices.length > 0) {
this.oldFormatDevices = oldFormatDevices;
} else {
this.selectedDevices = selectedDevices;
}
}, },
data() { data() {
@@ -77,6 +87,7 @@ export default {
selectedDevices: [], selectedDevices: [],
pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE), pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE),
showMatrix: false, showMatrix: false,
oldFormatDevices: [],
}; };
}, },
@@ -188,6 +199,11 @@ export default {
}); });
}, },
oldFormatDevicesHTML() {
return this.oldFormatDevices.map((device) => {
return `<li>${ device }</li>`;
}).join('');
},
}, },
methods: { methods: {
@@ -211,89 +227,101 @@ export default {
<template> <template>
<div> <div>
<div class="row"> <div
v-if="oldFormatDevices.length > 0"
class="row"
>
<div class="col span-12"> <div class="col span-12">
<Banner color="info"> <Banner color="warning">
<MessageLink <p v-clean-html="t('harvester.pci.oldFormatDevices.help', {oldFormatDevicesHTML}, true)" />
: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"
color="info"
>
<t k="harvester.pci.deviceInTheSameHost" />
</Banner> </Banner>
</div> </div>
</div> </div>
<template v-if="enabledDevices.length"> <div v-else>
<div class="row"> <div class="row">
<div class="col span-6">
<LabeledSelect
v-model:value="selectedDevices"
label="Available PCI Devices"
searchable
multiple
taggable
:options="deviceOpts"
:mode="mode"
>
<template #option="option">
<span>{{ option.value }} <span class="text-label">({{ option.displayLabel }})</span></span>
</template>
</LabeledSelect>
</div>
</div>
<div
v-if="compatibleNodes.length && selectedDevices.length"
class="row"
>
<div class="col span-12 text-muted">
Compatible hosts:
<!-- eslint-disable-next-line vue/no-parsing-error -->
<span
v-for="(node, idx) in compatibleNodes"
:key="idx"
>{{ node }}{{ idx < compatibleNodes.length-1 ? ', ' : '' }}</span>
</div>
</div>
<div
v-else-if="selectedDevices.length"
class="text-error"
>
{{ t('harvester.pci.impossibleSelection') }}
</div>
<button
type="button"
class="btn btn-sm role-link pl-0"
@click="e=>{showMatrix = !showMatrix; e.target.blur()}"
>
{{ showMatrix ? t('harvester.pci.hideCompatibility') : t('harvester.pci.showCompatibility') }}
</button>
<div
v-if="showMatrix"
class="row mt-20"
>
<div class="col span-12"> <div class="col span-12">
<CompatibilityMatrix <Banner color="info">
:enabled-devices="enabledDevices" <MessageLink
:devices-by-node="devicesByNode" :to="toVGpuDevicesPage"
:devices-in-use="devicesInUse" prefix-label="harvester.pci.howToUseDevice.prefix"
middle-label="harvester.pci.howToUseDevice.middle"
suffix-label="harvester.pci.howToUseDevice.suffix"
/>
</Banner>
<Banner
v-if="selectedDevices.length > 0"
color="info"
>
<t k="harvester.pci.deviceInTheSameHost" />
</Banner>
</div>
</div>
<template v-if="enabledDevices.length">
<div class="row">
<div class="col span-6">
<LabeledSelect
v-model:value="selectedDevices"
label="Available PCI Devices"
searchable
multiple
taggable
:options="deviceOpts"
:mode="mode"
>
<template #option="option">
<span>{{ option.value }} <span class="text-label">({{ option.displayLabel }})</span></span>
</template>
</LabeledSelect>
</div>
</div>
<div
v-if="compatibleNodes.length && selectedDevices.length"
class="row"
>
<div class="col span-12 text-muted">
Compatible hosts:
<!-- eslint-disable-next-line vue/no-parsing-error -->
<span
v-for="(node, idx) in compatibleNodes"
:key="idx"
>{{ node }}{{ idx < compatibleNodes.length-1 ? ', ' : '' }}</span>
</div>
</div>
<div
v-else-if="selectedDevices.length"
class="text-error"
>
{{ t('harvester.pci.impossibleSelection') }}
</div>
<button
type="button"
class="btn btn-sm role-link pl-0"
@click="e=>{showMatrix = !showMatrix; e.target.blur()}"
>
{{ showMatrix ? t('harvester.pci.hideCompatibility') : t('harvester.pci.showCompatibility') }}
</button>
<div
v-if="showMatrix"
class="row mt-20"
>
<div class="col span-12">
<CompatibilityMatrix
:enabled-devices="enabledDevices"
:devices-by-node="devicesByNode"
:devices-in-use="devicesInUse"
/>
</div>
</div>
</template>
<div class="row mt-20">
<div class="col span-12">
<DeviceList
:schema="pciDeviceSchema"
:devices="pciDevices"
@submit.prevent
/> />
</div> </div>
</div> </div>
</template>
<div class="row mt-20">
<div class="col span-12">
<DeviceList
:schema="pciDeviceSchema"
:devices="pciDevices"
@submit.prevent
/>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -10,7 +10,6 @@ import { _VIEW } from '@shell/config/query-params';
import { NAMESPACE } from '@shell/config/types'; import { NAMESPACE } from '@shell/config/types';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { getLoginAwareErrors } from '../../utils/error';
const _NEW = '_NEW'; const _NEW = '_NEW';
@@ -215,9 +214,7 @@ export default {
buttonCb(true); buttonCb(true);
this.cancel(); this.cancel();
} catch (err) { } catch (err) {
const message = this.t('harvester.virtualMachine.genericLoginError'); this.errors = [err.message];
this.errors = getLoginAwareErrors(err, message);
buttonCb(false); buttonCb(false);
} }
}, },
@@ -64,12 +64,6 @@ export default {
value: 'status.productID', value: 'status.productID',
sort: ['status.productID', 'status.vendorID'] sort: ['status.productID', 'status.vendorID']
}, },
{
name: 'classType',
labelKey: 'harvester.usb.classType',
value: 'status.classType',
sort: ['status.classType']
},
]; ];
if (!isSingleProduct) { if (!isSingleProduct) {
@@ -113,11 +107,6 @@ export default {
} }
}); });
}, },
canManageGroup(rows = []) {
return rows.every((row) => row.canUpdate === true);
},
groupIsAllEnabled(rows = []) { groupIsAllEnabled(rows = []) {
return !rows.find((device) => !device.passthroughClaim); return !rows.find((device) => !device.passthroughClaim);
}, },
@@ -158,27 +147,22 @@ export default {
v-trim-whitespace v-trim-whitespace
class="group-tab" class="group-tab"
> >
<div <button
v-if="canManageGroup(group.rows)" v-if="groupIsAllEnabled(group.rows)"
class="group-actions" type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{disableGroup(group.rows); e.target.blur()}"
> >
<button {{ t('harvester.usb.disableGroup') }}
v-if="groupIsAllEnabled(group.rows)" </button>
type="button" <button
class="btn btn-sm role-secondary mr-5" v-else
@click="e=>{disableGroup(group.rows); e.target.blur()}" type="button"
> class="btn btn-sm role-secondary mr-5"
{{ t('harvester.usb.disableGroup') }} @click="e=>{enableGroup(group.rows); e.target.blur()}"
</button> >
<button {{ t('harvester.usb.enableGroup') }}
v-else </button>
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{enableGroup(group.rows); e.target.blur()}"
>
{{ t('harvester.usb.enableGroup') }}
</button>
</div>
<span v-clean-html="group.key" /> <span v-clean-html="group.key" />
</div> </div>
</template> </template>
@@ -191,9 +175,3 @@ export default {
</template> </template>
</ResourceTable> </ResourceTable>
</template> </template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>
@@ -475,6 +475,7 @@ export default {
<button <button
type="button" type="button"
class="btn btn-sm bg-primary mr-15 mb-10" class="btn btn-sm bg-primary mr-15 mb-10"
:disabled="rows.length === 0"
@click="addVolume(SOURCE_TYPE.NEW)" @click="addVolume(SOURCE_TYPE.NEW)"
> >
{{ t('harvester.virtualMachine.volume.addVolume') }} {{ t('harvester.virtualMachine.volume.addVolume') }}
@@ -21,7 +21,9 @@ import { saferDump } from '@shell/utils/create-yaml';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook'; import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { parseVolumeClaimTemplates } from '@pkg/utils/vm'; import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
import VM_MIXIN from '../../mixins/harvester-vm'; import VM_MIXIN from '../../mixins/harvester-vm';
import { HCI } from '../../types'; import { HCI } from '../../types';
@@ -35,7 +37,6 @@ import Network from './VirtualMachineNetwork';
import Volume from './VirtualMachineVolume'; import Volume from './VirtualMachineVolume';
import SSHKey from './VirtualMachineSSHKey'; import SSHKey from './VirtualMachineSSHKey';
import Reserved from './VirtualMachineReserved'; import Reserved from './VirtualMachineReserved';
import Filesystem from './VirtualMachineFilesystem';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import MessageLink from '@shell/components/MessageLink'; import MessageLink from '@shell/components/MessageLink';
@@ -71,7 +72,6 @@ export default {
Banner, Banner,
MessageLink, MessageLink,
UsbDevices, UsbDevices,
Filesystem,
}, },
mixins: [CreateEditView, VM_MIXIN], mixins: [CreateEditView, VM_MIXIN],
@@ -91,36 +91,22 @@ export default {
const hostname = this.value.spec.template.spec.hostname || ''; const hostname = this.value.spec.template.spec.hostname || '';
const customizeDisplayName = !!(this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME]);
return { return {
cloneVM, cloneVM,
count: 2, count: 2,
templateId: '', templateId: '',
templateVersionId: '', templateVersionId: '',
namePrefix: '', namePrefix: '',
isSingle: true, isSingle: true,
isOpen: false, isOpen: false,
hostname, hostname,
isRestartImmediately, isRestartImmediately,
customizeDisplayName,
}; };
}, },
computed: { computed: {
...mapGetters({ t: 'i18n/t' }), ...mapGetters({ t: 'i18n/t' }),
// VM display name is stored as an annotation; bind a dedicated input to it
// so we don't have to mutate metadata.name (which would break the k8s PUT).
displayName: {
get() {
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || '';
},
set(val) {
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, val);
},
},
to() { to() {
return { return {
name: 'harvester-c-cluster-resource', name: 'harvester-c-cluster-resource',
@@ -232,9 +218,6 @@ export default {
usbPassthroughEnabled() { usbPassthroughEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough'); return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
}, },
filesystemEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
},
}, },
watch: { watch: {
@@ -306,12 +289,6 @@ export default {
this.getInitConfig({ value: this.value, init: this.isCreate }); this.getInitConfig({ value: this.value, init: this.isCreate });
} }
}, },
customizeDisplayName(neu) {
if (!neu) {
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, '');
}
},
}, },
created() { created() {
@@ -344,7 +321,6 @@ export default {
const diskRows = this.getDiskRows(this.value); const diskRows = this.getDiskRows(this.value);
this['diskRows'] = diskRows; this['diskRows'] = diskRows;
this['filesystemRows'] = this.getFilesystemRows(this.value);
const templateId = this.$route.query.templateId; const templateId = this.$route.query.templateId;
const templateVersionId = this.$route.query.versionId; const templateVersionId = this.$route.query.versionId;
@@ -634,33 +610,6 @@ export default {
</template> </template>
</NameNsDescription> </NameNsDescription>
<div v-if="isSingle">
<div class="row mb-20">
<div class="col span-12">
<Checkbox
v-model:value="customizeDisplayName"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.input.customizeDisplayName')"
:mode="mode"
/>
</div>
</div>
<div
v-if="customizeDisplayName"
class="row mb-20"
>
<div class="col span-6">
<LabeledInput
v-model:value="displayName"
:mode="mode"
:label="t('harvester.virtualMachine.input.displayName')"
:placeholder="t('harvester.virtualMachine.input.displayNamePlaceholder')"
/>
</div>
</div>
</div>
<Checkbox <Checkbox
v-if="isCreate" v-if="isCreate"
v-model:value="useTemplate" v-model:value="useTemplate"
@@ -834,23 +783,10 @@ export default {
/> />
</Tab> </Tab>
<Tab
v-if="filesystemEnabled"
name="filesystem"
:label="t('harvester.tab.filesystem')"
:weight="-9"
>
<Filesystem
v-model:value="filesystemRows"
:mode="isCreate ? mode : 'view'"
:namespace="value.metadata.namespace"
/>
</Tab>
<Tab <Tab
name="labels" name="labels"
:label="t('generic.labels')" :label="t('generic.labels')"
:weight="-10" :weight="-9"
> >
<Banner color="info"> <Banner color="info">
<t k="harvester.virtualMachine.labels.banner" /> <t k="harvester.virtualMachine.labels.banner" />
@@ -869,7 +805,7 @@ export default {
<Tab <Tab
name="instanceLabel" name="instanceLabel"
:label="t('harvester.tab.instanceLabel')" :label="t('harvester.tab.instanceLabel')"
:weight="-11" :weight="-10"
> >
<Banner color="info"> <Banner color="info">
<t k="harvester.virtualMachine.instanceLabels.banner" /> <t k="harvester.virtualMachine.instanceLabels.banner" />
@@ -890,7 +826,7 @@ export default {
<Tab <Tab
name="annotations" name="annotations"
:label="t('harvester.tab.annotations')" :label="t('harvester.tab.annotations')"
:weight="-12" :weight="-11"
> >
<Banner color="info"> <Banner color="info">
<t k="harvester.virtualMachine.annotations.banner" /> <t k="harvester.virtualMachine.annotations.banner" />
@@ -911,7 +847,7 @@ export default {
<Tab <Tab
name="advanced" name="advanced"
:label="t('harvester.tab.advanced')" :label="t('harvester.tab.advanced')"
:weight="-13" :weight="-12"
> >
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
@@ -1,419 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import InfoBox from '@shell/components/InfoBox';
import MessageLink from '@shell/components/MessageLink';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
import { Checkbox } from '@components/Form/Checkbox';
import HarvesterNodeSelector from '../components/HarvesterNodeSelector';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { set } from '@shell/utils/object';
import { NODE } from '@shell/config/types';
import { HCI } from '../types';
import { ADD_ONS } from '../config/harvester-map';
const MODE_DHCP = 'dhcp';
const MODE_STATIC = 'static';
export default {
name: 'HarvesterHostNetworkConfigEditPage',
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
InfoBox,
MessageLink,
LabeledSelect,
LabeledInput,
RadioGroup,
Checkbox,
HarvesterNodeSelector,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
hostNetworkConfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
});
},
data() {
const networkMode = this.value?.spec?.mode || MODE_DHCP;
const ips = { ...(this.value?.spec?.ips || {}) };
if (!this.value.spec) {
set(this.value, 'spec', {});
}
return {
networkMode,
ips,
hasNodeSelector: !!this.value?.spec?.nodeSelector,
};
},
computed: {
modeOptions() {
return [
{ label: 'DHCP', value: MODE_DHCP },
{ label: 'Static', value: MODE_STATIC },
];
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
return clusterNetworks.map((n) => {
const disabled = !n.isReady;
return {
label: disabled ? `${ n.id } (${ this.t('generic.notReady') })` : n.id,
value: n.id,
disabled,
};
});
},
nodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](NODE) || [];
},
isStaticMode() {
return this.networkMode === MODE_STATIC;
},
underlay: {
get() {
return !!this.value?.spec?.underlay;
},
set(val) {
set(this.value, 'spec.underlay', val);
},
},
vlanID: {
get() {
return this.value?.spec?.vlanID;
},
set(val) {
set(this.value, 'spec.vlanID', val);
},
},
clusterNetwork: {
get() {
return this.value?.spec?.clusterNetwork;
},
set(val) {
set(this.value, 'spec.clusterNetwork', val);
},
},
underlayConflict() {
const inStore = this.$store.getters['currentProduct'].inStore;
const all = this.$store.getters[`${ inStore }/all`](HCI.HOST_NETWORK_CONFIG) || [];
const currentId = this.value?.id;
return all.find((c) => c.id !== currentId && c.spec?.underlay === true) || null;
},
kubeovnEnabled() {
const inStore = this.$store.getters['currentProduct'].inStore;
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS) || [];
return addons.find((a) => a.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
},
underlayDisabled() {
return !this.kubeovnEnabled || !!this.underlayConflict;
},
kubeovnAddonTo() {
return {
name: 'c-cluster-product-resource-namespace-id',
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
resource: HCI.ADD_ONS,
namespace: 'kube-system',
id: ADD_ONS.KUBEOVN_OPERATOR,
},
query: { mode: 'edit' },
hash: '#basic',
};
},
},
watch: {
networkMode(neu) {
set(this.value, 'spec.mode', neu);
if (neu !== MODE_STATIC) {
if (this.value?.spec?.ips !== undefined) {
delete this.value.spec.ips;
}
this.ips = {};
}
},
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
methods: {
updateBeforeSave() {
set(this.value, 'spec.mode', this.networkMode);
if (this.isStaticMode) {
set(this.value, 'spec.ips', { ...this.ips });
}
},
updateIp(nodeName, val) {
this.ips = { ...this.ips, [nodeName]: val };
},
addNodeSelector() {
set(this.value.spec, 'nodeSelector', {
matchExpressions: [{
key: '', operator: 'In', values: []
}]
});
this.hasNodeSelector = true;
},
removeNodeSelector() {
delete this.value.spec.nodeSelector;
this.hasNodeSelector = false;
},
},
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:mode="mode"
:resource="value"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@cancel="done"
@error="e => errors = e"
>
<NameNsDescription
:value="value"
:mode="mode"
:namespaced="false"
description-key="spec.description"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('harvester.hostNetworkConfig.tabs.mode')"
:weight="99"
class="bordered-table"
>
<div class="row mb-20">
<div class="col span-6">
<RadioGroup
v-model:value="networkMode"
name="hostNetworkConfigMode"
:options="modeOptions"
:mode="mode"
:row="true"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="clusterNetwork"
:label="t('harvester.network.clusterNetwork.label')"
:options="clusterNetworkOptions"
:mode="mode"
required
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value.number="vlanID"
type="number"
required
:min="2"
:max="4094"
placeholder="e.g. 2 ~ 4094"
:label="t('harvester.hostNetworkConfig.vlanID.label')"
:mode="mode"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<Checkbox
v-model:value="underlay"
:label="t('harvester.hostNetworkConfig.underlay.label')"
:tooltip="t('harvester.hostNetworkConfig.underlay.tooltip')"
:mode="mode"
:disabled="underlayDisabled"
/>
<span
v-if="!kubeovnEnabled"
class="underlay-conflict-warning"
>
<i class="icon icon-warning" />
<MessageLink
:to="kubeovnAddonTo"
prefix-label="harvester.hostNetworkConfig.underlay.noKubeovn.prefix"
middle-label="harvester.hostNetworkConfig.underlay.noKubeovn.middle"
suffix-label="harvester.hostNetworkConfig.underlay.noKubeovn.suffix"
/>
</span>
<span
v-else-if="underlayConflict"
class="underlay-conflict-warning"
>
<i class="icon icon-warning" />
{{ t('harvester.hostNetworkConfig.underlay.conflict', { name: underlayConflict.nameDisplay || underlayConflict.id }) }}
</span>
</div>
</div>
<template v-if="isStaticMode">
<hr class="section-divider" />
<div
v-for="node in nodes"
:key="node.id"
class="row mb-10 ips-row"
>
<div class="col span-3">
<LabeledInput
:value="node.nameDisplay || node.id"
:label="t('harvester.hostNetworkConfig.ips.nodeLabel')"
mode="view"
:disabled="true"
/>
</div>
<div class="col span-5">
<LabeledInput
:value="ips[node.id]"
:label="t('harvester.hostNetworkConfig.ips.label')"
:placeholder="t('harvester.hostNetworkConfig.ips.placeholder')"
:mode="mode"
required
@update:value="updateIp(node.id, $event)"
/>
</div>
</div>
</template>
</Tab>
<Tab
name="nodeSelector"
:label="t('harvester.hostNetworkConfig.tabs.nodeSelector')"
:weight="98"
>
<template v-if="hasNodeSelector">
<InfoBox class="node-selector-box">
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
:aria-label="t('generic.remove')"
@click="removeNodeSelector"
>
<i class="icon icon-x" />
</button>
<HarvesterNodeSelector
class="mt-20"
:value="value.spec.nodeSelector"
:mode="mode"
/>
</InfoBox>
</template>
<template v-else>
<button
type="button"
class="btn role-secondary"
:disabled="isView"
@click="addNodeSelector"
>
{{ t('harvester.hostNetworkConfig.nodeSelector.addButton') }}
</button>
</template>
</Tab>
</ResourceTabs>
</CruResource>
</template>
<style lang="scss" scoped>
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 10px 0 20px;
}
.node-selector-box {
position: relative;
.remove {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
padding: 0;
}
}
.underlay-conflict-warning {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 4px;
color: var(--warning);
font-size: 13px;
}
</style>
@@ -31,10 +31,6 @@ export default {
to() { to() {
return this.vm?.detailLocation; return this.vm?.detailLocation;
}, },
attachVMName() {
return this.vm?.nameDisplay || this.vm?.metadata?.name || this.value;
}
} }
}; };
</script> </script>
@@ -44,10 +40,10 @@ export default {
v-if="to" v-if="to"
:to="to" :to="to"
> >
{{ attachVMName }} {{ value }}
</router-link> </router-link>
<span v-else> <span v-else>
{{ attachVMName }} {{ value }}
</span> </span>
</template> </template>
@@ -1,7 +1,6 @@
<script> <script>
import { NORMAN } from '@shell/config/types'; import { NORMAN } from '@shell/config/types';
import { HCI } from '../types'; import { HCI } from '../types';
import { getHarvesterUser } from '../utils/auth';
export default { export default {
props: { props: {
@@ -26,7 +25,7 @@ export default {
}, },
data() { data() {
const user = getHarvesterUser(this.$store.getters); const user = this.$store.getters['auth/v3User'];
return { return {
harvesterSettings: [], harvesterSettings: [],
@@ -1,23 +0,0 @@
<script>
export default {
name: 'HarvesterBooleanFormatter',
props: {
value: {
type: Boolean,
default: false,
},
},
};
</script>
<template>
<span v-if="value">
<i class="icon icon-checkmark" />
</span>
<span
v-else
class="text-muted"
>
&mdash;
</span>
</template>
@@ -1,29 +0,0 @@
<script>
export default {
name: 'HarvesterHostNetworkConfigModeFormatter',
props: {
value: {
type: String,
default: '',
},
},
computed: {
displayMode() {
if (this.value?.toLowerCase() === 'dhcp') {
return 'DHCP';
}
if (this.value?.toLowerCase() === 'static') {
return 'Static';
}
return this.value;
},
},
};
</script>
<template>
<span>{{ displayMode }}</span>
</template>
@@ -20,7 +20,12 @@ export default {
computed: { computed: {
vmiResource() { vmiResource() {
return this.$store.getters['harvester/byId'](HCI.VMI, this.vmResource?.id) || null; const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || [];
const vmi = vmiList.find( (VMI) => {
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.vmResource?.metadata?.uid;
});
return vmi;
}, },
migrationState() { migrationState() {
return this.vmiResource?.migrationState?.status || ''; return this.vmiResource?.migrationState?.status || '';
@@ -14,6 +14,20 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
allNodeNetwork: {
type: Array,
default: () => {
return [];
}
},
allClusterNetwork: {
type: Array,
default: () => {
return [];
}
}
}, },
data() { data() {
+22 -86
View File
@@ -144,10 +144,6 @@ harvester:
migration: migration:
failedMessage: Latest migration failed! failedMessage: Latest migration failed!
title: Migration title: Migration
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
unknownNode: (unknown node)
alreadyOnTarget: Already on Target
fields: fields:
nodeName: nodeName:
label: Target Node label: Target Node
@@ -252,7 +248,6 @@ harvester:
suspendSchedule: Suspend suspendSchedule: Suspend
restoreExistingVM: Replace Existing restoreExistingVM: Replace Existing
migrate: Migrate migrate: Migrate
vmMigrate: Virtual Machine Migration
cpuAndMemoryHotplug: Edit CPU and Memory cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration abortMigration: Abort Migration
storageMigration: Storage Migration storageMigration: Storage Migration
@@ -295,12 +290,6 @@ harvester:
harvesterIpAddress: harvesterIpAddress:
customIpTooltip: "Custom IP (set via annotation)" customIpTooltip: "Custom IP (set via annotation)"
tableHeaders: tableHeaders:
hostNetworkConfig:
underlay: Underlay
underlayTooltip: Allow this interface to act as the underlay for VM overlay networks.
vlanID: VLAN ID
mode: Mode
clusterNetwork: Cluster Network
imageEncryption: Encryption imageEncryption: Encryption
size: Size size: Size
virtualSize: Virtual Size virtualSize: Virtual Size
@@ -309,7 +298,6 @@ harvester:
phase: Phase phase: Phase
attachedVM: Attached Virtual Machine attachedVM: Attached Virtual Machine
cpuManager: CPU Manager cpuManager: CPU Manager
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
fingerprint: Fingerprint fingerprint: Fingerprint
value: Value value: Value
actions: Actions actions: Actions
@@ -361,7 +349,6 @@ harvester:
snapshots: Snapshots snapshots: Snapshots
instanceLabel: Instance Labels instanceLabel: Instance Labels
annotations: Annotations annotations: Annotations
filesystem: Filesystem Volume
fields: fields:
version: Version version: Version
name: Name name: Name
@@ -389,15 +376,26 @@ harvester:
available: Available Devices available: Available Devices
compatibleNodes: Compatible Nodes compatibleNodes: Compatible Nodes
impossibleSelection: 'There are no hosts with all of the selected devices.' impossibleSelection: 'There are no hosts with all of the selected devices.'
howToUseDeviceInVMCreation: howToUseDevice:
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' 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 middle: vGPU Devices
suffix: page first. 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.' deviceInTheSameHost: 'You can only select devices on the same host.'
oldFormatDevices:
help: |-
<p>
The following PCI devices are using the old naming convention and need to be updated in the YAML file:
</p>
<ul>
{oldFormatDevicesHTML}
</ul>
<p>
Please use the following instructions to update the virtual machine:
</p>
<ol>
<li>Stop the virtual machine, edit the virtual machine YAML, and remove the <Code>hostDevices</Code> section, and save virtual machine the changes to the YAML file.</li>
<li>Edit the virtual machine, and add the already enabled PCI Device from the list of available PCIDevices, and save and start VM.</li>
</ol>
showCompatibility: Show device compatibility matrix showCompatibility: Show device compatibility matrix
hideCompatibility: Hide device compatibility matrix hideCompatibility: Hide device compatibility matrix
claimError: Error enabling passthrough on {name} claimError: Error enabling passthrough on {name}
@@ -415,8 +413,6 @@ harvester:
suffix: to enable the add-on to successfully manage your PCI devices. suffix: to enable the add-on to successfully manage your PCI devices.
noPCIPermission: Please contact your system administrator to enable the PCI devices first. noPCIPermission: Please contact your system administrator to enable the PCI devices first.
enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure. enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure.
disableResourcePooling: Disable Resource Pooling
disableResourcePoolingDescription: Assigns this device a unique resource name so it can be pinned to a specific VM, instead of sharing the pool with other identical devices.
devices: devices:
matrixHostName: Host Name matrixHostName: Host Name
@@ -726,7 +722,6 @@ harvester:
other {Start} other {Start}
} Now } Now
createSSHKey: Create a New... createSSHKey: Create a New...
genericLoginError: Authentication failed. Please re-log in and try again.
installAgent: Install guest agent installAgent: Install guest agent
enableUsb: Enable USB Tablet enableUsb: Enable USB Tablet
advancedOptions: advancedOptions:
@@ -824,18 +819,6 @@ harvester:
username: Username username: Username
password: Password password: Password
reservedMemory: Reserved Memory reservedMemory: Reserved Memory
customizeDisplayName: Customize virtual machine display name
displayNamePlaceholder: Virtual machine alias name
displayName: Display Name
filesystem:
description: Harvester supports filesystem volumes for VM via virtiofs.
type: Filesystem Type
volume: Volume
resource: Resource
add: Add
mountBannerHint: "Please update the mount path (e.g. /mnt/appconfigfs) to your preferred location, then add the corresponding commands to the"
mountBannerHintLink: "runcmd"
mountBannerHintSuffix: "in Advanced tab User Data."
machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?' machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?'
detail: detail:
tabs: tabs:
@@ -1124,7 +1107,6 @@ harvester:
replaceExisting: Replace existing replaceExisting: Replace existing
virtualMachineName: Virtual Machine Name virtualMachineName: Virtual Machine Name
keepMacAddress: Keep MAC Address keepMacAddress: Keep MAC Address
haltAfterRestore: Keep powered off after restore
matchTarget: The current backup target does not match the existing one. matchTarget: The current backup target does not match the existing one.
progress: progress:
details: Volume details details: Volume details
@@ -1162,9 +1144,6 @@ harvester:
placeholder: key1=value1, key2=value2 placeholder: key1=value1, key2=value2
dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a> dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a>
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet. tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
externalConnectivity:
label: NAT Outgoing
tooltip: Enable NAT for VMs using this subnet to access external networks.
private: private:
label: Private Subnet label: Private Subnet
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC. tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
@@ -1302,13 +1281,6 @@ harvester:
rancherCluster: rancherCluster:
kubeConfig: Rancher KubeConfig kubeConfig: Rancher KubeConfig
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
clusterPodSecurityStandard:
whitelistedNamespaces:
label: 'Whitelisted Namespaces'
privilegedNamespaces:
label: 'Privileged Namespaces'
restrictedNamespaces:
label: 'Restricted Namespaces'
storageNetwork: storageNetwork:
range: range:
placeholder: e.g. 172.16.0.0/24 placeholder: e.g. 172.16.0.0/24
@@ -1316,7 +1288,6 @@ harvester:
invalid: '"Range" is invalid.' invalid: '"Range" is invalid.'
clusterNetwork: Cluster Network clusterNetwork: Cluster Network
vlan: VLAN ID vlan: VLAN ID
exclusiveVlan: Exclusive VLAN
exclude: exclude:
label: Exclude IPs label: Exclude IPs
placeholder: CIDR format, e.g. 172.16.0.10/32 placeholder: CIDR format, e.g. 172.16.0.10/32
@@ -1533,7 +1504,6 @@ harvester:
vmSnapshot: vmSnapshot:
label: Virtual Machine Snapshots label: Virtual Machine Snapshots
createText: Restore Snapshot createText: Restore Snapshot
title: Restore Virtual Machine
snapshot: Snapshot snapshot: Snapshot
storage: storage:
@@ -1541,7 +1511,6 @@ harvester:
useDefault: Use the default storage useDefault: Use the default storage
volumeEncryption: Volume Encryption volumeEncryption: Volume Encryption
secret: Secret secret: Secret
volumeExpansionCheckbox: Enable Expansion
migratable: migratable:
label: Migratable label: Migratable
numberOfReplicas: numberOfReplicas:
@@ -1791,8 +1760,6 @@ harvester:
repository: Image Repository repository: Image Repository
driver: driver:
location: Driver Location location: Driver Location
enable:
title: Enable NVIDIA Driver Toolkit
parsingSpecError: parsingSpecError:
The field 'spec.valuesContent' has invalid format. The field 'spec.valuesContent' has invalid format.
usbController: usbController:
@@ -1877,32 +1844,6 @@ harvester:
addLabel: Add CIDR addLabel: Add CIDR
range: range:
addLabel: Add Range addLabel: Add Range
hostNetworkConfig:
label: Host Networks
mode:
label: Mode
tabs:
mode: Mode
nodeSelector: Node Selector
nodeSelector:
addButton: Add Node Selector
underlay:
label: Underlay
tooltip: Allow this interface to act as the underlay for VM overlay networks.
conflict: '`{name}` host network config already has underlay enabled. Only one underlay is allowed in the cluster.'
noKubeovn:
prefix: The kubeovn-operator add-on is not enabled. Click
middle: here
suffix: to enable the add-on for overlay networking.
vlanID:
label: VLAN ID
ipRange:
label: IP Range ({node})
placeholder: e.g. 192.168.1.10/24
ips:
nodeLabel: Node
label: IP
placeholder: 'e.g. 192.168.1.10/24'
service: service:
healthCheckPort: healthCheckPort:
@@ -1948,8 +1889,6 @@ harvester:
migconfiguration: migconfiguration:
label: vGPU MIG Configurations label: vGPU MIG Configurations
status:
outOfSync: Out of Sync
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration. infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
profileSpec: Profile Specs profileSpec: Profile Specs
profileStatus: Profile Status profileStatus: Profile Status
@@ -2012,7 +1951,6 @@ harvester:
title: Cannot Disable Passthrough title: Cannot Disable Passthrough
message: Please detach the device from the VM and save it first before disabling passthrough. message: Please detach the device from the VM and save it first before disabling passthrough.
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/>&nbsp1) Re-plugging the USB device.<br/>&nbsp2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.' enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/>&nbsp1) Re-plugging the USB device.<br/>&nbsp2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
classType: Class Type
harvesterVlanConfigMigrateDialog: harvesterVlanConfigMigrateDialog:
targetClusterNetwork: targetClusterNetwork:
@@ -2097,7 +2035,6 @@ advancedSettings:
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.' 'harv-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-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.' 'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
typeLabel: typeLabel:
kubevirt.io.virtualmachine: |- kubevirt.io.virtualmachine: |-
@@ -2220,17 +2157,16 @@ typeLabel:
one { PCI Device } one { PCI Device }
other { PCI Devices } other { PCI Devices }
} }
persistentvolumeclaim: |-
{count, plural,
one { Volume }
other { Volumes }
}
network.harvesterhci.io.clusternetwork: |- network.harvesterhci.io.clusternetwork: |-
{count, plural, {count, plural,
one { Cluster Network } one { Cluster Network }
other { Cluster Networks } other { Cluster Networks }
} }
network.harvesterhci.io.hostnetworkconfig: |-
{count, plural,
one { Host Network }
other { Host Networks }
}
harvesterhci.io.addon: |- harvesterhci.io.addon: |-
{count, plural, {count, plural,
one { Add-on } one { Add-on }
@@ -60,17 +60,6 @@ export default {
return schema; 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() { rows() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE); const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
@@ -96,23 +85,11 @@ export default {
{{ t('harvester.pci.noPCIPermission') }} {{ t('harvester.pci.noPCIPermission') }}
</Banner> </Banner>
</div> </div>
<div v-else-if="hasSchema && enabledPCI"> <DeviceList
<Banner v-else-if="hasSchema && enabledPCI"
v-if="vGPUAsPCIDeviceEnabled" :devices="rows"
color="info" :schema="schema"
> />
<MessageLink
:to="toVGpuDevicesPage"
prefix-label="harvester.pci.howToUseDevice.prefix"
middle-label="harvester.pci.howToUseDevice.middle"
suffix-label="harvester.pci.howToUseDevice.suffix"
/>
</Banner>
<DeviceList
:devices="rows"
:schema="schema"
/>
</div>
<div v-else> <div v-else>
<Banner color="warning"> <Banner color="warning">
<MessageLink <MessageLink
@@ -3,6 +3,7 @@ import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable'; import ResourceTable from '@shell/components/ResourceTable';
import BadgeState from '@shell/components/formatter/BadgeStateFormatter'; import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers'; import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types'; import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
@@ -112,7 +113,6 @@ export default {
value: 'connectivity', value: 'connectivity',
labelKey: 'tableHeaders.routeConnectivity', labelKey: 'tableHeaders.routeConnectivity',
formatter: 'NetworkRouteConnectivity', formatter: 'NetworkRouteConnectivity',
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
formatterOpts: { arbitrary: true }, formatterOpts: { arbitrary: true },
width: 130, width: 130,
}, },
@@ -106,8 +106,8 @@ export default {
name: 'AttachedVM', name: 'AttachedVM',
labelKey: 'tableHeaders.attachedVM', labelKey: 'tableHeaders.attachedVM',
type: 'attached', type: 'attached',
value: 'attachVMName', value: 'spec.claimRef',
sort: 'attachVMName', sort: 'name',
}, },
{ {
name: 'VolumeSnapshotCounts', name: 'VolumeSnapshotCounts',
@@ -134,8 +134,8 @@ export default {
return row?.attachVM?.detailLocation; return row?.attachVM?.detailLocation;
}, },
getAttachedVMName(row) { getVMName(row) {
return row.attachVMName || ''; return row.attachVM?.metadata?.name || '';
}, },
isInternalStorageClass(storageClassName) { isInternalStorageClass(storageClassName) {
@@ -173,10 +173,10 @@ export default {
<template #cell:AttachedVM="{row}"> <template #cell:AttachedVM="{row}">
<div> <div>
<router-link <router-link
v-if="getAttachedVMName(row)" v-if="getVMName(row)"
:to="goTo(row)" :to="goTo(row)"
> >
{{ getAttachedVMName(row) }} {{ getVMName(row) }}
</router-link> </router-link>
</div> </div>
</template> </template>
@@ -1,5 +1,4 @@
<script> <script>
import { mapGetters } from 'vuex';
import ResourceTable from '@shell/components/ResourceTable'; import ResourceTable from '@shell/components/ResourceTable';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import { import {
@@ -13,18 +12,11 @@ import { HCI } from '../types';
import HarvesterVmState from '../formatters/HarvesterVmState'; import HarvesterVmState from '../formatters/HarvesterVmState';
import ConsoleBar from '../components/VMConsoleBar'; import ConsoleBar from '../components/VMConsoleBar';
const ENCRYPTED_VOLUME_TOOLTIP_KEYS = {
all: 'harvester.virtualMachine.volume.lockTooltip.all',
partial: 'harvester.virtualMachine.volume.lockTooltip.partial',
};
export const VM_HEADERS = [ export const VM_HEADERS = [
STATE, STATE,
{ {
...NAME, ...NAME,
width: 350, width: 350,
value: 'nameDisplay',
sort: ['nameDisplay'],
}, },
NAMESPACE, NAMESPACE,
{ {
@@ -101,9 +93,19 @@ export default {
this.hasNode = true; this.hasNode = true;
} }
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK)) {
_hash.nodeNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NODE_NETWORK });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.CLUSTER_NETWORK)) {
_hash.clusterNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK });
}
const hash = await allHash(_hash); const hash = await allHash(_hash);
this.allVMs = hash.vms; this.allVMs = hash.vms;
this.allNodeNetworks = hash.nodeNetworks || [];
this.allClusterNetworks = hash.clusterNetworks || [];
}, },
data() { data() {
@@ -111,14 +113,14 @@ export default {
hasNode: false, hasNode: false,
allVMs: [], allVMs: [],
allVMIs: [], allVMIs: [],
allNodeNetworks: [],
allClusterNetworks: [],
restartNotificationDisplayed: false, restartNotificationDisplayed: false,
HCI HCI
}; };
}, },
computed: { computed: {
...mapGetters({ actionCb: 'action-menu/performCallbackData' }),
headers() { headers() {
const restoreCol = { const restoreCol = {
name: 'restoreProgress', name: 'restoreProgress',
@@ -161,12 +163,6 @@ export default {
*/ */
hasBackUpRestoreInProgress() { hasBackUpRestoreInProgress() {
return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete); return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
},
vmRestartRequiredNames() {
return this.allVMs
.filter((vm) => vm.isRestartRequired)
.map((vm) => vm.metadata.name);
} }
}, },
@@ -185,42 +181,53 @@ export default {
}, },
watch: { watch: {
actionCb(neu) { allVMs: {
if (neu?.clearTableSelection) { handler(neu) {
this.$refs.resourceTable.clearSelection(); const vmNames = [];
this.$store.dispatch('action-menu/clearCallbackData');
}
},
vmRestartRequiredNames(vmNames) { neu.forEach((vm) => {
const count = vmNames.length; if (vm.isRestartRequired) {
vmNames.push(vm.metadata.name);
}
});
const count = vmNames.length;
if (count === 0 && this.restartNotificationDisplayed) { if ( count === 0 && this.restartNotificationDisplayed) {
this.restartNotificationDisplayed = false; this.restartNotificationDisplayed = false;
return; return;
}
if (count > 0) {
// clear old notification before showing new one
if (this.restartNotificationDisplayed) {
this.$store.dispatch('growl/clear');
} }
this.$store.dispatch('growl/warning', { if (count > 0) {
title: this.t('harvester.notification.restartRequired.title', { count }), // clear old notification before showing new one
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }), if (this.restartNotificationDisplayed) {
timeout: 10000, this.$store.dispatch('growl/clear');
}, { root: true }); }
this.restartNotificationDisplayed = true; }
}
if (count > 0 && vmNames.length > 0) {
this.$store.dispatch('growl/warning', {
title: this.t('harvester.notification.restartRequired.title', { count }),
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
timeout: 10000,
}, { root: true });
this.restartNotificationDisplayed = true;
}
},
deep: true,
} }
}, },
methods: { methods: {
lockIconTooltipMessage(row) { lockIconTooltipMessage(row) {
const key = ENCRYPTED_VOLUME_TOOLTIP_KEYS[row.encryptedVolumeType]; const message = '';
return key ? this.t(key) : ''; if (row.encryptedVolumeType === 'all') {
return this.t('harvester.virtualMachine.volume.lockTooltip.all');
} else if (row.encryptedVolumeType === 'partial') {
return this.t('harvester.virtualMachine.volume.lockTooltip.partial');
}
return message;
} }
} }
}; };
@@ -230,7 +237,6 @@ export default {
<Loading v-if="$fetchState.pending" /> <Loading v-if="$fetchState.pending" />
<div v-else> <div v-else>
<ResourceTable <ResourceTable
ref="resourceTable"
v-bind="$attrs" v-bind="$attrs"
:headers="headers" :headers="headers"
default-sort-by="age" default-sort-by="age"
@@ -247,6 +253,8 @@ export default {
<HarvesterVmState <HarvesterVmState
class="vmstate" class="vmstate"
:row="scope.row" :row="scope.row"
:all-node-network="allNodeNetworks"
:all-cluster-network="allClusterNetworks"
/> />
</div> </div>
</template> </template>
@@ -257,16 +265,16 @@ export default {
v-if="scope.row.type !== HCI.VMI" v-if="scope.row.type !== HCI.VMI"
:to="scope.row.detailLocation" :to="scope.row.detailLocation"
> >
{{ scope.row.nameDisplay }} {{ scope.row.metadata.name }}
<i <i
v-if="scope.row.encryptedVolumeType !== 'none'" v-if="lockIconTooltipMessage(scope.row)"
v-tooltip="lockIconTooltipMessage(scope.row)" v-tooltip="lockIconTooltipMessage(scope.row)"
class="icon icon-lock" class="icon icon-lock"
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}" :class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
/> />
</router-link> </router-link>
<span v-else> <span v-else>
{{ scope.row.nameDisplay }} {{ scope.row.metadata.name }}
</span> </span>
<ConsoleBar <ConsoleBar
:resource-type="scope.row" :resource-type="scope.row"
@@ -1,86 +0,0 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers';
import { HCI } from '../types';
const UNDERLAY = {
name: 'underlay',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.underlay',
tooltip: 'harvester.tableHeaders.hostNetworkConfig.underlayTooltip',
value: 'spec.underlay',
sort: 'spec.underlay',
formatter: 'HarvesterBoolean',
};
const VLAN_ID = {
name: 'vlanID',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.vlanID',
value: 'spec.vlanID',
sort: 'spec.vlanID',
};
const MODE = {
name: 'mode',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.mode',
value: 'spec.mode',
sort: 'spec.mode',
formatter: 'HarvesterHostNetworkConfigMode',
};
const CLUSTER_NETWORK = {
name: 'clusterNetwork',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.clusterNetwork',
value: 'spec.clusterNetwork',
sort: 'spec.clusterNetwork',
align: 'center',
};
export default {
name: 'HarvesterListHostNetworkConfig',
components: { ResourceTable, Loading },
inheritAttrs: false,
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG });
},
data() {
return { rows: [] };
},
computed: {
schema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/schemaFor`](HCI.HOST_NETWORK_CONFIG);
},
headers() {
return [
STATE,
NAME_COL,
UNDERLAY,
VLAN_ID,
MODE,
CLUSTER_NETWORK,
AGE,
];
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:headers="headers"
:rows="rows"
:schema="schema"
key-field="_key"
/>
</template>
+6 -92
View File
@@ -12,7 +12,7 @@ import { base64Decode } from '@shell/utils/crypto';
import { formatSi, parseSi } from '@shell/utils/units'; import { formatSi, parseSi } from '@shell/utils/units';
import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params'; import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params';
import { import {
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, SERVICE_ACCOUNT, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
} from '@shell/config/types'; } from '@shell/config/types';
import { HOSTNAME } from '@shell/config/labels-annotations'; import { HOSTNAME } from '@shell/config/labels-annotations';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
@@ -25,7 +25,7 @@ import { HCI } from '../../types';
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm'; import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
import impl, { QGA_JSON, USB_TABLET } from './impl'; import impl, { QGA_JSON, USB_TABLET } from './impl';
import { GIBIBYTE } from '../../utils/unit'; import { GIBIBYTE } from '../../utils/unit';
import { VOLUME_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types'; import { VOLUME_MODE } from '@pkg/harvester/config/types';
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine'; const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
@@ -102,8 +102,6 @@ export default {
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }), vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }), vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }), secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
configMaps: this.$store.dispatch(`${ inStore }/findAll`, { type: CONFIG_MAP }),
serviceAccounts: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE_ACCOUNT }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }), addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }), longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
}; };
@@ -158,7 +156,6 @@ export default {
imageId: '', imageId: '',
diskRows: [], diskRows: [],
networkRows: [], networkRows: [],
filesystemRows: [],
machineType: '', machineType: '',
machineTypes: [], machineTypes: [],
secretName: '', secretName: '',
@@ -443,7 +440,6 @@ export default {
this['imageId'] = imageId; this['imageId'] = imageId;
this['diskRows'] = diskRows; this['diskRows'] = diskRows;
this['filesystemRows'] = this.getFilesystemRows(vm);
this.refreshYamlEditor(); this.refreshYamlEditor();
}, },
@@ -645,80 +641,6 @@ export default {
this.parseAccessCredentials(); this.parseAccessCredentials();
this.parseNetworkRows(this.networkRows); this.parseNetworkRows(this.networkRows);
this.parseDiskRows(this.diskRows); this.parseDiskRows(this.diskRows);
this.parseFilesystemRows();
},
getFilesystemRows(vm) {
const _filesystems = vm.spec.template.spec.domain.devices?.filesystems || [];
const _volumes = vm.spec.template.spec.volumes || [];
return _filesystems.map((fs) => {
const volume = _volumes.find((v) => v.name === fs.name);
let fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
let resourceName = '';
if (volume?.configMap) {
fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
resourceName = volume.configMap.name;
} else if (volume?.secret) {
fsType = FILESYSTEM_SOURCE_TYPE.SECRET;
resourceName = volume.secret.secretName;
} else if (volume?.serviceAccount) {
fsType = FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT;
resourceName = volume.serviceAccount.serviceAccountName;
}
return {
fsType,
volumeName: fs.name,
resourceName,
};
});
},
parseFilesystemRows() {
const completedRows = this.filesystemRows.filter(
(r) => r.fsType && r.volumeName && r.resourceName
);
const filesystems = completedRows.map((r) => ({
name: r.volumeName,
virtiofs: {},
}));
const fsVolumes = completedRows.map((r) => {
if (r.fsType === FILESYSTEM_SOURCE_TYPE.CONFIGMAP) {
return {
name: r.volumeName,
configMap: { name: r.resourceName },
};
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SECRET) {
return {
name: r.volumeName,
secret: { secretName: r.resourceName },
};
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT) {
return {
name: r.volumeName,
serviceAccount: { serviceAccountName: r.resourceName },
};
}
return null;
}).filter(Boolean);
if (filesystems.length > 0) {
this.spec.template.spec.domain.devices['filesystems'] = filesystems;
} else {
delete this.spec.template.spec.domain.devices['filesystems'];
}
if (fsVolumes.length > 0) {
if (!this.spec.template.spec.volumes) {
this.spec.template.spec['volumes'] = [];
}
this.spec.template.spec.volumes.push(...fsVolumes);
}
}, },
parseOther() { parseOther() {
@@ -985,22 +907,14 @@ export default {
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces; const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces); const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
const devices = {
...this.spec.template.spec.domain.devices,
interfaces: mergedInterfaces,
};
if (this.isEdit && networkRow.length === 0) {
devices.autoattachPodInterface = false;
} else {
delete devices.autoattachPodInterface;
}
const spec = { const spec = {
...this.spec.template.spec, ...this.spec.template.spec,
domain: { domain: {
...this.spec.template.spec.domain, ...this.spec.template.spec.domain,
devices, devices: {
...this.spec.template.spec.domain.devices,
interfaces: mergedInterfaces,
},
}, },
networks networks
}; };
@@ -9,7 +9,6 @@ import HarvesterResource from './harvester';
export default class MIGCONFIGURATION extends HarvesterResource { export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() { get _availableActions() {
let out = super._availableActions; let out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out = out.map((action) => { out = out.map((action) => {
if (action.action === 'showConfiguration') { if (action.action === 'showConfiguration') {
@@ -27,13 +26,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
out.push( out.push(
{ {
action: 'enableConfig', action: 'enableConfig',
enabled: !this.isEnabled && canUpdate, enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable', label: 'Enable',
}, },
{ {
action: 'disableConfig', action: 'disableConfig',
enabled: this.isEnabled && canUpdate, enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable', label: 'Disable',
}, },
@@ -63,27 +62,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
} }
get stateDisplay() { get stateDisplay() {
if (this.configStatus === 'out-of-sync') {
return this.t('harvester.migconfiguration.status.outOfSync');
}
return this.actualState; return this.actualState;
} }
get stateDescription() {
if (this.status?.message) {
return this.status.message;
}
return super.stateDescription;
}
get stateColor() { get stateColor() {
if (this.configStatus === 'out-of-sync') { const state = this.actualState;
return 'text-warning';
}
return colorForState(this.actualState); return colorForState(state);
} }
get isEnabled() { get isEnabled() {
@@ -2,7 +2,6 @@ import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string'; import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types'; import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { getHarvesterUserName } from '../utils/auth';
const STATUS_DISPLAY = { const STATUS_DISPLAY = {
enabled: { enabled: {
@@ -34,7 +33,7 @@ export default class PCIDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enablePassthroughBulk', action: 'enablePassthroughBulk',
enabled: !this.isEnabling && !this.isvGPUDevice && this.canUpdate, enabled: !this.isEnabling && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough', label: 'Enable Passthrough',
bulkable: true, bulkable: true,
@@ -43,7 +42,7 @@ export default class PCIDevice extends SteveModel {
}, },
{ {
action: 'disablePassthrough', action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && this.canUpdate, enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough', label: 'Disable Passthrough',
bulkable: true, bulkable: true,
@@ -54,10 +53,6 @@ export default class PCIDevice extends SteveModel {
return out; return out;
} }
get canUpdate() {
return !!this.linkFor('update');
}
get isvGPUDevice() { get isvGPUDevice() {
if (!this.vGPUAsPCIDeviceFeatureEnabled) { if (!this.vGPUAsPCIDeviceFeatureEnabled) {
return false; return false;
@@ -101,8 +96,15 @@ export default class PCIDevice extends SteveModel {
if (!this.passthroughClaim) { if (!this.passthroughClaim) {
return false; return false;
} }
const isSingleProduct = this.$rootGetters['isSingleProduct'];
let userName = 'admin';
const userName = getHarvesterUserName(this.$rootGetters); // 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;
}
return this.claimedBy === userName; return this.claimedBy === userName;
} }
@@ -16,13 +16,13 @@ export default class SRIOVDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enableDevice', action: 'enableDevice',
enabled: !this.isEnabled && this.canUpdate, enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable', label: 'Enable',
}, },
{ {
action: 'disableDevice', action: 'disableDevice',
enabled: this.isEnabled && this.canUpdate, enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable', label: 'Disable',
}, },
@@ -31,10 +31,6 @@ export default class SRIOVDevice extends SteveModel {
return out; return out;
} }
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() { get canYaml() {
return false; return false;
} }
@@ -1,7 +1,6 @@
import SteveModel from '@shell/plugins/steve/steve-class'; import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string'; import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types'; import { HCI } from '../types';
import { getHarvesterUserName } from '../utils/auth';
const STATUS_DISPLAY = { const STATUS_DISPLAY = {
enabled: { enabled: {
@@ -33,7 +32,7 @@ export default class USBDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enablePassthroughBulk', action: 'enablePassthroughBulk',
enabled: !this.passthroughClaim && !this.status.enabled && this.canUpdate, enabled: !this.passthroughClaim && !this.status.enabled,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough', label: 'Enable Passthrough',
bulkable: true, bulkable: true,
@@ -42,7 +41,7 @@ export default class USBDevice extends SteveModel {
}, },
{ {
action: 'disablePassthrough', action: 'disablePassthrough',
enabled: this.status.enabled && this.canUpdate, enabled: this.status.enabled,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough', label: 'Disable Passthrough',
bulkable: true, bulkable: true,
@@ -53,10 +52,6 @@ export default class USBDevice extends SteveModel {
return out; return out;
} }
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() { get canYaml() {
return false; return false;
} }
@@ -92,8 +87,15 @@ export default class USBDevice extends SteveModel {
if (!this.passthroughClaim) { if (!this.passthroughClaim) {
return false; return false;
} }
const isSingleProduct = this.$rootGetters['isSingleProduct'];
let userName = 'admin';
const userName = getHarvesterUserName(this.$rootGetters); // 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;
}
return this.claimedBy === userName; return this.claimedBy === userName;
} }
@@ -27,18 +27,17 @@ const STATUS_DISPLAY = {
export default class VGpuDevice extends SteveModel { export default class VGpuDevice extends SteveModel {
get _availableActions() { get _availableActions() {
const out = super._availableActions; const out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out.push( out.push(
{ {
action: 'enableVGpu', action: 'enableVGpu',
enabled: !this.isEnabled && canUpdate, enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable', label: 'Enable',
}, },
{ {
action: 'disableVGpu', action: 'disableVGpu',
enabled: this.isEnabled && canUpdate, enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable', label: 'Disable',
bulkable: true, bulkable: true,
+1 -11
View File
@@ -24,7 +24,7 @@ const OBSCURE_NAMESPACE_PREFIX = [
export default class HciNamespace extends namespace { export default class HciNamespace extends namespace {
get _availableActions() { get _availableActions() {
let out = super._availableActions; const out = super._availableActions;
const remove = out.findIndex((a) => a.action === 'promptRemove'); const remove = out.findIndex((a) => a.action === 'promptRemove');
const promptRemove = { const promptRemove = {
@@ -53,16 +53,6 @@ export default class HciNamespace extends namespace {
insertAt(out, out.length - 1, promptRemove); insertAt(out, out.length - 1, promptRemove);
insertAt(out, out.length - 5, editQuotaAction); insertAt(out, out.length - 5, editQuotaAction);
const canUpdate = !!this.linkFor('update');
out = out.map((action) => {
if (['move'].includes(action.action)) {
return { ...action, enabled: action.enabled && canUpdate };
}
return action;
});
return out; return out;
} }
@@ -235,10 +235,6 @@ export default class HciPv extends HarvesterResource {
return allVMs.find(findAttachVM); return allVMs.find(findAttachVM);
} }
get attachVMName() {
return this.attachVM?.nameDisplay || this.attachVM?.metadata?.name || '';
}
get isAvailable() { get isAvailable() {
const unAvailable = ['Resizing', 'Not Ready']; const unAvailable = ['Resizing', 'Not Ready'];
-14
View File
@@ -5,20 +5,6 @@ import Secret from '@shell/models/secret';
import { NAMESPACE } from '@shell/config/types'; import { NAMESPACE } from '@shell/config/types';
export default class HciSecret extends Secret { export default class HciSecret extends Secret {
get _availableActions() {
let out = super._availableActions;
out = out.map((action) => {
if (['download'].includes(action.action)) {
return { ...action, enabled: !!this.linkFor('update') };
}
return action;
});
return out;
}
// prevent harvester secret detail page be overridden. // prevent harvester secret detail page be overridden.
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue // See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
get fullDetailPageOverride() { get fullDetailPageOverride() {
@@ -96,10 +96,6 @@ export default class HciStorageClass extends StorageClass {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption'); return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
} }
get expandOnlineEncryptedVolumeFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('expandOnlineEncryptedVolume');
}
get thirdPartyStorageFeatureEnabled() { get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage'); return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
} }
@@ -110,15 +106,6 @@ export default class HciStorageClass extends StorageClass {
get availableActions() { get availableActions() {
let out = super.availableActions || []; let out = super.availableActions || [];
const canUpdate = !!this.linkFor('update');
out = out.map((action) => {
if (['setDefault', 'setAsDefault', 'resetDefault'].includes(action.action)) {
return { ...action, enabled: canUpdate };
}
return action;
});
if (this.isInternalStorageClass()) { if (this.isInternalStorageClass()) {
out = out.filter((action) => { out = out.filter((action) => {
+1 -13
View File
@@ -4,8 +4,6 @@ import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
import HarvesterResource from './harvester'; import HarvesterResource from './harvester';
import { HCI } from '../types'; import { HCI } from '../types';
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
export default class HciAddonConfig extends HarvesterResource { export default class HciAddonConfig extends HarvesterResource {
get availableActions() { get availableActions() {
const out = super._availableActions; const out = super._availableActions;
@@ -21,10 +19,9 @@ export default class HciAddonConfig extends HarvesterResource {
out.push(rancherDashboard); out.push(rancherDashboard);
} }
const canUpdate = !!this.linkFor('update');
const toggleAddon = { const toggleAddon = {
action: 'toggleAddon', action: 'toggleAddon',
enabled: canUpdate, enabled: true,
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play', icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'), label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
}; };
@@ -48,15 +45,6 @@ export default class HciAddonConfig extends HarvesterResource {
} }
} }
if (!this.spec.enabled && this.id === HARVESTER_NVIDIA_DRIVER_TOOLKIT) {
this.$dispatch('promptModal', {
resources: [this],
component: 'HarvesterEnableNvidiaDriverToolkit',
});
return;
}
this.spec.enabled = !this.spec.enabled; this.spec.enabled = !this.spec.enabled;
await this.save(); await this.save();
} catch (err) { } catch (err) {
@@ -3,20 +3,6 @@ import { findBy } from '@shell/utils/array';
import HarvesterResource from './harvester'; import HarvesterResource from './harvester';
export default class HciKeypair extends HarvesterResource { export default class HciKeypair extends HarvesterResource {
get _availableActions() {
let out = super._availableActions;
out = out.map((action) => {
if (['download'].includes(action.action)) {
return { ...action, enabled: !!this.linkFor('update') };
}
return action;
});
return out;
}
get stateDisplay() { get stateDisplay() {
const conditions = get(this, 'status.conditions'); const conditions = get(this, 'status.conditions');
const status = (findBy(conditions, 'type', 'validated') || {}).status ; const status = (findBy(conditions, 'type', 'validated') || {}).status ;
@@ -19,18 +19,16 @@ export default class ScheduleVmBackup extends HarvesterResource {
} }
}); });
const canUpdate = !!this.linkFor('update');
return [ return [
{ {
action: 'resumeSchedule', action: 'resumeSchedule',
enabled: canUpdate && ucFirst(this.state) === STATES.suspended.label, enabled: ucFirst(this.state) === STATES.suspended.label,
icon: 'icons icon-play', icon: 'icons icon-play',
label: this.t('harvester.action.resumeSchedule'), label: this.t('harvester.action.resumeSchedule'),
}, },
{ {
action: 'suspendSchedule', action: 'suspendSchedule',
enabled: canUpdate && ucFirst(this.state) === STATES.active.label, enabled: ucFirst(this.state) === STATES.active.label,
icon: 'icons icon-pause', icon: 'icons icon-pause',
label: this.t('harvester.action.suspendSchedule'), label: this.t('harvester.action.suspendSchedule'),
}, },
@@ -39,6 +39,9 @@ function isReady() {
export default class HciVmImage extends HarvesterResource { export default class HciVmImage extends HarvesterResource {
get availableActions() { get availableActions() {
let out = super._availableActions; let out = super._availableActions;
const toFilter = ['goToEditYaml'];
out = out.filter( (A) => !toFilter.includes(A.action));
// show `Clone` only when imageSource is `download` // show `Clone` only when imageSource is `download`
if (this.imageSource !== 'download') { if (this.imageSource !== 'download') {
@@ -52,7 +55,6 @@ export default class HciVmImage extends HarvesterResource {
canCreateVM = false; canCreateVM = false;
} }
const canCreateImage = !!this.$getters?.['schemaFor']?.(HCI.IMAGE)?.collectionMethods?.some((method) => method.toLowerCase() === 'post');
const customActions = this.isReady ? [ const customActions = this.isReady ? [
{ {
action: 'createFromImage', action: 'createFromImage',
@@ -62,13 +64,13 @@ export default class HciVmImage extends HarvesterResource {
}, },
{ {
action: 'encryptImage', action: 'encryptImage',
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted && canCreateImage, enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
icon: 'icon icon-lock', icon: 'icon icon-lock',
label: this.t('harvester.action.encryptImage'), label: this.t('harvester.action.encryptImage'),
}, },
{ {
action: 'decryptImage', action: 'decryptImage',
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted && canCreateImage, enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
icon: 'icon icon-unlock', icon: 'icon icon-unlock',
label: this.t('harvester.action.decryptImage'), label: this.t('harvester.action.decryptImage'),
}, },
@@ -23,17 +23,17 @@ export default class HciVmTemplateVersion extends HarvesterResource {
}); });
const schema = this.$getters['schemaFor'](HCI.VM); const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = false; let canCreateVM = true;
if (schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) { if ( schema && !schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = true; canCreateVM = false;
} }
return [ return [
{ {
action: 'launchFromTemplate', action: 'launchFromTemplate',
icon: 'icon icon-spinner', icon: 'icon icon-spinner',
enabled: canCreateVM && this.isReady, disabled: !canCreateVM || !this.isReady,
label: this.t('harvester.action.launchFormTemplate'), label: this.t('harvester.action.launchFormTemplate'),
}, },
{ {
@@ -83,60 +83,17 @@ const VMIPhase = {
let productInStore; let productInStore;
let _podOwnerMap = null;
let _podOwnerMapSource = null;
function getPodByOwnerName(rootGetters, inStore, ownerName) {
const podList = rootGetters[`${ inStore }/all`](POD);
if (!Array.isArray(podList)) {
return undefined;
}
// if not equals (usually means the pod list has been updated), we need to rebuild the map, otherwise we can reuse the map for better performance
if (_podOwnerMapSource !== podList) {
_podOwnerMap = new Map(); // use Map to store ownerReference name and pod mapping
for (const pod of podList) {
const refName = pod.metadata?.ownerReferences?.[0]?.name;
if (refName) {
_podOwnerMap.set(refName, pod);
}
}
_podOwnerMapSource = podList;
}
return _podOwnerMap.get(ownerName);
}
function getPvcsByNames(rootGetters, inStore, names) {
const pvcList = rootGetters[`${ inStore }/all`](PVC);
if (!Array.isArray(pvcList)) {
return [];
}
const uniqueNames = new Set(names);
return pvcList.filter((pvc) => uniqueNames.has(pvc.metadata?.name));
}
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims']; const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
export default class VirtVm extends HarvesterResource { export default class VirtVm extends HarvesterResource {
get availableActions() { get availableActions() {
let out = super._availableActions; const out = super._availableActions;
if (this.isCloneBackendStorageCloning || this.isCloneBackendStorageFailed) {
out = out.filter(({ action }) => action !== 'goToClone');
}
const clone = out.find((action) => action.action === 'goToClone'); const clone = out.find((action) => action.action === 'goToClone');
if (clone) { if (clone) {
clone.action = 'goToCloneVM'; clone.action = 'goToCloneVM';
} }
const canCreateVMSSchedule = !!this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
return [ return [
{ {
action: 'stopVM', action: 'stopVM',
@@ -169,7 +126,6 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'restartVM', action: 'restartVM',
altAction: 'altRestartVM',
enabled: !!this.actions?.restart, enabled: !!this.actions?.restart,
icon: 'icon icon-refresh', icon: 'icon icon-refresh',
label: this.t('harvester.action.restart'), label: this.t('harvester.action.restart'),
@@ -177,19 +133,17 @@ export default class VirtVm extends HarvesterResource {
bulkAction: 'restartVM' bulkAction: 'restartVM'
}, },
{ {
action: 'softrebootVM', action: 'softrebootVM',
altAction: 'doSoftReboot', enabled: !!this.actions?.softreboot,
enabled: !!this.actions?.softreboot, icon: 'icon icon-pipeline',
icon: 'icon icon-pipeline', label: this.t('harvester.action.softreboot')
label: this.t('harvester.action.softreboot')
}, },
{ {
action: 'startVM', action: 'startVM',
enabled: !!this.actions?.start, enabled: !!this.actions?.start,
icon: 'icon icon-play', icon: 'icon icon-play',
label: this.t('harvester.action.start'), label: this.t('harvester.action.start'),
bulkable: true, bulkable: true
bulkAction: 'startVM'
}, },
{ {
action: 'backupVM', action: 'backupVM',
@@ -217,7 +171,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'createSchedule', action: 'createSchedule',
enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled, enabled: this.schedulingVMBackupFeatureEnabled,
icon: 'icon icon-history', icon: 'icon icon-history',
label: this.t('harvester.action.createSchedule') label: this.t('harvester.action.createSchedule')
}, },
@@ -234,12 +188,10 @@ export default class VirtVm extends HarvesterResource {
label: this.t('harvester.action.ejectCDROM') label: this.t('harvester.action.ejectCDROM')
}, },
{ {
action: 'migrateVM', action: 'migrateVM',
enabled: !!this.actions?.migrate, enabled: !!this.actions?.migrate,
icon: 'icon icon-copy', icon: 'icon icon-copy',
label: this.t('harvester.action.vmMigrate'), label: this.t('harvester.action.migrate')
bulkable: true,
bulkAction: 'migrateVM'
}, },
{ {
action: 'abortMigrationVM', action: 'abortMigrationVM',
@@ -550,38 +502,16 @@ export default class VirtVm extends HarvesterResource {
}); });
} }
async altRestartVM() { altStopVM() {
await this.doActionGrowl('restart', {}); this.doActionGrowl('stop', {});
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
} }
async altStopVM() { forceStop() {
await this.doActionGrowl('stop', {}); this.doActionGrowl('forceStop', {});
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
} }
async forceStop() { startVM() {
await this.doActionGrowl('forceStop', {}); this.doActionGrowl('start', {});
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
}
async startVM(resources = this) {
const list = Array.isArray(resources) ? resources : [resources];
for (const r of list) {
await r.doActionGrowl('start', {});
}
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
}
async download() {
await super.download();
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
}
async downloadBulk(items) {
await super.downloadBulk(items);
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
} }
migrateVM(resources = this) { migrateVM(resources = this) {
@@ -730,13 +660,16 @@ export default class VirtVm extends HarvesterResource {
get podResource() { get podResource() {
const inStore = this.productInStore; const inStore = this.productInStore;
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id); const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
const podList = this.$rootGetters[`${ inStore }/all`](POD);
if (!vmiResource?.metadata?.name) { return podList.find((P) => {
return undefined; return (
} vmiResource?.metadata?.name &&
vmiResource?.metadata?.name === P.metadata?.ownerReferences?.[0].name
return getPodByOwnerName(this.$rootGetters, inStore, vmiResource.metadata.name); );
});
} }
get isPaused() { get isPaused() {
@@ -777,13 +710,17 @@ export default class VirtVm extends HarvesterResource {
get vmi() { get vmi() {
const inStore = this.productInStore; const inStore = this.productInStore;
return this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id); const vmis = this.$rootGetters[`${ inStore }/all`](HCI.VMI);
return vmis.find((VMI) => VMI.id === this.id);
} }
get volumes() { get volumes() {
const pvcs = this.$rootGetters[`${ this.productInStore }/all`](PVC);
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || []; const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
return getPvcsByNames(this.$rootGetters, this.productInStore, volumeClaimNames); return pvcs.filter((pvc) => volumeClaimNames.includes(pvc.metadata.name));
} }
get lvmVolumes() { get lvmVolumes() {
@@ -794,18 +731,6 @@ export default class VirtVm extends HarvesterResource {
return this.volumes.filter((volume) => volume?.isLonghornV2); return this.volumes.filter((volume) => volume?.isLonghornV2);
} }
get cloneBackendStorageStatus() {
return this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS]?.toLowerCase() || '';
}
get isCloneBackendStorageCloning() {
return this.cloneBackendStorageStatus === 'cloning';
}
get isCloneBackendStorageFailed() {
return this.cloneBackendStorageStatus === 'failed';
}
get encryptedVolumeType() { get encryptedVolumeType() {
if (!this.volumes || this.volumes.length === 0) { if (!this.volumes || this.volumes.length === 0) {
return 'none'; return 'none';
@@ -828,6 +753,17 @@ export default class VirtVm extends HarvesterResource {
return { status: 'VMI error', detailedMessage: vmiFailureCond.message }; return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
} }
if ((this.vmi || this.isVMCreated) && this.podResource) {
// const podStatus = this.podResource.getPodStatus;
// if (POD_STATUS_ALL_ERROR.includes(podStatus?.status)) {
// return {
// ...podStatus,
// status: 'LAUNCHER_POD_ERROR',
// pod: this.podResource,
// };
// }
}
return this?.vmi?.status?.phase; return this?.vmi?.status?.phase;
} }
@@ -861,21 +797,13 @@ export default class VirtVm extends HarvesterResource {
!this.isVMExpectedRunning && !this.isVMExpectedRunning &&
this.isVMCreated && this.isVMCreated &&
this.vmi?.status?.phase === VMIPhase.Pending this.vmi?.status?.phase === VMIPhase.Pending
) || this.isCloneBackendStorageCloning) { ) || (this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS] === 'cloning')) {
return { status: VMIPhase.Pending }; return { status: VMIPhase.Pending };
} }
return null; return null;
} }
get isCloneFailed() {
if (this.isCloneBackendStorageFailed) {
return { status: VMIPhase.Failed };
}
return null;
}
get isStopping() { get isStopping() {
if (this && if (this &&
!this.isVMExpectedRunning && !this.isVMExpectedRunning &&
@@ -973,7 +901,9 @@ export default class VirtVm extends HarvesterResource {
const inStore = this.productInStore; const inStore = this.productInStore;
const res = this.$rootGetters[`${ inStore }/byId`](HCI.RESTORE, id); const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE);
const res = allRestore.find((O) => O.id === id);
if (res) { if (res) {
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP); const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
@@ -1032,7 +962,6 @@ export default class VirtVm extends HarvesterResource {
this.isUnschedulable?.status || this.isUnschedulable?.status ||
this.isPaused?.status || this.isPaused?.status ||
this.isVMError?.status || this.isVMError?.status ||
this.isCloneFailed?.status ||
this.isPending?.status || this.isPending?.status ||
this.isStopping?.status || this.isStopping?.status ||
this.isOff?.status || this.isOff?.status ||
@@ -1040,7 +969,7 @@ export default class VirtVm extends HarvesterResource {
this.isRunning?.status || this.isRunning?.status ||
this.isNotReady?.status || this.isNotReady?.status ||
this.isStarting?.status || this.isStarting?.status ||
this.isWaitingForVMI?.status || this.isWaitingForVMI?.state ||
this.otherState?.status; this.otherState?.status;
return state; return state;
@@ -1144,6 +1073,42 @@ export default class VirtVm extends HarvesterResource {
return out; return out;
} }
get warningCount() {
return this.resourcesStatus.warningCount;
}
get errorCount() {
return this.resourcesStatus.errorCount;
}
get resourcesStatus() {
const inStore = this.productInStore;
const vmList = this.$rootGetters[`${ inStore }/all`](HCI.VM);
let warningCount = 0;
let errorCount = 0;
vmList.forEach((vm) => {
const status = vm.actualState;
if (status === VM_ERROR) {
errorCount += 1;
} else if (
status === 'Stopping' ||
status === 'Waiting' ||
status === 'Pending' ||
status === 'Starting' ||
status === 'Terminating'
) {
warningCount += 1;
}
});
return {
warningCount,
errorCount
};
}
get volumeClaimTemplates() { get volumeClaimTemplates() {
return parseVolumeClaimTemplates(this); return parseVolumeClaimTemplates(this);
} }
@@ -1161,6 +1126,7 @@ export default class VirtVm extends HarvesterResource {
get rootImageId() { get rootImageId() {
let imageId = ''; let imageId = '';
const inStore = this.productInStore; const inStore = this.productInStore;
const pvcs = this.$rootGetters[`${ inStore }/all`](PVC) || [];
const volumes = this.spec.template.spec.volumes || []; const volumes = this.spec.template.spec.volumes || [];
@@ -1170,7 +1136,9 @@ export default class VirtVm extends HarvesterResource {
}); });
if (!isNoExistingVolume) { if (!isNoExistingVolume) {
const existingVolume = this.$rootGetters[`${ inStore }/byId`](PVC, `${ this.metadata.namespace }/${ firstVolumeName }`); const existingVolume = pvcs.find(
(P) => P.id === `${ this.metadata.namespace }/${ firstVolumeName }`
);
if (existingVolume) { if (existingVolume) {
return existingVolume?.metadata?.annotations?.[ return existingVolume?.metadata?.annotations?.[
@@ -1319,10 +1287,6 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup'); return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
} }
get nameDisplay() {
return this.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || this.metadata?.name || this.id;
}
get volumeEncryptionFeatureEnabled() { get volumeEncryptionFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption'); return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
} }
@@ -1352,7 +1316,8 @@ export default class VirtVm extends HarvesterResource {
} }
get isBackupTargetUnavailable() { get isBackupTargetUnavailable() {
const backupTargetSetting = this.$rootGetters['harvester/byId'](HCI.SETTING, 'backup-target'); const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
return isBackupTargetSettingUnavailable(backupTargetSetting); return isBackupTargetSettingUnavailable(backupTargetSetting);
} }
@@ -1,19 +0,0 @@
import shellProject from '@shell/models/management.cattle.io.project';
// This model controls `Project / Namespace` page in rancher integration mode
// Extend management.cattle.io.project model from shell
export default class Project extends shellProject {
get _availableActions() {
const canUpdate = !!this.linkFor('update');
// disable `Edit Config` action if user does not have update permission.
return super._availableActions.map((action) => {
if (action.action === 'goToEdit') {
return { ...action, enabled: canUpdate };
}
return action;
});
}
}
@@ -57,11 +57,7 @@ export default class HciVlanConfig extends HarvesterResource {
get _availableActions() { get _availableActions() {
const out = super._availableActions; const out = super._availableActions;
const canMigrate = !!this.$getters?.['schemaFor']?.(HCI.VLAN_CONFIG)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase())); insertAt(out, 0, this.migrateAction);
if (canMigrate) {
insertAt(out, 0, this.migrateAction);
}
return out; return out;
} }
+5 -5
View File
@@ -1,7 +1,7 @@
{ {
"name": "harvester", "name": "harvester",
"description": "Rancher UI Extension for Harvester", "description": "Rancher UI Extension for Harvester",
"version": "1.9.0-dev", "version": "1.8.0-rc4",
"private": false, "private": false,
"rancher": { "rancher": {
"annotations": { "annotations": {
@@ -17,12 +17,12 @@
"nuxt": "./node_modules/.bin/nuxt" "nuxt": "./node_modules/.bin/nuxt"
}, },
"engines": { "engines": {
"node": ">=24.0.0" "node": ">=20.0.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~5.0.9", "@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-service": "~5.0.9", "@vue/cli-service": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.9" "@vue/cli-plugin-typescript": "~5.0.0"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
@@ -123,7 +123,6 @@ export default {
this.value?.[0]?.currentRouter().push(goTo); this.value?.[0]?.currentRouter().push(goTo);
} }
this.close(); this.close();
this.$store.commit('action-menu/togglePromptModal', { performCallback: true, clearTableSelection: true });
}).catch((err) => { }).catch((err) => {
this.$emit('errors', err); this.$emit('errors', err);
}); });
+1 -22
View File
@@ -123,26 +123,5 @@ export default {
const clusterId = currentCluster.id; const clusterId = currentCluster.id;
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System'); return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
}, }
// Few harvester resources name and REAL resource are different. E.g. HCI.NETWORK_ATTACHMENT page resource is NETWORK_ATTACHMENT.
// Check in config/harvester-cluster.js for more details.
// We need to look up the schema by resource name, and fallback to find using real resource name
schemaFor: (state, getters, rootState, rootGetters) => (type, _fuzzy = false, _allowThrow = true) => {
// follow the same logic as type-map/schemaFor in /dashboard/shell/plugins/dashboard-store/getters.js
const normalizedType = getters.normalizeType(type);
const schemas = state.types['schema'];
const out = schemas?.map?.get(normalizedType);
if (out) return out;
// if not found, use the resource mapping in configureType for a second try
const resourceType = rootGetters['type-map/optionsFor'](type)?.resource;
if (resourceType && resourceType !== type) {
const normalizedResource = getters.normalizeType(resourceType);
return schemas?.map?.get(normalizedResource) || null;
}
return null;
},
}; };
-2
View File
@@ -16,7 +16,6 @@ export const HCI = {
RESTORE: 'harvesterhci.io.virtualmachinerestore', RESTORE: 'harvesterhci.io.virtualmachinerestore',
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork', NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork', CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
HOST_NETWORK_CONFIG: 'network.harvesterhci.io.hostnetworkconfig',
SUBNET: 'kubeovn.io.subnet', SUBNET: 'kubeovn.io.subnet',
VPC: 'kubeovn.io.vpc', VPC: 'kubeovn.io.vpc',
IP: 'kubeovn.io.ip', IP: 'kubeovn.io.ip',
@@ -63,7 +62,6 @@ export const HCI = {
VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource', VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource',
VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport', VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport',
MIGRATION: 'migration.harvesterhci.io', MIGRATION: 'migration.harvesterhci.io',
}; };
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot'; export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
-31
View File
@@ -1,31 +0,0 @@
/**
* 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'];
}
+12 -32
View File
@@ -31,42 +31,22 @@ export function registerAddonSideNav(store, productName, {
}, 600); }, 600);
}; };
const hasAccessibleSchema = (t) => {
try {
return !!store.getters[`${ productName }/schemaFor`]?.(t);
} catch (e) {
return false;
}
};
const showTypes = (visibleTypes) => {
store.commit('type-map/basicType', {
product: productName,
group: navGroup,
types: visibleTypes
});
};
const hideTypes = () => {
const basicTypes = store.state['type-map'].basicTypes[productName];
if (basicTypes) {
types.forEach((t) => delete basicTypes[t]);
}
};
// Adds or removes the resource IDs from the product visibility whitelist. // Adds or removes the resource IDs from the product visibility whitelist.
const setMenuVisibility = (visible) => { const setMenuVisibility = (visible) => {
const accessibleTypes = visible ? types.filter(hasAccessibleSchema) : []; if (visible) {
store.commit('type-map/basicType', {
product: productName,
group: navGroup,
types
});
} else {
// Manually delete the keys from the state object to hide them.
const basicTypes = store.state['type-map'].basicTypes[productName];
// Always clear first to remove any previously-registered types that are if (basicTypes) {
// no longer accessible (e.g. partial permission changes like types=[A,B] where B is dropped). types.forEach((t) => delete basicTypes[t]);
hideTypes(); }
if (accessibleTypes.length > 0) {
showTypes(accessibleTypes);
} }
kickSideNav(); kickSideNav();
}; };
-21
View File
@@ -1,21 +0,0 @@
const AUTH_ERROR_CODES = [401, 403, 404];
export function getLoginAwareErrors(err, message = '') {
const errors = Array.isArray(err) ? err : (err ? [err] : []);
if (!errors.length) {
return [];
}
const generic = message;
if (errors.some((e) => AUTH_ERROR_CODES.includes(e?._status || e?.response?.status))) {
return [generic];
}
const msgs = errors
.map((e) => (typeof e === 'string' ? e : (e?.message || e?._statusText || '')))
.filter(Boolean);
return msgs.length ? msgs : [generic];
}

Some files were not shown because too many files have changed in this diff Show More