mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-05-15 07:21:48 +00:00
Compare commits
39 Commits
main
...
v1.8.1-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b887e5ee01 | ||
|
|
bf9b5866a7 | ||
|
|
b97017e076 | ||
|
|
3740c9c424 | ||
|
|
af8e8b2eeb | ||
|
|
7906033287 | ||
|
|
5584c86789 | ||
|
|
1dcb528717 | ||
|
|
36fc21ccbc | ||
|
|
8803b79524 | ||
|
|
3f80a71303 | ||
|
|
13e4154335 | ||
|
|
a80b87ce4f | ||
|
|
809b9925fa | ||
|
|
7788161d94 | ||
|
|
90b8a821a1 | ||
|
|
87ff014bd8 | ||
|
|
20328b06e6 | ||
|
|
bbc415a2b4 | ||
|
|
c12d4b5fba | ||
|
|
dc91be11f9 | ||
|
|
e5b72499b5 | ||
|
|
e66037b007 | ||
|
|
c676cad057 | ||
|
|
254abd4648 | ||
|
|
1df1d3534a | ||
|
|
ff1e6e6e7f | ||
|
|
622242e4a5 | ||
|
|
d8ea9be174 | ||
|
|
f8a479cfcf | ||
|
|
6d5e584113 | ||
|
|
a9eb04195e | ||
|
|
0441c1f6e3 | ||
|
|
dde58269d5 | ||
|
|
b8111f0ad7 | ||
|
|
6d627f82e9 | ||
|
|
cfc7a76fe7 | ||
|
|
71d3067354 | ||
|
|
9ecc372009 |
3
.github/actions/setup/action.yaml
vendored
3
.github/actions/setup/action.yaml
vendored
@ -4,7 +4,8 @@ description: Setup node and install dependencies
|
|||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Nodejs with yarn caching
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|||||||
6
.github/auto-assign-config.yaml
vendored
6
.github/auto-assign-config.yaml
vendored
@ -1,6 +0,0 @@
|
|||||||
addAssignees: author
|
|
||||||
addReviewers: true
|
|
||||||
numberOfReviewers: 0
|
|
||||||
reviewers:
|
|
||||||
- a110605
|
|
||||||
- houhoucoop
|
|
||||||
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@ -34,12 +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", "digest"],
|
"matchUpdateTypes": ["patch", "digest"],
|
||||||
"automerge": true,
|
"automerge": true,
|
||||||
|
"minimumReleaseAge": "7 days",
|
||||||
"groupName": "patch digest dependencies",
|
"groupName": "patch digest dependencies",
|
||||||
"labels": ["patch-update", "automerge"]
|
"labels": ["patch-update", "automerge"]
|
||||||
}
|
}
|
||||||
|
|||||||
39
.github/workflows/add-pr-label-check.yaml
vendored
Normal file
39
.github/workflows/add-pr-label-check.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: "PR Management Add PR Label Collect Data"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, edited]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
collect:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
|
- name: Setup Nodejs and yarn install
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
|
||||||
|
- name: Extract PR label
|
||||||
|
run: |
|
||||||
|
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||||
|
echo "PR_LABEL=$PR_LABEL"
|
||||||
|
{
|
||||||
|
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||||
|
echo "PR_LABEL=$PR_LABEL"
|
||||||
|
} > pr-add-label-data.env
|
||||||
|
|
||||||
|
- name: Upload PR data artifact
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: pr-add-label-data
|
||||||
|
path: pr-add-label-data.env
|
||||||
33
.github/workflows/add-pr-label.yaml
vendored
Normal file
33
.github/workflows/add-pr-label.yaml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: "PR Management Add PR Label"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "PR Management Add PR Label Collect Data"
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign-pr-label:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Download PR data artifact
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
name: pr-add-label-data
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Load PR data
|
||||||
|
run: |
|
||||||
|
cat pr-add-label-data.env >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set PR label
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
echo "PR_LABEL = $PR_LABEL"
|
||||||
|
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$PR_LABEL"
|
||||||
28
.github/workflows/auto-assign-check.yaml
vendored
Normal file
28
.github/workflows/auto-assign-check.yaml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
name: "PR Management Auto Assign Collect Data"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, ready_for_review]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
collect:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
steps:
|
||||||
|
- name: Save PR data to artifact
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||||
|
echo "PR_AUTHOR=${{ github.event.pull_request.user.login }}"
|
||||||
|
} > pr-auto-assign-data.env
|
||||||
|
|
||||||
|
- name: Upload PR data artifact
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: pr-auto-assign-data
|
||||||
|
path: pr-auto-assign-data.env
|
||||||
17
.github/workflows/auto-assign-reviewer.yaml
vendored
17
.github/workflows/auto-assign-reviewer.yaml
vendored
@ -1,17 +0,0 @@
|
|||||||
name: "[PR Management] Auto Assign Reviewer & Assignee"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, ready_for_review]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-assign:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: rancher/gh-issue-mgr/auto-assign-action@main
|
|
||||||
with:
|
|
||||||
configuration-path: .github/auto-assign-config.yaml
|
|
||||||
35
.github/workflows/auto-assign.yaml
vendored
Normal file
35
.github/workflows/auto-assign.yaml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: "PR Management Auto Assign"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "PR Management Auto Assign Collect Data"
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
env:
|
||||||
|
REVIEWERS: "a110605,houhoucoop"
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Download PR data artifact
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
name: pr-auto-assign-data
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Load PR data
|
||||||
|
run: |
|
||||||
|
cat pr-auto-assign-data.env >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Auto assign PR author
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
echo "Assigning PR author: $PR_AUTHOR"
|
||||||
|
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-assignee "$PR_AUTHOR" --add-reviewer "$REVIEWERS"
|
||||||
30
.github/workflows/backport-label-check.yaml
vendored
Normal file
30
.github/workflows/backport-label-check.yaml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: "PR Management Add Labels Collect Data"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
collect:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
steps:
|
||||||
|
- name: Save PR data to artifact
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||||
|
echo "PR_BASE_REF=${{ github.event.pull_request.base.ref }}"
|
||||||
|
echo "PR_USER_LOGIN=${{ github.event.pull_request.user.login }}"
|
||||||
|
} > pr-backport-label-data.env
|
||||||
|
|
||||||
|
- name: Upload PR data artifact
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: pr-backport-label-data
|
||||||
|
path: pr-backport-label-data.env
|
||||||
99
.github/workflows/backport-label.yaml
vendored
99
.github/workflows/backport-label.yaml
vendored
@ -1,40 +1,44 @@
|
|||||||
name: "[PR Management] Add Labels"
|
name: "PR Management Add Backport Labels"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
workflow_run:
|
||||||
types: [opened, reopened]
|
workflows:
|
||||||
branches:
|
- "PR Management Add Labels Collect Data"
|
||||||
- main
|
types: [completed]
|
||||||
- '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: Checkout repository
|
- name: Download PR data artifact
|
||||||
uses: actions/checkout@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
name: pr-backport-label-data
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
- name: Fetch release branches and PR labels
|
- name: Load PR data
|
||||||
id: fetch_info
|
run: |
|
||||||
|
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: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo="${{ github.repository }}"
|
repo="${{ github.repository }}"
|
||||||
pr_number=${{ github.event.pull_request.number }}
|
pr_number="$PR_NUMBER"
|
||||||
|
|
||||||
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
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 "should_label=false" >> "$GITHUB_OUTPUT"
|
echo "No release branches found, skipping."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -44,51 +48,36 @@ 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 "should_label=false" >> "$GITHUB_OUTPUT"
|
echo "Release $release_tag already published, skipping."
|
||||||
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"
|
|
||||||
|
|
||||||
- name: Add label if needed
|
if echo "$pr_labels" | grep -Fxq "$label"; then
|
||||||
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
|
echo "Label '$label' already present, skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Adding label: $label"
|
||||||
|
gh pr edit "$pr_number" --repo "$repo" --add-label "$label"
|
||||||
|
|
||||||
|
- name: Add backport label (release branch PRs opened by Mergify)
|
||||||
|
if: startsWith(env.PR_BASE_REF, 'release-harvester-v')
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
|
set -euo pipefail
|
||||||
gh pr edit ${{ github.event.pull_request.number }} \
|
|
||||||
--repo ${{ github.repository }} \
|
|
||||||
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
|
|
||||||
|
|
||||||
add-backport-label:
|
IS_MERGIFY=$(echo "$PR_USER_LOGIN" | grep -iq 'mergify' && echo true || echo false)
|
||||||
if: github.event.pull_request.draft == false &&
|
|
||||||
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check conditions for backport label
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
|
|
||||||
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
|
|
||||||
|
|
||||||
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
|
if [[ "$IS_MERGIFY" != "true" ]]; then
|
||||||
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
|
echo "PR author is not Mergify, skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Add label if needed
|
version="${PR_BASE_REF#release-harvester-v}"
|
||||||
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 ${{ github.event.pull_request.number }} \
|
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$label"
|
||||||
--repo ${{ github.repository }} \
|
|
||||||
--add-label "$label"
|
|
||||||
|
|||||||
28
.github/workflows/backport-request-check.yaml
vendored
Normal file
28
.github/workflows/backport-request-check.yaml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
name: "PR Management Request Backport via Mergify Collect Data"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
collect:
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
steps:
|
||||||
|
- name: Save PR data to artifact
|
||||||
|
run: |
|
||||||
|
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
||||||
|
{
|
||||||
|
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||||
|
echo "PR_LABELS=$(echo "$labels_json" | jq -r '[.[]] | join(",")')"
|
||||||
|
} > pr-backport-request-data.env
|
||||||
|
|
||||||
|
- name: Upload PR data artifact
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: pr-backport-request-data
|
||||||
|
path: pr-backport-request-data.env
|
||||||
38
.github/workflows/backport-request.yaml
vendored
38
.github/workflows/backport-request.yaml
vendored
@ -1,41 +1,47 @@
|
|||||||
name: "[PR Management] Request Backport via Mergify"
|
name: "PR Management Request Backport via Mergify"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
workflow_run:
|
||||||
types: [closed]
|
workflows:
|
||||||
branches: [main]
|
- "PR Management Request Backport via Mergify Collect Data"
|
||||||
|
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: Checkout repository
|
- name: Download PR data artifact
|
||||||
uses: actions/checkout@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
name: pr-backport-request-data
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Load PR data
|
||||||
|
run: |
|
||||||
|
cat pr-backport-request-data.env >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Post Mergify backport command
|
- name: Post Mergify backport command
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
pr_number=${{ github.event.pull_request.number }}
|
pr_number="$PR_NUMBER"
|
||||||
repo="${{ github.repository }}"
|
repo="${{ github.repository }}"
|
||||||
|
|
||||||
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
labels=$(echo "$PR_LABELS" | tr ',' '\n')
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Check package version
|
- name: Check package version
|
||||||
env:
|
env:
|
||||||
|
|||||||
@ -25,12 +25,12 @@ jobs:
|
|||||||
name: Build & Upload Hosted
|
name: Build & Upload Hosted
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@ -45,19 +45,20 @@ 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@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # 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 ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: 'google-github-actions/auth@v2'
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||||
|
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload build
|
- name: Upload build
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
# https://github.com/google-github-actions/upload-cloud-storage
|
# https://github.com/google-github-actions/upload-cloud-storage
|
||||||
with:
|
with:
|
||||||
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
||||||
@ -71,12 +72,12 @@ jobs:
|
|||||||
name: Build & Upload Embedded
|
name: Build & Upload Embedded
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@ -89,19 +90,19 @@ 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@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # 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 ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: 'google-github-actions/auth@v2'
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload tar
|
- name: Upload tar
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
with:
|
with:
|
||||||
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
||||||
destination: releases.rancher.com/harvester-ui/dashboard
|
destination: releases.rancher.com/harvester-ui/dashboard
|
||||||
@ -114,12 +115,12 @@ jobs:
|
|||||||
name: Build & Upload Plugin
|
name: Build & Upload Plugin
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@ -133,19 +134,19 @@ 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@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # 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 ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: 'google-github-actions/auth@v2'
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload plugin tar
|
- name: Upload plugin tar
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
with:
|
with:
|
||||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
||||||
destination: releases.rancher.com/harvester-ui/plugin
|
destination: releases.rancher.com/harvester-ui/plugin
|
||||||
@ -155,7 +156,7 @@ jobs:
|
|||||||
process_gcloudignore: false
|
process_gcloudignore: false
|
||||||
|
|
||||||
- name: Upload plugin directory
|
- name: Upload plugin directory
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
with:
|
with:
|
||||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||||
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||||
|
|||||||
20
.github/workflows/build-extension-catalog.yml
vendored
20
.github/workflows/build-extension-catalog.yml
vendored
@ -27,14 +27,14 @@ jobs:
|
|||||||
build-status: ${{ job.status }}
|
build-status: ${{ job.status }}
|
||||||
steps:
|
steps:
|
||||||
- name: Read Secrets
|
- name: Read Secrets
|
||||||
uses: rancher-eio/read-vault-secrets@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # 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 ;
|
||||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
||||||
|
|
||||||
- name: Checkout repository (normal flow)
|
- name: Checkout repository (normal flow)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
@ -45,26 +45,20 @@ jobs:
|
|||||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_USERNAME }}
|
username: ${{ env.DOCKER_USERNAME }}
|
||||||
password: ${{ env.DOCKER_PASSWORD }}
|
password: ${{ env.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v3
|
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||||
with:
|
with:
|
||||||
version: v3.8.0
|
version: v3.8.0
|
||||||
|
|
||||||
- name: Setup Nodejs with yarn caching
|
- name: Setup Nodejs with yarn install
|
||||||
uses: actions/setup-node@v4
|
uses: ./.github/actions/setup
|
||||||
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: |
|
||||||
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
||||||
$publish
|
$publish
|
||||||
|
|||||||
24
.github/workflows/build-extension-on-merge.yml
vendored
24
.github/workflows/build-extension-on-merge.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Determine target branch
|
- name: Determine target branch
|
||||||
id: get-version
|
id: get-version
|
||||||
@ -44,7 +44,7 @@ jobs:
|
|||||||
version: ${{ steps.get_version.outputs.version }}
|
version: ${{ steps.get_version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Extract version from package.json
|
- name: Extract version from package.json
|
||||||
id: get_version
|
id: get_version
|
||||||
@ -62,7 +62,13 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
|
- name: Setup Node from .nvmrc
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Setup environment
|
- name: Setup environment
|
||||||
run: |
|
run: |
|
||||||
@ -70,7 +76,7 @@ jobs:
|
|||||||
yarn install --frozen-lockfile
|
yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v3
|
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||||
with:
|
with:
|
||||||
version: v3.8.0
|
version: v3.8.0
|
||||||
|
|
||||||
@ -79,7 +85,7 @@ jobs:
|
|||||||
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
||||||
|
|
||||||
- name: Upload charts artifact
|
- name: Upload charts artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: charts
|
name: charts
|
||||||
path: tmp
|
path: tmp
|
||||||
@ -94,7 +100,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout release branch
|
- name: Checkout release branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
ref: '${{ github.ref_name }}'
|
ref: '${{ github.ref_name }}'
|
||||||
|
|
||||||
@ -105,7 +111,7 @@ jobs:
|
|||||||
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout target branch
|
- name: Checkout target branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
||||||
|
|
||||||
@ -121,7 +127,7 @@ jobs:
|
|||||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
name: charts
|
name: charts
|
||||||
|
|
||||||
@ -132,7 +138,7 @@ jobs:
|
|||||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||||
|
|
||||||
- name: Run Helm chart releaser
|
- name: Run Helm chart releaser
|
||||||
uses: helm/chart-releaser-action@v1.7.0
|
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
|
||||||
with:
|
with:
|
||||||
charts_dir: ./charts
|
charts_dir: ./charts
|
||||||
env:
|
env:
|
||||||
|
|||||||
@ -17,7 +17,7 @@ jobs:
|
|||||||
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Determine release tag
|
- name: Determine release tag
|
||||||
id: determine_tag
|
id: determine_tag
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: Check package version
|
- name: Check package version
|
||||||
env:
|
env:
|
||||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||||
@ -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@master
|
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@3b26a36bad555e5e2b8634b24823be29732f287c # master
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: Check package version
|
- name: Check package version
|
||||||
env:
|
env:
|
||||||
TAG_VERSION: ${{github.ref_name}}
|
TAG_VERSION: ${{github.ref_name}}
|
||||||
|
|||||||
4
.github/workflows/fossa.yml
vendored
4
.github/workflows/fossa.yml
vendored
@ -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@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # 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@main
|
uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.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
.github/workflows/release-label.yaml
vendored
30
.github/workflows/release-label.yaml
vendored
@ -1,30 +0,0 @@
|
|||||||
name: "[PR Management] Add PR Label"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, reopened, edited]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'release-harvester-v*'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-assign-pr-label:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.base_ref }}
|
|
||||||
- name: Setup Nodejs and yarn install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
- name: Set PR label
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
|
||||||
echo "PR_LABEL = $PR_LABEL"
|
|
||||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
|
||||||
2
.github/workflows/run-lint.yaml
vendored
2
.github/workflows/run-lint.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Need full history for commit-lint
|
fetch-depth: 0 # Need full history for commit-lint
|
||||||
|
|
||||||
|
|||||||
261
AGENTS.md
Normal file
261
AGENTS.md
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
|
||||||
|
|
||||||
|
|
||||||
|
# Project Overview
|
||||||
|
|
||||||
|
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the Rancher Dashboard.
|
||||||
|
|
||||||
|
```
|
||||||
|
Note: This extension is available starting from Rancher 2.10.0. Ensure your Rancher version is 2.10.0 or later to access Harvester integration.
|
||||||
|
```
|
||||||
|
|
||||||
|
# Personas
|
||||||
|
|
||||||
|
## Software Developer
|
||||||
|
|
||||||
|
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
|
||||||
|
- **Focus**: Writing clean, maintainable, and performant code.
|
||||||
|
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.
|
||||||
|
|
||||||
|
|
||||||
|
# Agents
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- ✅ **Always:**
|
||||||
|
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
|
||||||
|
- Make commits in a new branch (for a PR).
|
||||||
|
- Run `yarn lint:fix` before commits.
|
||||||
|
- Use conventional commit format:
|
||||||
|
```
|
||||||
|
<type>:
|
||||||
|
|
||||||
|
<description>
|
||||||
|
```
|
||||||
|
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
||||||
|
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
||||||
|
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
|
||||||
|
- ⚠️ **Ask first:**
|
||||||
|
- Adding dependencies
|
||||||
|
- Upgrading dependencies
|
||||||
|
- 🚫 **Never:**
|
||||||
|
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
|
||||||
|
- Edit `node_modules/`.
|
||||||
|
- Commit directly to `main` (use PRs).
|
||||||
|
- Skip git hooks with `--no-verify` flag.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
|
||||||
|
- **Package manager**: `yarn` (v1 classic)
|
||||||
|
|
||||||
|
### Common Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
|
||||||
|
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
|
||||||
|
| `yarn build-pkg harvester` | Build the Harvester extension package |
|
||||||
|
| `yarn serve-pkgs` | Serve the locally built extension for testing |
|
||||||
|
| `yarn lint` | Run ESLint (zero warnings allowed) |
|
||||||
|
| `yarn lint:fix` | Run ESLint with auto-fix |
|
||||||
|
| `yarn clean` | Clean build artifacts |
|
||||||
|
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
- **Start dev server**:
|
||||||
|
```bash
|
||||||
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
```
|
||||||
|
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
|
||||||
|
- The dashboard will be available at `https://127.0.0.1:8005`.
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
- **Build extension package**:
|
||||||
|
```bash
|
||||||
|
yarn build-pkg harvester
|
||||||
|
```
|
||||||
|
- **Serve locally built extension** (for integration testing with a Rancher instance):
|
||||||
|
```bash
|
||||||
|
yarn serve-pkgs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
- **Check** (must pass with zero warnings):
|
||||||
|
```bash
|
||||||
|
yarn lint
|
||||||
|
```
|
||||||
|
- **Auto-fix**:
|
||||||
|
```bash
|
||||||
|
yarn lint:fix
|
||||||
|
```
|
||||||
|
- ESLint covers `.js`, `.ts`, and `.vue` files.
|
||||||
|
- Always run `yarn lint:fix` before committing.
|
||||||
|
|
||||||
|
### Commit Conventions
|
||||||
|
|
||||||
|
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
|
||||||
|
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
|
||||||
|
|
||||||
|
### Agent Documentation
|
||||||
|
|
||||||
|
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
|
||||||
|
- After editing any source file, regenerate the root `AGENTS.md`:
|
||||||
|
```bash
|
||||||
|
yarn agents:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
# Contributors Guide
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
|
||||||
|
|
||||||
|
To get started, follow the `Development Setup` section.
|
||||||
|
|
||||||
|
## Project Information
|
||||||
|
|
||||||
|
- **Tech Stack:**
|
||||||
|
- `Vue.js`: Framework
|
||||||
|
- `Linting`: ESLint
|
||||||
|
- `CSS`: SCSS should be used
|
||||||
|
- `TypeScript`: Primary language for logic.
|
||||||
|
- **Code Style and Standards:**
|
||||||
|
- `Language`: TypeScript is preferred for new code.
|
||||||
|
- `Vue.js`:
|
||||||
|
- Composition API components are preferred over Options API.
|
||||||
|
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
||||||
|
- Place source tag above template above style.
|
||||||
|
- style tag should contain `lang='scss' scoped`.
|
||||||
|
- `Linting`: Follow the ESLint configuration in the root.
|
||||||
|
|
||||||
|
- **File Structure:**
|
||||||
|
- `.github/`: CI/CD workflows and Renovate config.
|
||||||
|
- `docs/`: Documentation source for `AGENTS.md` generation.
|
||||||
|
- `scripts/`: Bash scripts for build, CI and doc generation.
|
||||||
|
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
|
||||||
|
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
|
||||||
|
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
|
||||||
|
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
|
||||||
|
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
|
||||||
|
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
|
||||||
|
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
|
||||||
|
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
|
||||||
|
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
|
||||||
|
- `l10n/`: Localization (`en-us.yaml`).
|
||||||
|
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
|
||||||
|
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
|
||||||
|
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
|
||||||
|
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
|
||||||
|
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
|
||||||
|
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
|
||||||
|
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
|
||||||
|
- `styles/`: Global SCSS files.
|
||||||
|
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
|
||||||
|
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Harvester UI Extension Development Guide
|
||||||
|
|
||||||
|
1. Backward Compatibility
|
||||||
|
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
|
||||||
|
|
||||||
|
2. Implementation Steps for New Features
|
||||||
|
To add a feature in a new release, follow these steps:
|
||||||
|
|
||||||
|
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
|
||||||
|
|
||||||
|
Check: Use the following getter to verify if the feature is enabled for the current version:
|
||||||
|
|
||||||
|
```
|
||||||
|
computed: {
|
||||||
|
newFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
Render: Use the result of the check to conditionally render the UI components.
|
||||||
|
|
||||||
|
|
||||||
|
## E2E Tests (Cypress)
|
||||||
|
|
||||||
|
See https://github.com/harvester/harvester-ui-tests
|
||||||
|
|
||||||
|
## Node Dependencies
|
||||||
|
|
||||||
|
Dependencies are managed via `package.json` and `yarn`
|
||||||
|
|
||||||
|
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
|
||||||
|
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
|
||||||
|
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
|
||||||
|
|
||||||
|
|
||||||
|
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
|
||||||
|
For the Renovate config, see `.github/renovate.json`.
|
||||||
|
|
||||||
|
|
||||||
|
## Milestone guidance
|
||||||
|
|
||||||
|
- All issues must first be resolved in the `main` branch
|
||||||
|
- If backports are needed they can be made via the backport bot
|
||||||
|
- pull requests
|
||||||
|
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
|
||||||
|
- All backported pull requests must link to a backported issue
|
||||||
|
|
||||||
|
|
||||||
|
## Creating a branch
|
||||||
|
|
||||||
|
### To resolve an issue
|
||||||
|
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
|
||||||
|
- `main` for the latest unreleased minor version
|
||||||
|
- `release-harvester-v.X` for release minor versions
|
||||||
|
- `release-harvester-v1.6`
|
||||||
|
- `release-harvester-v1.7`
|
||||||
|
- `release-harvester-v1.8`
|
||||||
|
- Ensure you have the latest of that branch `git pull --rebase`
|
||||||
|
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
|
||||||
|
|
||||||
|
## Creating a commit
|
||||||
|
|
||||||
|
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to ensure consistent and meaningful commit messages.
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
All commit messages must follow the conventional commit format:
|
||||||
|
```
|
||||||
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Types
|
||||||
|
- feat: New features
|
||||||
|
- fix: Bug fixes
|
||||||
|
- docs: Documentation changes
|
||||||
|
- style: Code style changes (formatting, missing semicolons, etc.)
|
||||||
|
- refactor: Code refactoring
|
||||||
|
- perf: Performance improvements
|
||||||
|
- test: Adding or updating tests
|
||||||
|
- build: Build system or external dependencies
|
||||||
|
- ci: CI/CD changes
|
||||||
|
- chore: Other changes that don't modify src or test files
|
||||||
|
- revert: Reverts a previous commit
|
||||||
|
- wip: Work in progress
|
||||||
|
- deps: Dependency updates
|
||||||
|
- security: Security fixes
|
||||||
|
|
||||||
|
## Creating a Pull Request
|
||||||
|
|
||||||
|
- Pull requests must come from forks
|
||||||
|
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
|
||||||
|
- A Pull Request will only be merged once
|
||||||
|
- ALL CI gates have passed
|
||||||
|
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR
|
||||||
|
|
||||||
2
CODEOWNERS
Normal file
2
CODEOWNERS
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@a110605
|
||||||
|
@houhoucoop
|
||||||
@ -7,7 +7,7 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
||||||
|
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|||||||
63
docs/agents.md/README
Normal file
63
docs/agents.md/README
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Agents.md
|
||||||
|
|
||||||
|
This folder contains files used to construct a single root `AGENTS.md`
|
||||||
|
|
||||||
|
We do this because
|
||||||
|
- The single commonality between almost all AI tools is that they look for a root `AGENTS.md`
|
||||||
|
- Having the information in a single place for all AI tools is better than duplicating and maintaining the information in multiple files for each AI tool
|
||||||
|
|
||||||
|
## Process
|
||||||
|
1. Add files to or update files in `./docs/agents.md/agents`, `./docs/agents.md/personas`, `./docs/agents.md/contributors`.
|
||||||
|
2. Each file must
|
||||||
|
- Start markdown headers at the `##` level
|
||||||
|
- End in a new line
|
||||||
|
- Order contents in AGENTS.md by giving an optional number prefix
|
||||||
|
3. Run `yarn agents:generate`.
|
||||||
|
- Take the contents of `./docs/agents.md/template_agents.md`
|
||||||
|
- Insert the contents of the agents, personas and contributors files
|
||||||
|
- Output to `./AGENTS.md`
|
||||||
|
4. Commit all changes, including root `./AGENTS.md` and any files that link to it
|
||||||
|
|
||||||
|
## Guidance
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
When referencing code or configuration it's good to include an example
|
||||||
|
|
||||||
|
## Future Tasks
|
||||||
|
- Add real world examples when referencing code
|
||||||
|
- Create a GH workflow to monitor `agents.md` folder files in a PR, if changed run generate script and add new AGENTS.md to PR
|
||||||
|
- Discuss
|
||||||
|
- Three multi AI tool related questions
|
||||||
|
- Questions
|
||||||
|
- how AI specific patterns, like co-pilot instructions, can be expressed in a way to be adopted by or reduce duplication with the root agents.md
|
||||||
|
- how AI tool specific metadata code can be included
|
||||||
|
- multiple agents vs personas approach works
|
||||||
|
- Proposal
|
||||||
|
- Generic information applicable to all AI tools is added to the root `agents.md`
|
||||||
|
- Agent, instruction or other AI specific files or patterns can be used in files specific to that AI tool. They must not contain duplicate information to the root `agents.md`
|
||||||
|
- The root `agents.md` continues to be AI tool agnostic
|
||||||
|
- How can folder specific agents be used
|
||||||
|
- Proposal
|
||||||
|
- `agents.md` are added to folders as long as most LLMs can make use of them and they do not duplicate information in the root `agents.md`
|
||||||
|
|
||||||
|
## Test Prompts
|
||||||
|
|
||||||
|
To validate LLMs are picking up information consider running the following prompts.
|
||||||
|
|
||||||
|
### Information
|
||||||
|
|
||||||
|
Can they provide correct information?
|
||||||
|
|
||||||
|
- `what's this project about?`
|
||||||
|
- `can you tell me the process for fixing a github issue in this repo?`
|
||||||
|
- `what makes a good software developer?`
|
||||||
|
- `what should never be done in this project?`
|
||||||
|
- `what are the folders in this project for?`
|
||||||
|
- `what are the env vars i could use when running e2e tests?`
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
Can they use the information in their actions?
|
||||||
|
|
||||||
|
Pending
|
||||||
23
docs/agents.md/agents/boundaries.md
Normal file
23
docs/agents.md/agents/boundaries.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- ✅ **Always:**
|
||||||
|
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
|
||||||
|
- Make commits in a new branch (for a PR).
|
||||||
|
- Run `yarn lint:fix` before commits.
|
||||||
|
- Use conventional commit format:
|
||||||
|
```
|
||||||
|
<type>:
|
||||||
|
|
||||||
|
<description>
|
||||||
|
```
|
||||||
|
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
||||||
|
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
||||||
|
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
|
||||||
|
- ⚠️ **Ask first:**
|
||||||
|
- Adding dependencies
|
||||||
|
- Upgrading dependencies
|
||||||
|
- 🚫 **Never:**
|
||||||
|
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
|
||||||
|
- Edit `node_modules/`.
|
||||||
|
- Commit directly to `main` (use PRs).
|
||||||
|
- Skip git hooks with `--no-verify` flag.
|
||||||
65
docs/agents.md/agents/tools.md
Normal file
65
docs/agents.md/agents/tools.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
## Tools
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
|
||||||
|
- **Package manager**: `yarn` (v1 classic)
|
||||||
|
|
||||||
|
### Common Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
|
||||||
|
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
|
||||||
|
| `yarn build-pkg harvester` | Build the Harvester extension package |
|
||||||
|
| `yarn serve-pkgs` | Serve the locally built extension for testing |
|
||||||
|
| `yarn lint` | Run ESLint (zero warnings allowed) |
|
||||||
|
| `yarn lint:fix` | Run ESLint with auto-fix |
|
||||||
|
| `yarn clean` | Clean build artifacts |
|
||||||
|
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
- **Start dev server**:
|
||||||
|
```bash
|
||||||
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
```
|
||||||
|
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
|
||||||
|
- The dashboard will be available at `https://127.0.0.1:8005`.
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
- **Build extension package**:
|
||||||
|
```bash
|
||||||
|
yarn build-pkg harvester
|
||||||
|
```
|
||||||
|
- **Serve locally built extension** (for integration testing with a Rancher instance):
|
||||||
|
```bash
|
||||||
|
yarn serve-pkgs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
- **Check** (must pass with zero warnings):
|
||||||
|
```bash
|
||||||
|
yarn lint
|
||||||
|
```
|
||||||
|
- **Auto-fix**:
|
||||||
|
```bash
|
||||||
|
yarn lint:fix
|
||||||
|
```
|
||||||
|
- ESLint covers `.js`, `.ts`, and `.vue` files.
|
||||||
|
- Always run `yarn lint:fix` before committing.
|
||||||
|
|
||||||
|
### Commit Conventions
|
||||||
|
|
||||||
|
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
|
||||||
|
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
|
||||||
|
|
||||||
|
### Agent Documentation
|
||||||
|
|
||||||
|
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
|
||||||
|
- After editing any source file, regenerate the root `AGENTS.md`:
|
||||||
|
```bash
|
||||||
|
yarn agents:generate
|
||||||
|
```
|
||||||
70
docs/agents.md/contributors/1_contributors.md
Normal file
70
docs/agents.md/contributors/1_contributors.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
|
||||||
|
|
||||||
|
To get started, follow the `Development Setup` section.
|
||||||
|
|
||||||
|
## Project Information
|
||||||
|
|
||||||
|
- **Tech Stack:**
|
||||||
|
- `Vue.js`: Framework
|
||||||
|
- `Linting`: ESLint
|
||||||
|
- `CSS`: SCSS should be used
|
||||||
|
- `TypeScript`: Primary language for logic.
|
||||||
|
- **Code Style and Standards:**
|
||||||
|
- `Language`: TypeScript is preferred for new code.
|
||||||
|
- `Vue.js`:
|
||||||
|
- Composition API components are preferred over Options API.
|
||||||
|
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
||||||
|
- Place source tag above template above style.
|
||||||
|
- style tag should contain `lang='scss' scoped`.
|
||||||
|
- `Linting`: Follow the ESLint configuration in the root.
|
||||||
|
|
||||||
|
- **File Structure:**
|
||||||
|
- `.github/`: CI/CD workflows and Renovate config.
|
||||||
|
- `docs/`: Documentation source for `AGENTS.md` generation.
|
||||||
|
- `scripts/`: Bash scripts for build, CI and doc generation.
|
||||||
|
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
|
||||||
|
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
|
||||||
|
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
|
||||||
|
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
|
||||||
|
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
|
||||||
|
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
|
||||||
|
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
|
||||||
|
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
|
||||||
|
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
|
||||||
|
- `l10n/`: Localization (`en-us.yaml`).
|
||||||
|
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
|
||||||
|
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
|
||||||
|
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
|
||||||
|
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
|
||||||
|
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
|
||||||
|
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
|
||||||
|
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
|
||||||
|
- `styles/`: Global SCSS files.
|
||||||
|
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
|
||||||
|
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Harvester UI Extension Development Guide
|
||||||
|
|
||||||
|
1. Backward Compatibility
|
||||||
|
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
|
||||||
|
|
||||||
|
2. Implementation Steps for New Features
|
||||||
|
To add a feature in a new release, follow these steps:
|
||||||
|
|
||||||
|
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
|
||||||
|
|
||||||
|
Check: Use the following getter to verify if the feature is enabled for the current version:
|
||||||
|
|
||||||
|
```
|
||||||
|
computed: {
|
||||||
|
newFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
Render: Use the result of the check to conditionally render the UI components.
|
||||||
|
|
||||||
3
docs/agents.md/contributors/2_e2e-tests.md
Normal file
3
docs/agents.md/contributors/2_e2e-tests.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## E2E Tests (Cypress)
|
||||||
|
|
||||||
|
See https://github.com/harvester/harvester-ui-tests
|
||||||
12
docs/agents.md/contributors/node-dependencies.md
Normal file
12
docs/agents.md/contributors/node-dependencies.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
## Node Dependencies
|
||||||
|
|
||||||
|
Dependencies are managed via `package.json` and `yarn`
|
||||||
|
|
||||||
|
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
|
||||||
|
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
|
||||||
|
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
|
||||||
|
|
||||||
|
|
||||||
|
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
|
||||||
|
For the Renovate config, see `.github/renovate.json`.
|
||||||
|
|
||||||
58
docs/agents.md/contributors/resolving_gh_issues.md
Normal file
58
docs/agents.md/contributors/resolving_gh_issues.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
## Milestone guidance
|
||||||
|
|
||||||
|
- All issues must first be resolved in the `main` branch
|
||||||
|
- If backports are needed they can be made via the backport bot
|
||||||
|
- pull requests
|
||||||
|
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
|
||||||
|
- All backported pull requests must link to a backported issue
|
||||||
|
|
||||||
|
|
||||||
|
## Creating a branch
|
||||||
|
|
||||||
|
### To resolve an issue
|
||||||
|
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
|
||||||
|
- `main` for the latest unreleased minor version
|
||||||
|
- `release-harvester-v.X` for release minor versions
|
||||||
|
- `release-harvester-v1.6`
|
||||||
|
- `release-harvester-v1.7`
|
||||||
|
- `release-harvester-v1.8`
|
||||||
|
- Ensure you have the latest of that branch `git pull --rebase`
|
||||||
|
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
|
||||||
|
|
||||||
|
## Creating a commit
|
||||||
|
|
||||||
|
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to ensure consistent and meaningful commit messages.
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
All commit messages must follow the conventional commit format:
|
||||||
|
```
|
||||||
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Types
|
||||||
|
- feat: New features
|
||||||
|
- fix: Bug fixes
|
||||||
|
- docs: Documentation changes
|
||||||
|
- style: Code style changes (formatting, missing semicolons, etc.)
|
||||||
|
- refactor: Code refactoring
|
||||||
|
- perf: Performance improvements
|
||||||
|
- test: Adding or updating tests
|
||||||
|
- build: Build system or external dependencies
|
||||||
|
- ci: CI/CD changes
|
||||||
|
- chore: Other changes that don't modify src or test files
|
||||||
|
- revert: Reverts a previous commit
|
||||||
|
- wip: Work in progress
|
||||||
|
- deps: Dependency updates
|
||||||
|
- security: Security fixes
|
||||||
|
|
||||||
|
## Creating a Pull Request
|
||||||
|
|
||||||
|
- Pull requests must come from forks
|
||||||
|
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
|
||||||
|
- A Pull Request will only be merged once
|
||||||
|
- ALL CI gates have passed
|
||||||
|
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR
|
||||||
5
docs/agents.md/personas/software_developer.md
Normal file
5
docs/agents.md/personas/software_developer.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
## Software Developer
|
||||||
|
|
||||||
|
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
|
||||||
|
- **Focus**: Writing clean, maintainable, and performant code.
|
||||||
|
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.
|
||||||
22
docs/agents.md/template_agents.md
Normal file
22
docs/agents.md/template_agents.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
|
||||||
|
|
||||||
|
|
||||||
|
# Project Overview
|
||||||
|
|
||||||
|
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the Rancher Dashboard.
|
||||||
|
|
||||||
|
```
|
||||||
|
Note: This extension is available starting from Rancher 2.10.0. Ensure your Rancher version is 2.10.0 or later to access Harvester integration.
|
||||||
|
```
|
||||||
|
|
||||||
|
# Personas
|
||||||
|
|
||||||
|
`<personas>`
|
||||||
|
|
||||||
|
# Agents
|
||||||
|
|
||||||
|
`<agents>`
|
||||||
|
|
||||||
|
# Contributors Guide
|
||||||
|
|
||||||
|
`<contributors>`
|
||||||
17
package.json
17
package.json
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.8.0-dev",
|
"version": "1.8.1-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=24.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.28.6",
|
"@babel/plugin-transform-class-static-block": "7.28.6",
|
||||||
"@rancher/shell": "3.0.9-rc.6",
|
"@rancher/shell": "3.0.12-rc.1",
|
||||||
"@vue-flow/background": "^1.3.0",
|
"@vue-flow/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,19 +21,19 @@
|
|||||||
"yaml": "^2.5.1"
|
"yaml": "^2.5.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "~20.19.0",
|
"@types/node": "25.6.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.15.11",
|
"follow-redirects": "1.16.0",
|
||||||
"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.3.3",
|
"node-forge": "1.4.0",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.15.0",
|
"qs": "6.15.1",
|
||||||
"roarr": "7.21.4",
|
"roarr": "7.21.4",
|
||||||
"semver": "7.7.4",
|
"semver": "7.7.4",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
@ -49,7 +49,8 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -7,8 +7,7 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
||||||
|
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
|
|||||||
104
pkg/harvester/components/settings/instance-manager-resources.vue
Normal file
104
pkg/harvester/components/settings/instance-manager-resources.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<script>
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterInstanceManagerResources',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
UnitInput,
|
||||||
|
Banner,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
value: '',
|
||||||
|
default: '{}'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const resources = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
resources,
|
||||||
|
parseError: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseJSON(string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(string);
|
||||||
|
} catch (e) {
|
||||||
|
this.parseError = this.t('harvester.setting.instanceManagerResources.parseError', { error: e.message });
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (!this.value) return;
|
||||||
|
|
||||||
|
const cpu = { ...this.resources?.cpu };
|
||||||
|
|
||||||
|
if (cpu.v1 !== null && cpu.v1 !== undefined) cpu.v1 = String(cpu.v1);
|
||||||
|
if (cpu.v2 !== null && cpu.v2 !== undefined) cpu.v2 = String(cpu.v2);
|
||||||
|
|
||||||
|
this.value.value = JSON.stringify({ ...this.resources, cpu });
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
if (this.value?.default) {
|
||||||
|
this.resources = this.parseJSON(this.value.default) || {};
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-if="parseError"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ parseError }}
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="resources.cpu.v1"
|
||||||
|
:label="t('harvester.setting.instanceManagerResources.v1')"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="resources.cpu.v2"
|
||||||
|
:label="t('harvester.setting.instanceManagerResources.v2')"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
371
pkg/harvester/components/settings/rwx-network.vue
Normal file
371
pkg/harvester/components/settings/rwx-network.vue
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { NETWORK_TYPE } from '../../config/types';
|
||||||
|
|
||||||
|
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||||
|
const SHARE_STORAGE_NETWORK = 'share-storage-network';
|
||||||
|
const NETWORK = 'network';
|
||||||
|
|
||||||
|
const DEFAULT_DEDICATED_NETWORK = {
|
||||||
|
vlan: '',
|
||||||
|
clusterNetwork: '',
|
||||||
|
range: '',
|
||||||
|
exclude: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RwxNetworkSetting',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
RadioGroup,
|
||||||
|
Banner,
|
||||||
|
ArrayList,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let enabled = false; // enabled / disabled options
|
||||||
|
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
|
||||||
|
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
let networkType = L2VLAN;
|
||||||
|
let exclude = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
|
||||||
|
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
|
||||||
|
|
||||||
|
if (parsedValue && typeof parsedValue === 'object') {
|
||||||
|
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
|
||||||
|
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
|
||||||
|
dedicatedNetwork = {
|
||||||
|
vlan: parsedNetwork.vlan || '',
|
||||||
|
clusterNetwork: parsedNetwork.clusterNetwork || '',
|
||||||
|
range: parsedNetwork.range || '',
|
||||||
|
};
|
||||||
|
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
|
||||||
|
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
enabled = false;
|
||||||
|
shareStorageNetwork = false;
|
||||||
|
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
shareStorageNetwork,
|
||||||
|
dedicatedNetwork,
|
||||||
|
networkType,
|
||||||
|
exclude,
|
||||||
|
defaultAddValue: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showDedicatedNetworkConfig() {
|
||||||
|
return this.enabled && !this.shareStorageNetwork;
|
||||||
|
},
|
||||||
|
|
||||||
|
showVlan() {
|
||||||
|
return this.networkType === L2VLAN;
|
||||||
|
},
|
||||||
|
|
||||||
|
networkTypes() {
|
||||||
|
return [L2VLAN, UNTAGGED];
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
|
||||||
|
|
||||||
|
return clusterNetworksOptions.map((network) => {
|
||||||
|
const disabled = !network.isReadyForStorageNetwork;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
|
||||||
|
value: network.id,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onUpdateEnabled() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.shareStorageNetwork = false;
|
||||||
|
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateNetworkType() {
|
||||||
|
if (this.shareStorageNetwork) {
|
||||||
|
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateDedicatedType(neu) {
|
||||||
|
this.dedicatedNetwork.clusterNetwork = '';
|
||||||
|
|
||||||
|
if (neu === L2VLAN) {
|
||||||
|
this.dedicatedNetwork.vlan = '';
|
||||||
|
} else {
|
||||||
|
delete this.dedicatedNetwork.vlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
inputVlan(neu) {
|
||||||
|
if (neu === '') {
|
||||||
|
this.dedicatedNetwork.vlan = '';
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = Number(neu);
|
||||||
|
|
||||||
|
if (newValue > 4094) {
|
||||||
|
this.dedicatedNetwork.vlan = 4094;
|
||||||
|
} else if (newValue < 1) {
|
||||||
|
this.dedicatedNetwork.vlan = 1;
|
||||||
|
} else {
|
||||||
|
this.dedicatedNetwork.vlan = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.enabled = false;
|
||||||
|
this.shareStorageNetwork = false;
|
||||||
|
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const value = { [SHARE_STORAGE_NETWORK]: false };
|
||||||
|
|
||||||
|
if (this.enabled && this.shareStorageNetwork) {
|
||||||
|
value[SHARE_STORAGE_NETWORK] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showDedicatedNetworkConfig) {
|
||||||
|
value[NETWORK] = {};
|
||||||
|
|
||||||
|
if (this.showVlan) {
|
||||||
|
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
|
||||||
|
value[NETWORK].range = this.dedicatedNetwork.range;
|
||||||
|
|
||||||
|
const excludeList = this.exclude.filter((ip) => ip);
|
||||||
|
|
||||||
|
if (Array.isArray(excludeList) && excludeList.length > 0) {
|
||||||
|
value[NETWORK].exclude = excludeList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.value = JSON.stringify(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
if (!this.showDedicatedNetworkConfig) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (this.showVlan && !this.dedicatedNetwork.vlan) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dedicatedNetwork.clusterNetwork) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dedicatedNetwork.range) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
|
||||||
|
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.exclude) {
|
||||||
|
const hasInvalidCIDR = this.exclude.find((cidr) => {
|
||||||
|
return cidr && !isValidCIDR(cidr);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasInvalidCIDR) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="mode">
|
||||||
|
<Banner color="warning">
|
||||||
|
<t
|
||||||
|
k="harvester.setting.rwxNetwork.warning"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="rwx-network-enable"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="onUpdateEnabled"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
v-if="enabled"
|
||||||
|
v-model:value="shareStorageNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
name="rwx-network-type"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
|
||||||
|
@update:value="onUpdateNetworkType"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="shareStorageNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<template v-if="showDedicatedNetworkConfig">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="networkType"
|
||||||
|
class="mb-20"
|
||||||
|
:options="networkTypes"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.fields.type')"
|
||||||
|
required
|
||||||
|
@update:value="onUpdateDedicatedType"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-if="showVlan"
|
||||||
|
v-model:value.number="dedicatedNetwork.vlan"
|
||||||
|
type="number"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
placeholder="e.g. 1 - 4094"
|
||||||
|
label-key="harvester.setting.storageNetwork.vlan"
|
||||||
|
@update:value="inputVlan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="dedicatedNetwork.clusterNetwork"
|
||||||
|
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="dedicatedNetwork.range"
|
||||||
|
class="mb-5"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
|
||||||
|
label-key="harvester.setting.storageNetwork.range.label"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="exclude"
|
||||||
|
:show-header="true"
|
||||||
|
:default-add-value="defaultAddValue"
|
||||||
|
:mode="mode"
|
||||||
|
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||||
|
class="mt-20"
|
||||||
|
@update:value="update"
|
||||||
|
>
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="box mb-10">
|
||||||
|
<div class="key">
|
||||||
|
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #columns="scope">
|
||||||
|
<div class="key">
|
||||||
|
<input
|
||||||
|
v-model="scope.row.value"
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -61,7 +61,11 @@ const FEATURE_FLAGS = {
|
|||||||
'v1.8.0': [
|
'v1.8.0': [
|
||||||
'hotplugCdRom',
|
'hotplugCdRom',
|
||||||
'supportBundleFileNameSetting',
|
'supportBundleFileNameSetting',
|
||||||
'clusterRegistrationTLSVerify'
|
'clusterRegistrationTLSVerify',
|
||||||
|
'vGPUAsPCIDevice',
|
||||||
|
'instanceManagerResourcesSetting',
|
||||||
|
'rwxNetworkSetting',
|
||||||
|
'createPVCWithDataVolume'
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -81,7 +81,6 @@ 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;
|
||||||
@ -168,7 +167,7 @@ export function init($plugin, store) {
|
|||||||
group: 'Root',
|
group: 'Root',
|
||||||
name: HCI.HOST,
|
name: HCI.HOST,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 399,
|
weight: 499,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.HOST }
|
params: { resource: HCI.HOST }
|
||||||
@ -200,7 +199,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
name: HCI.VM,
|
name: HCI.VM,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 299,
|
weight: 498,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VM }
|
params: { resource: HCI.VM }
|
||||||
@ -361,7 +360,7 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: PVC,
|
ifHaveType: PVC,
|
||||||
name: HCI.VOLUME,
|
name: HCI.VOLUME,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 199,
|
weight: 497,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VOLUME }
|
params: { resource: HCI.VOLUME }
|
||||||
@ -387,7 +386,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
name: HCI.IMAGE,
|
name: HCI.IMAGE,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 198,
|
weight: 496,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.IMAGE }
|
params: { resource: HCI.IMAGE }
|
||||||
@ -402,7 +401,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
name: 'projects-namespaces',
|
name: 'projects-namespaces',
|
||||||
weight: 98,
|
weight: 495,
|
||||||
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
@ -414,7 +413,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.namespace.label',
|
labelKey: 'harvester.namespace.label',
|
||||||
name: NAMESPACE,
|
name: NAMESPACE,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 89,
|
weight: 495,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: NAMESPACE }
|
params: { resource: NAMESPACE }
|
||||||
@ -592,9 +591,8 @@ export function init($plugin, store) {
|
|||||||
'backupAndSnapshot'
|
'backupAndSnapshot'
|
||||||
);
|
);
|
||||||
|
|
||||||
weightGroup('networks', 300, true);
|
weightGroup('networks', 494, true);
|
||||||
weightType(NAMESPACE, 299, true);
|
weightGroup('backupAndSnapshot', 493, true);
|
||||||
weightGroup('backupAndSnapshot', 289, true);
|
|
||||||
|
|
||||||
basicType(
|
basicType(
|
||||||
[
|
[
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export const HCI = {
|
|||||||
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
||||||
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
||||||
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
||||||
|
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
|
||||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||||
@ -79,4 +80,5 @@ export const HCI = {
|
|||||||
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
||||||
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
||||||
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
||||||
|
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export const HCI_SETTING = {
|
|||||||
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
||||||
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
||||||
STORAGE_NETWORK: 'storage-network',
|
STORAGE_NETWORK: 'storage-network',
|
||||||
|
RWX_NETWORK: 'rwx-network',
|
||||||
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
||||||
SSL_CERTIFICATES: 'ssl-certificates',
|
SSL_CERTIFICATES: 'ssl-certificates',
|
||||||
SSL_PARAMETERS: 'ssl-parameters',
|
SSL_PARAMETERS: 'ssl-parameters',
|
||||||
@ -39,7 +40,8 @@ export const HCI_SETTING = {
|
|||||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
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'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@ -80,6 +82,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||||
kind: 'custom', from: 'import', canReset: true
|
kind: 'custom', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
|
[HCI_SETTING.RWX_NETWORK]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
|
||||||
|
},
|
||||||
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_PARAMETERS]: {
|
[HCI_SETTING.SSL_PARAMETERS]: {
|
||||||
@ -122,6 +127,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
},
|
},
|
||||||
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
||||||
|
},
|
||||||
|
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
||||||
|
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -41,3 +41,8 @@ export const VMIMPORT_SOURCE_KINDS = {
|
|||||||
OPENSTACK: 'OpenstackSource',
|
OPENSTACK: 'OpenstackSource',
|
||||||
OVA: 'OvaSource',
|
OVA: 'OvaSource',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CDI_POPULATOR_KIND = {
|
||||||
|
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
||||||
|
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
||||||
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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: 'HarvesterEnablePciPassthrough',
|
name: 'HarvesterEnablePciPassthrough',
|
||||||
@ -34,16 +35,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
// isSingleProduct == this is a standalone Harvester cluster
|
const userName = getHarvesterUserName(this.$store.getters);
|
||||||
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];
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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',
|
||||||
@ -34,16 +35,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
// isSingleProduct == this is a standalone Harvester cluster
|
const userName = getHarvesterUserName(this.$store.getters);
|
||||||
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];
|
||||||
|
|||||||
183
pkg/harvester/dialog/HarvesterDataMigrationDialog.vue
Normal file
183
pkg/harvester/dialog/HarvesterDataMigrationDialog.vue
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { STORAGE_CLASS } from '@shell/config/types';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { sortBy } from '@shell/utils/sort';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterDataMigrationDialog',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton, Banner, Card, LabeledInput, LabeledSelect
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
this.storageClasses = await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
targetVolumeName: '',
|
||||||
|
targetStorageClassName: '',
|
||||||
|
errors: [],
|
||||||
|
storageClasses: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
storageClassOptions() {
|
||||||
|
return sortBy(
|
||||||
|
this.storageClasses
|
||||||
|
.filter((sc) => !isInternalStorageClass(sc.metadata?.name))
|
||||||
|
.map((sc) => ({
|
||||||
|
label: sc.metadata?.name,
|
||||||
|
value: sc.metadata?.name
|
||||||
|
})),
|
||||||
|
'label'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
disableSave() {
|
||||||
|
return !this.targetVolumeName || !this.targetStorageClassName;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.targetVolumeName = '';
|
||||||
|
this.targetStorageClassName = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async apply(buttonDone) {
|
||||||
|
if (!this.actionResource) {
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.targetVolumeName) {
|
||||||
|
const name = this.t('harvester.modal.dataMigration.fields.targetVolumeName.label');
|
||||||
|
|
||||||
|
this['errors'] = [this.t('validation.required', { key: name })];
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.targetStorageClassName) {
|
||||||
|
const name = this.t('harvester.modal.dataMigration.fields.targetStorageClassName.label');
|
||||||
|
|
||||||
|
this['errors'] = [this.t('validation.required', { key: name })];
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.actionResource.doAction('dataMigration', {
|
||||||
|
targetVolumeName: this.targetVolumeName,
|
||||||
|
targetStorageClassName: this.targetStorageClassName
|
||||||
|
}, {}, false);
|
||||||
|
|
||||||
|
buttonDone(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
|
||||||
|
this['errors'] = exceptionToErrorsArray(error);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.dataMigration.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="targetVolumeName"
|
||||||
|
:label="t('harvester.modal.dataMigration.fields.targetVolumeName.label')"
|
||||||
|
:placeholder="t('harvester.modal.dataMigration.fields.targetVolumeName.placeholder')"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="targetStorageClassName"
|
||||||
|
:label="t('harvester.modal.dataMigration.fields.targetStorageClassName.label')"
|
||||||
|
:placeholder="t('harvester.modal.dataMigration.fields.targetStorageClassName.placeholder')"
|
||||||
|
:options="storageClassOptions"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
#actions
|
||||||
|
class="actions"
|
||||||
|
>
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:disabled="disableSave"
|
||||||
|
@click="apply"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
239
pkg/harvester/dialog/HarvesterStorageMigrationDialog.vue
Normal file
239
pkg/harvester/dialog/HarvesterStorageMigrationDialog.vue
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { PVC } from '@shell/config/types';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { sortBy } from '@shell/utils/sort';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { parseVolumeClaimTemplates } from '@pkg/harvester/utils/vm';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterStorageMigrationDialog',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton, Banner, Card, LabeledSelect
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
sourceVolume: '',
|
||||||
|
targetVolume: '',
|
||||||
|
errors: [],
|
||||||
|
allPVCs: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
sourceVolumeOptions() {
|
||||||
|
const volumes = this.actionResource.spec?.template?.spec?.volumes || [];
|
||||||
|
|
||||||
|
return sortBy(
|
||||||
|
volumes
|
||||||
|
.map((v) => v.persistentVolumeClaim?.claimName)
|
||||||
|
.filter((name) => !!name)
|
||||||
|
.map((name) => ({
|
||||||
|
label: name,
|
||||||
|
value: name
|
||||||
|
})),
|
||||||
|
'label'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
namespacePVCs() {
|
||||||
|
return this.allPVCs.filter((pvc) => pvc.metadata.namespace === this.actionResource.metadata.namespace);
|
||||||
|
},
|
||||||
|
|
||||||
|
vmUsedVolumeNames() {
|
||||||
|
const allVMs = this.$store.getters['harvester/all'](HCI.VM) || [];
|
||||||
|
const names = new Set();
|
||||||
|
|
||||||
|
allVMs.forEach((vm) => {
|
||||||
|
// Collect volume names from spec.template.spec.volumes (both PVC and DataVolume references)
|
||||||
|
const volumes = vm.spec?.template?.spec?.volumes || [];
|
||||||
|
|
||||||
|
volumes.forEach((v) => {
|
||||||
|
const name = v.persistentVolumeClaim?.claimName || v.dataVolume?.name;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
names.add(`${ vm.metadata.namespace }/${ name }`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect volume names from volumeClaimTemplates annotation
|
||||||
|
const templates = parseVolumeClaimTemplates(vm);
|
||||||
|
|
||||||
|
templates.forEach((t) => {
|
||||||
|
if (t.metadata?.name) {
|
||||||
|
names.add(`${ vm.metadata.namespace }/${ t.metadata.name }`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return names;
|
||||||
|
},
|
||||||
|
|
||||||
|
targetVolumeOptions() {
|
||||||
|
return sortBy(
|
||||||
|
this.namespacePVCs
|
||||||
|
.filter((pvc) => {
|
||||||
|
// Exclude volumes used by any VM (via spec.volumes or volumeClaimTemplates)
|
||||||
|
if (this.vmUsedVolumeNames.has(`${ pvc.metadata.namespace }/${ pvc.metadata.name }`)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((pvc) => ({
|
||||||
|
label: pvc.metadata.name,
|
||||||
|
value: pvc.metadata.name
|
||||||
|
})),
|
||||||
|
'label'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
disableSave() {
|
||||||
|
return !this.sourceVolume || !this.targetVolume;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.sourceVolume = '';
|
||||||
|
this.targetVolume = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async apply(buttonDone) {
|
||||||
|
if (!this.actionResource) {
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.sourceVolume) {
|
||||||
|
const name = this.t('harvester.modal.storageMigration.fields.sourceVolume.label');
|
||||||
|
|
||||||
|
this['errors'] = [this.t('validation.required', { key: name })];
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.targetVolume) {
|
||||||
|
const name = this.t('harvester.modal.storageMigration.fields.targetVolume.label');
|
||||||
|
|
||||||
|
this['errors'] = [this.t('validation.required', { key: name })];
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.actionResource.doAction('storageMigration', {
|
||||||
|
sourceVolume: this.sourceVolume,
|
||||||
|
targetVolume: this.targetVolume
|
||||||
|
}, {}, false);
|
||||||
|
|
||||||
|
buttonDone(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
|
||||||
|
this['errors'] = exceptionToErrorsArray(error);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.storageMigration.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="sourceVolume"
|
||||||
|
:label="t('harvester.modal.storageMigration.fields.sourceVolume.label')"
|
||||||
|
:placeholder="t('harvester.modal.storageMigration.fields.sourceVolume.placeholder')"
|
||||||
|
:options="sourceVolumeOptions"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="targetVolume"
|
||||||
|
:label="t('harvester.modal.storageMigration.fields.targetVolume.label')"
|
||||||
|
:placeholder="t('harvester.modal.storageMigration.fields.targetVolume.placeholder')"
|
||||||
|
:options="targetVolumeOptions"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
#actions
|
||||||
|
class="actions"
|
||||||
|
>
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:disabled="disableSave"
|
||||||
|
@click="apply"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -71,21 +71,38 @@ 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 = [];
|
||||||
|
|
||||||
@ -283,20 +300,6 @@ 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;
|
||||||
|
|||||||
@ -9,8 +9,11 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
import Conditions from '@shell/components/form/Conditions';
|
import Conditions from '@shell/components/form/Conditions';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { get } from '@shell/utils/object';
|
import { clone, get } from '@shell/utils/object';
|
||||||
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
|
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { saferDump } from '@shell/utils/create-yaml';
|
import { saferDump } from '@shell/utils/create-yaml';
|
||||||
@ -33,6 +36,7 @@ export default {
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
Banner,
|
Banner,
|
||||||
|
Checkbox,
|
||||||
Tab,
|
Tab,
|
||||||
UnitInput,
|
UnitInput,
|
||||||
CruResource,
|
CruResource,
|
||||||
@ -90,14 +94,34 @@ export default {
|
|||||||
source,
|
source,
|
||||||
storage,
|
storage,
|
||||||
imageId,
|
imageId,
|
||||||
snapshots: [],
|
showAdvanced: false,
|
||||||
images: [],
|
createWithDataVolume: false,
|
||||||
|
snapshots: [],
|
||||||
|
images: [],
|
||||||
GIBIBYTE
|
GIBIBYTE
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.registerBeforeHook(this.willSave, 'willSave');
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
|
||||||
|
if (this.mode === _CREATE) {
|
||||||
|
const originalSaveYaml = this.value.saveYaml?.bind(this.value);
|
||||||
|
|
||||||
|
this.value.saveYaml = async(yaml) => {
|
||||||
|
if (this.createWithDataVolume && this.isBlank) {
|
||||||
|
const parsed = jsyaml.load(yaml);
|
||||||
|
const dvObj = { ...parsed, type: 'cdi.kubevirt.io.datavolume' };
|
||||||
|
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
|
||||||
|
|
||||||
|
await dataVolume.save();
|
||||||
|
|
||||||
|
return dataVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalSaveYaml(yaml);
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@ -135,6 +159,10 @@ export default {
|
|||||||
return Object.values(VOLUME_MODE);
|
return Object.values(VOLUME_MODE);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
accessModeOptions() {
|
||||||
|
return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
|
||||||
|
},
|
||||||
|
|
||||||
imageOption() {
|
imageOption() {
|
||||||
return sortBy(
|
return sortBy(
|
||||||
this.images
|
this.images
|
||||||
@ -275,6 +303,10 @@ export default {
|
|||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isCreatePVCWithDataVolumeFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
|
||||||
|
},
|
||||||
|
|
||||||
isResizeDisabled() {
|
isResizeDisabled() {
|
||||||
return (
|
return (
|
||||||
!this.isLHV2VolExpansionFeatureEnabled &&
|
!this.isLHV2VolExpansionFeatureEnabled &&
|
||||||
@ -341,6 +373,58 @@ export default {
|
|||||||
|
|
||||||
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
||||||
},
|
},
|
||||||
|
buildDataVolumeObj() {
|
||||||
|
const storage = {
|
||||||
|
storageClassName: this.value.spec.storageClassName,
|
||||||
|
resources: { requests: { storage: this.storage } },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.showAdvanced && this.value.spec.accessModes?.length > 0) {
|
||||||
|
storage.accessModes = this.value.spec.accessModes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showAdvanced && this.value.spec.volumeMode) {
|
||||||
|
storage.volumeMode = this.value.spec.volumeMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'cdi.kubevirt.io.datavolume',
|
||||||
|
apiVersion: 'cdi.kubevirt.io/v1beta1',
|
||||||
|
kind: 'DataVolume',
|
||||||
|
metadata: {
|
||||||
|
name: this.value.metadata.name,
|
||||||
|
namespace: this.value.metadata.namespace,
|
||||||
|
annotations: this.value.metadata.annotations || {},
|
||||||
|
labels: this.value.metadata.labels || {},
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
source: { blank: {} },
|
||||||
|
storage,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonDone) {
|
||||||
|
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
|
||||||
|
try {
|
||||||
|
this.update();
|
||||||
|
const dvObj = this.buildDataVolumeObj();
|
||||||
|
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
|
||||||
|
|
||||||
|
await dataVolume.save();
|
||||||
|
buttonDone(true);
|
||||||
|
this.done();
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
|
||||||
|
this['errors'] = exceptionToErrorsArray(error);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await CreateEditView.methods.save.call(this, buttonDone);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
willSave() {
|
willSave() {
|
||||||
this.update();
|
this.update();
|
||||||
},
|
},
|
||||||
@ -383,9 +467,17 @@ export default {
|
|||||||
this.update();
|
this.update();
|
||||||
},
|
},
|
||||||
generateYaml() {
|
generateYaml() {
|
||||||
const out = saferDump(this.value);
|
this.update();
|
||||||
|
|
||||||
return out;
|
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
|
||||||
|
return saferDump(this.buildDataVolumeObj());
|
||||||
|
}
|
||||||
|
|
||||||
|
const plain = clone(this.value);
|
||||||
|
|
||||||
|
delete plain.saveYaml;
|
||||||
|
|
||||||
|
return saferDump(plain);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -458,18 +550,6 @@ export default {
|
|||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-if="showVolumeMode"
|
|
||||||
v-model:value="value.spec.volumeMode"
|
|
||||||
:label="t('harvester.volume.volumeMode')"
|
|
||||||
:options="volumeModeOptions"
|
|
||||||
required
|
|
||||||
:disabled="!isCreate"
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UnitInput
|
<UnitInput
|
||||||
v-model:value="storage"
|
v-model:value="storage"
|
||||||
:label="t('harvester.volume.size')"
|
:label="t('harvester.volume.size')"
|
||||||
@ -490,6 +570,44 @@ export default {
|
|||||||
>
|
>
|
||||||
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<Checkbox
|
||||||
|
v-if="isCreate && isBlank && isCreatePVCWithDataVolumeFeatureEnabled"
|
||||||
|
v-model:value="createWithDataVolume"
|
||||||
|
:label="t('harvester.volume.createWithDataVolume')"
|
||||||
|
tooltip-key="harvester.volume.createWithDataVolumeTooltip"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
v-if="isCreate && isCreatePVCWithDataVolumeFeatureEnabled"
|
||||||
|
role="button"
|
||||||
|
class="hand"
|
||||||
|
@click="showAdvanced = !showAdvanced"
|
||||||
|
>
|
||||||
|
{{ showAdvanced ? t('harvester.volume.hideAdvanced') : t('harvester.volume.showAdvanced') }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="showAdvanced"
|
||||||
|
v-model:value="value.spec.accessModes"
|
||||||
|
:label="t('harvester.volume.accessModes')"
|
||||||
|
:options="accessModeOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20 mt-20"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="showAdvanced"
|
||||||
|
v-model:value="value.spec.volumeMode"
|
||||||
|
:label="t('harvester.volume.volumeMode')"
|
||||||
|
:options="volumeModeOptions"
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="!isCreate"
|
v-if="!isCreate"
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export default {
|
|||||||
const _hash = {
|
const _hash = {
|
||||||
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
|
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
|
||||||
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
|
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
|
||||||
|
srigpuovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE }),
|
||||||
};
|
};
|
||||||
|
|
||||||
await allHash(_hash);
|
await allHash(_hash);
|
||||||
@ -106,19 +107,32 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
parentSriovOptions() {
|
allSriovs() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const allSriovs = this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
|
|
||||||
|
|
||||||
return allSriovs.map((sriov) => {
|
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
|
||||||
return sriov.id;
|
},
|
||||||
});
|
allSriovGPUs() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
|
||||||
|
},
|
||||||
|
parentSriovOptions() {
|
||||||
|
return this.allSriovs.map((sriov) => sriov.id);
|
||||||
|
},
|
||||||
|
parentSriovGPUOptions() {
|
||||||
|
return this.allSriovGPUs.map((sriovgpu) => sriovgpu.id);
|
||||||
},
|
},
|
||||||
parentSriovLabel() {
|
parentSriovLabel() {
|
||||||
return HCI_ANNOTATIONS.PARENT_SRIOV;
|
return HCI_ANNOTATIONS.PARENT_SRIOV;
|
||||||
}
|
},
|
||||||
|
parentSriovGPULabel() {
|
||||||
|
return HCI_ANNOTATIONS.PARENT_SRIOV_GPU;
|
||||||
|
},
|
||||||
|
vGPUAsPCIDeviceEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
enableGroup(rows = []) {
|
enableGroup(rows = []) {
|
||||||
const row = rows[0];
|
const row = rows[0];
|
||||||
@ -206,6 +220,15 @@ export default {
|
|||||||
:rows="rows"
|
:rows="rows"
|
||||||
@change-rows="changeRows"
|
@change-rows="changeRows"
|
||||||
/>
|
/>
|
||||||
|
<FilterBySriov
|
||||||
|
v-if="vGPUAsPCIDeviceEnabled"
|
||||||
|
ref="filterByParentSRIOVGPU"
|
||||||
|
:parent-sriov-options="parentSriovGPUOptions"
|
||||||
|
:parent-sriov-label="parentSriovGPULabel"
|
||||||
|
:label="t('harvester.sriov.parentSriovGPU')"
|
||||||
|
:rows="rows"
|
||||||
|
@change-rows="changeRows"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ResourceTable>
|
</ResourceTable>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { set } from '@shell/utils/object';
|
|||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import DeviceList from './DeviceList';
|
import DeviceList from './DeviceList';
|
||||||
import CompatibilityMatrix from '../CompatibilityMatrix';
|
import CompatibilityMatrix from '../CompatibilityMatrix';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VirtualMachinePCIDevices',
|
name: 'VirtualMachinePCIDevices',
|
||||||
@ -15,7 +16,8 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
DeviceList,
|
DeviceList,
|
||||||
CompatibilityMatrix,
|
CompatibilityMatrix,
|
||||||
Banner
|
Banner,
|
||||||
|
MessageLink
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
@ -138,6 +140,13 @@ export default {
|
|||||||
return inUse;
|
return inUse;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toVGpuDevicesPage() {
|
||||||
|
return {
|
||||||
|
name: 'harvester-c-cluster-resource',
|
||||||
|
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
devicesByNode() {
|
devicesByNode() {
|
||||||
return this.enabledDevices?.reduce((acc, device) => {
|
return this.enabledDevices?.reduce((acc, device) => {
|
||||||
const nodeName = device.status?.nodeName;
|
const nodeName = device.status?.nodeName;
|
||||||
@ -232,7 +241,12 @@ export default {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.pci.howToUseDevice" />
|
<MessageLink
|
||||||
|
:to="toVGpuDevicesPage"
|
||||||
|
prefix-label="harvester.pci.howToUseDeviceInVMCreation.prefix"
|
||||||
|
middle-label="harvester.pci.howToUseDeviceInVMCreation.middle"
|
||||||
|
suffix-label="harvester.pci.howToUseDeviceInVMCreation.suffix"
|
||||||
|
/>
|
||||||
</Banner>
|
</Banner>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="selectedDevices.length > 0"
|
v-if="selectedDevices.length > 0"
|
||||||
|
|||||||
@ -211,6 +211,10 @@ export default {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vGPUAsPCIDeviceEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
||||||
|
},
|
||||||
usbPassthroughEnabled() {
|
usbPassthroughEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
||||||
},
|
},
|
||||||
@ -740,7 +744,7 @@ export default {
|
|||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
v-if="enabledSriovgpu"
|
v-if="enabledSriovgpu && !vGPUAsPCIDeviceEnabled"
|
||||||
:label="t('harvester.tab.vGpuDevices')"
|
:label="t('harvester.tab.vGpuDevices')"
|
||||||
name="vGpuDevices"
|
name="vGpuDevices"
|
||||||
:weight="-6"
|
:weight="-6"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<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: {
|
||||||
@ -25,7 +26,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
const user = this.$store.getters['auth/v3User'];
|
const user = getHarvesterUser(this.$store.getters);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
harvesterSettings: [],
|
harvesterSettings: [],
|
||||||
|
|||||||
@ -123,6 +123,24 @@ harvester:
|
|||||||
namespace: Namespace
|
namespace: Namespace
|
||||||
message:
|
message:
|
||||||
success: 'Image { name } created successfully.'
|
success: 'Image { name } created successfully.'
|
||||||
|
storageMigration:
|
||||||
|
title: Storage Migration
|
||||||
|
fields:
|
||||||
|
sourceVolume:
|
||||||
|
label: Source Volume
|
||||||
|
placeholder: Select a source volume
|
||||||
|
targetVolume:
|
||||||
|
label: Target Volume
|
||||||
|
placeholder: Select a target volume
|
||||||
|
dataMigration:
|
||||||
|
title: Data Migration
|
||||||
|
fields:
|
||||||
|
targetVolumeName:
|
||||||
|
label: Target Volume Name
|
||||||
|
placeholder: Enter a target volume name
|
||||||
|
targetStorageClassName:
|
||||||
|
label: Target Storage Class
|
||||||
|
placeholder: Select a storage class
|
||||||
migration:
|
migration:
|
||||||
failedMessage: Latest migration failed!
|
failedMessage: Latest migration failed!
|
||||||
title: Migration
|
title: Migration
|
||||||
@ -232,6 +250,9 @@ harvester:
|
|||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
|
storageMigration: Storage Migration
|
||||||
|
cancelStorageMigration: Cancel Storage Migration
|
||||||
|
dataMigration: Data Migration
|
||||||
createTemplate: Generate Template
|
createTemplate: Generate Template
|
||||||
enableMaintenance: Enable Maintenance Mode
|
enableMaintenance: Enable Maintenance Mode
|
||||||
disableMaintenance: Disable Maintenance Mode
|
disableMaintenance: Disable Maintenance Mode
|
||||||
@ -355,7 +376,14 @@ 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.'
|
||||||
howToUseDevice: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.'
|
howToUseDeviceInVMCreation:
|
||||||
|
prefix: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine. <br>For vGPU devices, please enable them on the'
|
||||||
|
middle: vGPU Devices
|
||||||
|
suffix: page first.
|
||||||
|
howToUseDevice:
|
||||||
|
prefix: 'Select the device in the table to enable PCI passthrough. <br>For vGPU devices, please enable them on the'
|
||||||
|
middle: vGPU Devices
|
||||||
|
suffix: page.
|
||||||
deviceInTheSameHost: 'You can only select devices on the same host.'
|
deviceInTheSameHost: 'You can only select devices on the same host.'
|
||||||
oldFormatDevices:
|
oldFormatDevices:
|
||||||
help: |-
|
help: |-
|
||||||
@ -425,7 +453,7 @@ harvester:
|
|||||||
volume:
|
volume:
|
||||||
upperType: Volume name
|
upperType: Volume name
|
||||||
lowerType: volume name
|
lowerType: volume name
|
||||||
needImageOrExisting: 'At least an image volume or an existing root-disk volume is required!'
|
needAtLeastOneBootable: 'At least one bootable volume is required!'
|
||||||
image:
|
image:
|
||||||
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
@ -898,6 +926,11 @@ harvester:
|
|||||||
conditions: Conditions
|
conditions: Conditions
|
||||||
size: Size
|
size: Size
|
||||||
volumeMode: Volume Mode
|
volumeMode: Volume Mode
|
||||||
|
accessModes: Access Modes
|
||||||
|
createWithDataVolume: Create with DataVolume
|
||||||
|
createWithDataVolumeTooltip: Create Volume with Kubevirt/Containerized Data Importer way. It can fill accessMode/volumeMode automatically.
|
||||||
|
showAdvanced: Show Advanced Options
|
||||||
|
hideAdvanced: Hide Advanced Options
|
||||||
source: Source
|
source: Source
|
||||||
kind: Kind
|
kind: Kind
|
||||||
sourceOptions:
|
sourceOptions:
|
||||||
@ -1266,6 +1299,11 @@ harvester:
|
|||||||
addIp: Add Exclude IP
|
addIp: Add Exclude IP
|
||||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||||
|
rwxNetwork:
|
||||||
|
warning: 'WARNING: <br/> Any change to rwx-network requires longhorn RWX volumes detached before applying this setting.<br/>Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
|
shareStorageNetwork: Share Storage Network
|
||||||
|
dedicatedRwxNetwork: Dedicated RWX Network
|
||||||
|
shareStorageNetworkWarning: The rwx-network is governed by storage-network, and changes here won't take effect until share-storage-network is set to false.
|
||||||
vmForceDeletionPolicy:
|
vmForceDeletionPolicy:
|
||||||
period: Period
|
period: Period
|
||||||
vmMigrationTimeout: VM Migration Timeout
|
vmMigrationTimeout: VM Migration Timeout
|
||||||
@ -1353,14 +1391,18 @@ harvester:
|
|||||||
clusterRegistrationUrl:
|
clusterRegistrationUrl:
|
||||||
url: URL
|
url: URL
|
||||||
insecureSkipTLSVerify: Insecure Skip TLS Verify
|
insecureSkipTLSVerify: Insecure Skip TLS Verify
|
||||||
tip:
|
tip:
|
||||||
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure SKip TLS Verify", you must provide custom CA certificates using the
|
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure Skip TLS Verify", you must provide custom CA certificates using the
|
||||||
middle: 'additional-ca'
|
middle: 'additional-ca'
|
||||||
suffix: setting.
|
suffix: setting.
|
||||||
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
|
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
|
||||||
ntpServers:
|
ntpServers:
|
||||||
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
|
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
|
||||||
isDuplicate: There are duplicate NTP server configurations.
|
isDuplicate: There are duplicate NTP server configurations.
|
||||||
|
instanceManagerResources:
|
||||||
|
parseError: "Failed to parse configuration: {error}"
|
||||||
|
v1: "V1 Data Engine"
|
||||||
|
v2: "V2 Data Engine"
|
||||||
kubevirtMigration:
|
kubevirtMigration:
|
||||||
parseError: "Failed to parse configuration: {error}"
|
parseError: "Failed to parse configuration: {error}"
|
||||||
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
||||||
@ -1832,7 +1874,8 @@ harvester:
|
|||||||
numVFs: Number Of Virtual Functions
|
numVFs: Number Of Virtual Functions
|
||||||
vfAddresses: Virtual Functions Addresses
|
vfAddresses: Virtual Functions Addresses
|
||||||
showMore: Show More
|
showMore: Show More
|
||||||
parentSriov: Filter By Parent SR-IOV
|
parentSriov: Filter By Parent SR-IOV Netork Device
|
||||||
|
parentSriovGPU: Filter By Parent SR-IOV GPU Device
|
||||||
|
|
||||||
sriovgpu:
|
sriovgpu:
|
||||||
label: SR-IOV GPU Devices
|
label: SR-IOV GPU Devices
|
||||||
@ -1979,6 +2022,7 @@ advancedSettings:
|
|||||||
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
||||||
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
||||||
'harv-storage-network': 'Longhorn storage-network setting.'
|
'harv-storage-network': 'Longhorn storage-network setting.'
|
||||||
|
'harv-rwx-network': 'Configure RWX network behavior for shared or dedicated storage network usage.'
|
||||||
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||||
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
||||||
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
||||||
@ -1994,6 +2038,7 @@ advancedSettings:
|
|||||||
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
||||||
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
'harv-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.'
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
@ -2186,12 +2231,12 @@ typeLabel:
|
|||||||
other { VMware Sources }
|
other { VMware Sources }
|
||||||
}
|
}
|
||||||
migration.harvesterhci.io.ovasource: |-
|
migration.harvesterhci.io.ovasource: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { OVA Source }
|
one { OVA Source }
|
||||||
other { OVA Sources }
|
other { OVA Sources }
|
||||||
}
|
}
|
||||||
migration.harvesterhci.io.virtualmachineimport: |-
|
migration.harvesterhci.io.virtualmachineimport: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Virtual Machine Import }
|
one { Virtual Machine Import }
|
||||||
other { Virtual Machine Imports }
|
other { Virtual Machine Imports }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,17 @@ 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);
|
||||||
@ -85,11 +96,23 @@ export default {
|
|||||||
{{ t('harvester.pci.noPCIPermission') }}
|
{{ t('harvester.pci.noPCIPermission') }}
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
<DeviceList
|
<div v-else-if="hasSchema && enabledPCI">
|
||||||
v-else-if="hasSchema && enabledPCI"
|
<Banner
|
||||||
:devices="rows"
|
v-if="vGPUAsPCIDeviceEnabled"
|
||||||
:schema="schema"
|
color="info"
|
||||||
/>
|
>
|
||||||
|
<MessageLink
|
||||||
|
:to="toVGpuDevicesPage"
|
||||||
|
prefix-label="harvester.pci.howToUseDevice.prefix"
|
||||||
|
middle-label="harvester.pci.howToUseDevice.middle"
|
||||||
|
suffix-label="harvester.pci.howToUseDevice.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<DeviceList
|
||||||
|
:devices="rows"
|
||||||
|
:schema="schema"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Banner color="warning">
|
<Banner color="warning">
|
||||||
<MessageLink
|
<MessageLink
|
||||||
|
|||||||
@ -70,8 +70,13 @@ export default {
|
|||||||
return schema;
|
return schema;
|
||||||
},
|
},
|
||||||
filterRows() {
|
filterRows() {
|
||||||
// we only show the non golden image PVCs in the list
|
return this.rows.filter((pvc) => {
|
||||||
return this.rows.filter((pvc) => !pvc?.isGoldenImageVolume);
|
if (pvc?.isGoldenImageVolume || pvc?.isCDIPopulatorVolume) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
headers() {
|
headers() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -707,18 +707,22 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
needVolume(R) {
|
needVolumeRelatedInfo(R) {
|
||||||
if (R.image === EMPTY_IMAGE) {
|
// return [needVolume, needVolumeClaimTemplate]
|
||||||
return false;
|
if (R.source === SOURCE_TYPE.CONTAINER) {
|
||||||
|
return [true, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if (R.source === SOURCE_TYPE.IMAGE && R.image === EMPTY_IMAGE) {
|
||||||
|
return [false, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [true, true];
|
||||||
},
|
},
|
||||||
|
|
||||||
parseDiskRows(disk) {
|
parseDiskRows(disk) {
|
||||||
const disks = [];
|
const disks = [];
|
||||||
const volumes = [];
|
const volumes = [];
|
||||||
const diskNameLabels = [];
|
|
||||||
const volumeClaimTemplates = [];
|
const volumeClaimTemplates = [];
|
||||||
|
|
||||||
disk.forEach( (R, index) => {
|
disk.forEach( (R, index) => {
|
||||||
@ -726,14 +730,18 @@ export default {
|
|||||||
|
|
||||||
disks.push(_disk);
|
disks.push(_disk);
|
||||||
|
|
||||||
if (this.needVolume(R)) {
|
const prefixName = this.value.metadata?.name || '';
|
||||||
const prefixName = this.value.metadata?.name || '';
|
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
||||||
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
const [needVolume, needVolumeClaimTemplate] = this.needVolumeRelatedInfo(R);
|
||||||
|
|
||||||
|
if (needVolume) {
|
||||||
const _volume = this.parseVolume(R, dataVolumeName);
|
const _volume = this.parseVolume(R, dataVolumeName);
|
||||||
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
|
|
||||||
|
|
||||||
volumes.push(_volume);
|
volumes.push(_volume);
|
||||||
diskNameLabels.push(dataVolumeName);
|
}
|
||||||
|
if (needVolumeClaimTemplate) {
|
||||||
|
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
|
||||||
|
|
||||||
volumeClaimTemplates.push(_dataVolumeTemplate);
|
volumeClaimTemplates.push(_dataVolumeTemplate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
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 { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import { getHarvesterUserName } from '../utils/auth';
|
||||||
|
|
||||||
const STATUS_DISPLAY = {
|
const STATUS_DISPLAY = {
|
||||||
enabled: {
|
enabled: {
|
||||||
@ -32,7 +34,7 @@ export default class PCIDevice extends SteveModel {
|
|||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
action: 'enablePassthroughBulk',
|
action: 'enablePassthroughBulk',
|
||||||
enabled: !this.isEnabling,
|
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,
|
||||||
@ -41,7 +43,7 @@ export default class PCIDevice extends SteveModel {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'disablePassthrough',
|
action: 'disablePassthrough',
|
||||||
enabled: this.isEnabling && this.claimedByMe,
|
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,
|
||||||
@ -52,6 +54,14 @@ export default class PCIDevice extends SteveModel {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isvGPUDevice() {
|
||||||
|
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!this.metadata?.labels?.[HCI_ANNOTATIONS.PARENT_SRIOV_GPU];
|
||||||
|
}
|
||||||
|
|
||||||
get canYaml() {
|
get canYaml() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -87,15 +97,8 @@ export default class PCIDevice extends SteveModel {
|
|||||||
if (!this.passthroughClaim) {
|
if (!this.passthroughClaim) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const isSingleProduct = this.$rootGetters['isSingleProduct'];
|
|
||||||
let userName = 'admin';
|
|
||||||
|
|
||||||
// if this is imported Harvester, there may be users other than admin
|
const userName = getHarvesterUserName(this.$rootGetters);
|
||||||
if (!isSingleProduct) {
|
|
||||||
const user = this.$rootGetters['auth/v3User'];
|
|
||||||
|
|
||||||
userName = user?.username || user?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.claimedBy === userName;
|
return this.claimedBy === userName;
|
||||||
}
|
}
|
||||||
@ -176,6 +179,10 @@ export default class PCIDevice extends SteveModel {
|
|||||||
return this.status?.description;
|
return this.status?.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get vGPUAsPCIDeviceFeatureEnabled() {
|
||||||
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
||||||
|
}
|
||||||
|
|
||||||
showDetachWarning() {
|
showDetachWarning() {
|
||||||
this.$dispatch('growl/warning', {
|
this.$dispatch('growl/warning', {
|
||||||
title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'),
|
title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
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: {
|
||||||
@ -87,15 +88,8 @@ export default class USBDevice extends SteveModel {
|
|||||||
if (!this.passthroughClaim) {
|
if (!this.passthroughClaim) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const isSingleProduct = this.$rootGetters['isSingleProduct'];
|
|
||||||
let userName = 'admin';
|
|
||||||
|
|
||||||
// if this is imported Harvester, there may be users other than admin
|
const userName = getHarvesterUserName(this.$rootGetters);
|
||||||
if (!isSingleProduct) {
|
|
||||||
const user = this.$rootGetters['auth/v3User'];
|
|
||||||
|
|
||||||
userName = user?.username || user?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.claimedBy === userName;
|
return this.claimedBy === userName;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
|
|||||||
import { HCI, VOLUME_SNAPSHOT } from '../../types';
|
import { HCI, VOLUME_SNAPSHOT } from '../../types';
|
||||||
import HarvesterResource from '../harvester';
|
import HarvesterResource from '../harvester';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
||||||
|
import { CDI_POPULATOR_KIND } from '../../config/types';
|
||||||
import { LVM_DRIVER } from './storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from './storage.k8s.io.storageclass';
|
||||||
|
|
||||||
const DEGRADED_ERRORS = ['replica scheduling failed', 'precheck new replica failed'];
|
const DEGRADED_ERRORS = ['replica scheduling failed', 'precheck new replica failed'];
|
||||||
@ -44,7 +45,7 @@ export default class HciPv extends HarvesterResource {
|
|||||||
const exportImageAction = {
|
const exportImageAction = {
|
||||||
action: 'exportImage',
|
action: 'exportImage',
|
||||||
enabled: this.hasAction('export') && !this.isEncrypted,
|
enabled: this.hasAction('export') && !this.isEncrypted,
|
||||||
icon: 'icon icon-copy',
|
icon: 'icon icon-external-link',
|
||||||
label: this.t('harvester.action.exportImage')
|
label: this.t('harvester.action.exportImage')
|
||||||
};
|
};
|
||||||
const takeSnapshotAction = {
|
const takeSnapshotAction = {
|
||||||
@ -77,10 +78,23 @@ export default class HciPv extends HarvesterResource {
|
|||||||
icon: 'icon icon-backup',
|
icon: 'icon icon-backup',
|
||||||
label: this.t('harvester.action.cancelExpand')
|
label: this.t('harvester.action.cancelExpand')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: 'dataMigration',
|
||||||
|
enabled: this.hasAction('dataMigration') && this.createPVCWithDataVolumeFeatureEnabled,
|
||||||
|
icon: 'icon icon-copy',
|
||||||
|
label: this.t('harvester.action.dataMigration')
|
||||||
|
},
|
||||||
...out
|
...out
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataMigration(resources = this) {
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources,
|
||||||
|
component: 'HarvesterDataMigrationDialog'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
exportImage(resources = this) {
|
exportImage(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
@ -339,10 +353,20 @@ export default class HciPv extends HarvesterResource {
|
|||||||
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true';
|
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isCDIPopulatorVolume() {
|
||||||
|
const kind = this?.metadata?.annotations?.[HCI_ANNOTATIONS.CDI_POPULATOR_KIND];
|
||||||
|
|
||||||
|
return kind === CDI_POPULATOR_KIND.VOLUME_IMPORT_SOURCE || kind === CDI_POPULATOR_KIND.VOLUME_CLONE_SOURCE;
|
||||||
|
}
|
||||||
|
|
||||||
get thirdPartyStorageFeatureEnabled() {
|
get thirdPartyStorageFeatureEnabled() {
|
||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get createPVCWithDataVolumeFeatureEnabled() {
|
||||||
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
|
||||||
|
}
|
||||||
|
|
||||||
get resourceExternalLink() {
|
get resourceExternalLink() {
|
||||||
const host = window.location.host;
|
const host = window.location.host;
|
||||||
const { params } = this.currentRoute();
|
const { params } = this.currentRoute();
|
||||||
|
|||||||
@ -199,6 +199,18 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
icon: 'icon icon-close',
|
icon: 'icon icon-close',
|
||||||
label: this.t('harvester.action.abortMigration')
|
label: this.t('harvester.action.abortMigration')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: 'storageMigration',
|
||||||
|
enabled: !!this.actions?.storageMigration,
|
||||||
|
icon: 'icon icon-copy',
|
||||||
|
label: this.t('harvester.action.storageMigration')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'cancelStorageMigration',
|
||||||
|
enabled: !!this.actions?.cancelStorageMigration,
|
||||||
|
icon: 'icon icon-close',
|
||||||
|
label: this.t('harvester.action.cancelStorageMigration')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: 'addHotplugVolume',
|
action: 'addHotplugVolume',
|
||||||
enabled: !!this.actions?.addVolume,
|
enabled: !!this.actions?.addVolume,
|
||||||
@ -368,6 +380,13 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storageMigration(resources = this) {
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources,
|
||||||
|
component: 'HarvesterStorageMigrationDialog'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
backupVM(resources = this) {
|
backupVM(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
@ -520,6 +539,10 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
this.doActionGrowl('abortMigration', {});
|
this.doActionGrowl('abortMigration', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelStorageMigration() {
|
||||||
|
this.doActionGrowl('cancelStorageMigration', {});
|
||||||
|
}
|
||||||
|
|
||||||
createTemplate(resources = this) {
|
createTemplate(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
@ -770,11 +793,11 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isPending() {
|
get isPending() {
|
||||||
if (this &&
|
if ((this &&
|
||||||
!this.isVMExpectedRunning &&
|
!this.isVMExpectedRunning &&
|
||||||
this.isVMCreated &&
|
this.isVMCreated &&
|
||||||
this.vmi?.status?.phase === VMIPhase.Pending
|
this.vmi?.status?.phase === VMIPhase.Pending
|
||||||
) {
|
) || (this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS] === 'cloning')) {
|
||||||
return { status: VMIPhase.Pending };
|
return { status: VMIPhase.Pending };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.8.0-dev",
|
"version": "1.8.1-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
@ -17,16 +17,16 @@
|
|||||||
"nuxt": "./node_modules/.bin/nuxt"
|
"nuxt": "./node_modules/.bin/nuxt"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=24.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~5.0.0",
|
"@vue/cli-plugin-babel": "~5.0.9",
|
||||||
"@vue/cli-service": "~5.0.0",
|
"@vue/cli-service": "~5.0.9",
|
||||||
"@vue/cli-plugin-typescript": "~5.0.0"
|
"@vue/cli-plugin-typescript": "~5.0.9"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not dead"
|
"not dead"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
31
pkg/harvester/utils/auth.js
Normal file
31
pkg/harvester/utils/auth.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Resolve the Harvester username from Vuex getters.
|
||||||
|
*
|
||||||
|
* Works with both `this.$store.getters` (in components) and
|
||||||
|
* `this.$rootGetters` (in Steve models).
|
||||||
|
*
|
||||||
|
* - In single-product (standalone Harvester) mode, always returns the
|
||||||
|
* default username (`admin`).
|
||||||
|
* - Otherwise, falls back to the authenticated user's `username` or `id`.
|
||||||
|
*/
|
||||||
|
export function getHarvesterUserName(getters, defaultUserName = 'admin') {
|
||||||
|
const isSingleProduct = getters?.['isSingleProduct'];
|
||||||
|
|
||||||
|
if (isSingleProduct) {
|
||||||
|
return defaultUserName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = getHarvesterUser(getters);
|
||||||
|
|
||||||
|
return user?.username || user?.id || defaultUserName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the authenticated user object from Vuex getters.
|
||||||
|
*
|
||||||
|
* Works with both `this.$store.getters` (in components) and
|
||||||
|
* `this.$rootGetters` (in Steve models).
|
||||||
|
*/
|
||||||
|
export function getHarvesterUser(getters) {
|
||||||
|
return getters?.['auth/user'];
|
||||||
|
}
|
||||||
@ -69,7 +69,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
|
|||||||
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
|
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
|
||||||
});
|
});
|
||||||
|
|
||||||
let requiredVolume = false;
|
let hasBootableVolume = false;
|
||||||
|
|
||||||
_volumes.forEach((V, idx) => {
|
_volumes.forEach((V, idx) => {
|
||||||
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
|
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
|
||||||
@ -77,7 +77,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
|
|||||||
const prefix = V.name || idx + 1;
|
const prefix = V.name || idx + 1;
|
||||||
|
|
||||||
if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) {
|
if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) {
|
||||||
requiredVolume = true;
|
hasBootableVolume = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
|
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
|
||||||
@ -137,10 +137,10 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* At least one volume must be create. (Verify only when create.)
|
* At least one bootable volume must be provided. (Verify only when create.)
|
||||||
*/
|
*/
|
||||||
if ((!requiredVolume || _volumes.length === 0) && !value.links) {
|
if (!hasBootableVolume && !value.links) {
|
||||||
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needImageOrExisting'));
|
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needAtLeastOneBootable'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
|
|||||||
40
scripts/bump_version.sh
Executable file
40
scripts/bump_version.sh
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
# Usage: update package.json and pkg/harvester/package.json to desired version
|
||||||
|
# Example: ./scripts/bump_version.sh v1.8.0-rc3
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "Usage: $0 <version>"
|
||||||
|
echo "Example: $0 v1.8.0-rc3"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strip leading 'v' if present
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
FILES=(
|
||||||
|
"$ROOT_DIR/package.json"
|
||||||
|
"$ROOT_DIR/pkg/harvester/package.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
for FILE in "${FILES[@]}"; do
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "File not found: $FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use node to update version in-place while preserving formatting
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const raw = fs.readFileSync('$FILE', 'utf8');
|
||||||
|
const updated = raw.replace(/\"version\":\s*\"[^\"]*\"/, '\"version\": \"$VERSION\"');
|
||||||
|
fs.writeFileSync('$FILE', updated);
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "Updated $FILE -> $VERSION"
|
||||||
|
done
|
||||||
44
scripts/generate-agent-and-persona-mds.sh
Executable file
44
scripts/generate-agent-and-persona-mds.sh
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Get the directory of the script
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
# Project root is one level up from scripts/
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# Source files
|
||||||
|
TEMPLATE_FILE="${PROJECT_ROOT}/docs/agents.md/template_agents.md"
|
||||||
|
AGENTS_DIR="${PROJECT_ROOT}/docs/agents.md/agents"
|
||||||
|
CONTRIBUTORS_DIR="${PROJECT_ROOT}/docs/agents.md/contributors"
|
||||||
|
PERSONAS_DIR="${PROJECT_ROOT}/docs/agents.md/personas"
|
||||||
|
|
||||||
|
# Destination file
|
||||||
|
OUTPUT_FILE="${PROJECT_ROOT}/AGENTS.md"
|
||||||
|
|
||||||
|
insert_directory_contents() {
|
||||||
|
local dir="$1"
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
for file in "$dir"/*; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
cat "$file"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Generating ${OUTPUT_FILE}..."
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
if [[ "$line" == *"<agents>"* ]]; then
|
||||||
|
insert_directory_contents "${AGENTS_DIR}"
|
||||||
|
elif [[ "$line" == *"<contributors>"* ]]; then
|
||||||
|
insert_directory_contents "${CONTRIBUTORS_DIR}"
|
||||||
|
elif [[ "$line" == *"<personas>"* ]]; then
|
||||||
|
insert_directory_contents "${PERSONAS_DIR}"
|
||||||
|
else
|
||||||
|
echo "$line"
|
||||||
|
fi
|
||||||
|
done < "${TEMPLATE_FILE}" > "${OUTPUT_FILE}"
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
Loading…
x
Reference in New Issue
Block a user