mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-05-15 07:21:48 +00:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af52df0ba0 | ||
|
|
e5a1929ac5 | ||
|
|
9de065a5c9 | ||
|
|
cd933bdbf8 | ||
|
|
5fe642a42d | ||
|
|
18c66083ab | ||
|
|
d291a35754 | ||
|
|
5cc8b4c301 | ||
|
|
032700293c | ||
|
|
8cb793e7ad | ||
|
|
2c45b71d1f | ||
|
|
5a301dcf55 | ||
|
|
1a92265d03 | ||
|
|
8f65915bad | ||
|
|
67bb6dfbd5 | ||
|
|
e74428d951 | ||
|
|
629f7df6b9 | ||
|
|
b7119d5c4c | ||
|
|
6fdd1e3954 | ||
|
|
7d0f33f31d | ||
|
|
9ce95daf76 | ||
|
|
ce72232bc3 | ||
|
|
afc0e0f531 | ||
|
|
e941cc9a90 | ||
|
|
9961523d08 | ||
|
|
c4d1018388 | ||
|
|
be64329776 | ||
|
|
35411ed87a | ||
|
|
15eb0f07f7 | ||
|
|
fb78f24fdd | ||
|
|
81ad827829 | ||
|
|
6dd9b33336 | ||
|
|
1f9e9b336b | ||
|
|
c5b4f6cd1e | ||
|
|
4ce35ce075 | ||
|
|
27c26bd782 | ||
|
|
9d698b1230 | ||
|
|
566e79eda5 | ||
|
|
42ddcfc1fe | ||
|
|
ad3decf71f | ||
|
|
8083a41df0 | ||
|
|
46b860260a | ||
|
|
62801b3b13 | ||
|
|
161e3bbd97 | ||
|
|
97e93dba0b | ||
|
|
9a8a709e56 | ||
|
|
d1949641a7 | ||
|
|
9c9f59c939 | ||
|
|
ccc14c7fb9 | ||
|
|
2ba471907e | ||
|
|
5aea476f64 | ||
|
|
519c7d9f1f | ||
|
|
a9c392c13f | ||
|
|
888ec7a50f | ||
|
|
a2486a7d38 | ||
|
|
df3d249923 | ||
|
|
23344e0c07 |
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:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- name: Setup Nodejs with yarn caching
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
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
|
||||
17
.github/renovate.json
vendored
17
.github/renovate.json
vendored
@ -39,11 +39,20 @@
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["patch", "digest"],
|
||||
"automerge": true,
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"automerge": false,
|
||||
"minimumReleaseAge": "7 days",
|
||||
"groupName": "patch digest dependencies",
|
||||
"labels": ["patch-update", "automerge"]
|
||||
"groupName": "patch dependencies",
|
||||
"labels": ["patch-update"],
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["digest", "pinDigest"],
|
||||
"automerge": false,
|
||||
"groupName": "digest dependencies",
|
||||
"labels": ["digest-update"],
|
||||
"schedule": ["on the first day of the month"],
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
39
.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@b70f0bdf12a03e5e3f33e4f92ccb6c89deb3ebd9 # 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:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
- 'release-harvester-v*'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "PR Management Add Labels Collect Data"
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
add-require-backport-label:
|
||||
if: github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.base.ref == 'main'
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Download PR data artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
name: pr-backport-label-data
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Fetch release branches and PR labels
|
||||
id: fetch_info
|
||||
- name: Load PR data
|
||||
run: |
|
||||
cat pr-backport-label-data.env >> $GITHUB_ENV
|
||||
|
||||
- name: Add require-backport label (main branch PRs)
|
||||
if: env.PR_BASE_REF == 'main'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
repo="${{ github.repository }}"
|
||||
pr_number=${{ github.event.pull_request.number }}
|
||||
pr_number="$PR_NUMBER"
|
||||
|
||||
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
||||
|
||||
if [[ -z "$release_branches" ]]; then
|
||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No release branches found, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@ -44,51 +48,36 @@ jobs:
|
||||
|
||||
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
||||
if echo "$tags" | grep -Fxq "$release_tag"; then
|
||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Release $release_tag already published, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
label="require backport/v${version}"
|
||||
echo "should_label=true" >> "$GITHUB_OUTPUT"
|
||||
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
|
||||
|
||||
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
||||
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
|
||||
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Add label if needed
|
||||
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
|
||||
if echo "$pr_labels" | grep -Fxq "$label"; then
|
||||
echo "Label '$label' already present, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Adding label: $label"
|
||||
gh pr edit "$pr_number" --repo "$repo" --add-label "$label"
|
||||
|
||||
- name: Add backport label (release branch PRs opened by Mergify)
|
||||
if: startsWith(env.PR_BASE_REF, 'release-harvester-v')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
|
||||
set -euo pipefail
|
||||
|
||||
add-backport-label:
|
||||
if: github.event.pull_request.draft == false &&
|
||||
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check conditions for backport label
|
||||
id: check
|
||||
run: |
|
||||
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
|
||||
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
|
||||
IS_MERGIFY=$(echo "$PR_USER_LOGIN" | grep -iq 'mergify' && echo true || echo false)
|
||||
|
||||
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
|
||||
if [[ "$IS_MERGIFY" != "true" ]]; then
|
||||
echo "PR author is not Mergify, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
- name: Add label if needed
|
||||
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
|
||||
version="${TARGET_BRANCH#release-harvester-v}"
|
||||
version="${PR_BASE_REF#release-harvester-v}"
|
||||
label="backport/v${version}"
|
||||
echo "Adding label $label"
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--add-label "$label"
|
||||
echo "Adding label: $label"
|
||||
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$label"
|
||||
|
||||
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:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "PR Management Request Backport via Mergify Collect Data"
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
comment-backport:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Download PR data artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
name: pr-backport-request-data
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Load PR data
|
||||
run: |
|
||||
cat pr-backport-request-data.env >> $GITHUB_ENV
|
||||
|
||||
- name: Post Mergify backport command
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
pr_number=${{ github.event.pull_request.number }}
|
||||
pr_number="$PR_NUMBER"
|
||||
repo="${{ github.repository }}"
|
||||
|
||||
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
||||
labels=$(echo "$labels_json" | jq -r '.[] // empty')
|
||||
labels=$(echo "$PR_LABELS" | tr ',' '\n')
|
||||
|
||||
echo "Labels on PR: $labels"
|
||||
|
||||
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
||||
|
||||
if [[ -z "$matches" ]]; then
|
||||
echo "No back‑port labels found — skipping."
|
||||
echo "No back-port labels found — skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
10
.github/workflows/build-extension-catalog.yml
vendored
10
.github/workflows/build-extension-catalog.yml
vendored
@ -55,14 +55,8 @@ jobs:
|
||||
with:
|
||||
version: v3.8.0
|
||||
|
||||
- name: Setup Nodejs with yarn caching
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Setup Nodejs with yarn install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build and push UI image
|
||||
run: |
|
||||
|
||||
@ -64,6 +64,12 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Node from .nvmrc
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
corepack enable
|
||||
|
||||
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
- name: Setup Nodejs and yarn install
|
||||
uses: ./.github/actions/setup
|
||||
- name: Set PR label
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||
echo "PR_LABEL = $PR_LABEL"
|
||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
||||
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
|
||||
|
||||
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",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0-dev",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=24.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-class-static-block": "7.28.6",
|
||||
"@rancher/shell": "3.0.9-rc.6",
|
||||
"@rancher/shell": "3.0.12-rc.1",
|
||||
"@vue-flow/background": "^1.3.0",
|
||||
"@vue-flow/controls": "^1.1.1",
|
||||
"@vue-flow/core": "^1.33.5",
|
||||
@ -21,19 +21,19 @@
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "~20.19.0",
|
||||
"@types/node": "25.6.0",
|
||||
"cronstrue": "2.59.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.10",
|
||||
"follow-redirects": "1.15.11",
|
||||
"follow-redirects": "1.16.0",
|
||||
"glob": "7.2.3",
|
||||
"glob-parent": "6.0.2",
|
||||
"json5": "2.2.3",
|
||||
"@types/lodash": "4.17.24",
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.3.3",
|
||||
"node-forge": "1.4.0",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.15.0",
|
||||
"qs": "6.15.1",
|
||||
"roarr": "7.21.4",
|
||||
"semver": "7.7.4",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
@ -49,7 +49,8 @@
|
||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||
"commitlint": "commitlint --edit",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky",
|
||||
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/load": "^19.8.1",
|
||||
|
||||
@ -122,7 +122,7 @@ export default {
|
||||
return this.$store.getters['currentCluster'].isLocal;
|
||||
},
|
||||
canEditClusterMembers() {
|
||||
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,196 @@
|
||||
<script>
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterClusterPodSecurityStandard',
|
||||
|
||||
components: {
|
||||
RadioGroup,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
|
||||
},
|
||||
|
||||
data() {
|
||||
let enabled = false;
|
||||
let whitelistedNamespaces = [];
|
||||
let privilegedNamespaces = [];
|
||||
let restrictedNamespaces = [];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(this.value.value || this.value.default || '{}');
|
||||
|
||||
enabled = !!parsed.enabled;
|
||||
whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
||||
privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
||||
restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
||||
} catch (e) {
|
||||
enabled = false;
|
||||
whitelistedNamespaces = [];
|
||||
privilegedNamespaces = [];
|
||||
restrictedNamespaces = [];
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
whitelistedNamespaces,
|
||||
privilegedNamespaces,
|
||||
restrictedNamespaces,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
enabledOptions() {
|
||||
return [
|
||||
{ label: this.t('generic.enabled'), value: true },
|
||||
{ label: this.t('generic.disabled'), value: false },
|
||||
];
|
||||
},
|
||||
|
||||
allNamespaces() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/all`](NAMESPACE).filter((ns) => !ns.isSystem).map((ns) => ns.id);
|
||||
},
|
||||
|
||||
whitelistedOptions() {
|
||||
const excluded = new Set([...this.privilegedNamespaces, ...this.restrictedNamespaces]);
|
||||
|
||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||
},
|
||||
|
||||
privilegedOptions() {
|
||||
const excluded = new Set([...this.whitelistedNamespaces, ...this.restrictedNamespaces]);
|
||||
|
||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||
},
|
||||
|
||||
restrictedOptions() {
|
||||
const excluded = new Set([...this.whitelistedNamespaces, ...this.privilegedNamespaces]);
|
||||
|
||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
useDefault() {
|
||||
try {
|
||||
const parsed = JSON.parse(this.value.default || '{}');
|
||||
|
||||
this.enabled = !!parsed.enabled;
|
||||
this.whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
||||
this.privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
||||
this.restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
||||
} catch (e) {
|
||||
this.enabled = false;
|
||||
this.whitelistedNamespaces = [];
|
||||
this.privilegedNamespaces = [];
|
||||
this.restrictedNamespaces = [];
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
|
||||
onUpdateEnabled() {
|
||||
if (!this.enabled) {
|
||||
this.whitelistedNamespaces = [];
|
||||
this.privilegedNamespaces = [];
|
||||
this.restrictedNamespaces = [];
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
|
||||
updateWhitelisted(selected) {
|
||||
this.whitelistedNamespaces = selected;
|
||||
this.save();
|
||||
},
|
||||
|
||||
updatePrivileged(selected) {
|
||||
this.privilegedNamespaces = selected;
|
||||
this.save();
|
||||
},
|
||||
|
||||
updateRestricted(selected) {
|
||||
this.restrictedNamespaces = selected;
|
||||
this.save();
|
||||
},
|
||||
|
||||
save() {
|
||||
this.value.value = JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
whitelistedNamespacesList: this.whitelistedNamespaces.join(','),
|
||||
privilegedNamespacesList: this.privilegedNamespaces.join(','),
|
||||
restrictedNamespacesList: this.restrictedNamespaces.join(','),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model:value="enabled"
|
||||
name="enabled"
|
||||
:options="enabledOptions"
|
||||
@update:value="onUpdateEnabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="enabled">
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="whitelistedNamespaces"
|
||||
:label="t('harvester.setting.clusterPodSecurityStandard.whitelistedNamespaces.label')"
|
||||
:options="whitelistedOptions"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
@update:value="updateWhitelisted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="privilegedNamespaces"
|
||||
:label="t('harvester.setting.clusterPodSecurityStandard.privilegedNamespaces.label')"
|
||||
:options="privilegedOptions"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
@update:value="updatePrivileged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="restrictedNamespaces"
|
||||
:label="t('harvester.setting.clusterPodSecurityStandard.restrictedNamespaces.label')"
|
||||
:options="restrictedOptions"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
@update:value="updateRestricted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -65,8 +65,11 @@ const FEATURE_FLAGS = {
|
||||
'vGPUAsPCIDevice',
|
||||
'instanceManagerResourcesSetting',
|
||||
'rwxNetworkSetting',
|
||||
'createPVCWithDataVolume'
|
||||
'createPVCWithDataVolume',
|
||||
'clusterPodSecurityStandardSetting'
|
||||
],
|
||||
'v1.8.1': [],
|
||||
'v1.9.0': [],
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
|
||||
@ -81,7 +81,6 @@ export function init($plugin, store) {
|
||||
configureType,
|
||||
virtualType,
|
||||
weightGroup,
|
||||
weightType,
|
||||
} = $plugin.DSL(store, PRODUCT_NAME);
|
||||
|
||||
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
|
||||
@ -168,7 +167,7 @@ export function init($plugin, store) {
|
||||
group: 'Root',
|
||||
name: HCI.HOST,
|
||||
namespaced: true,
|
||||
weight: 399,
|
||||
weight: 499,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.HOST }
|
||||
@ -200,7 +199,7 @@ export function init($plugin, store) {
|
||||
group: 'root',
|
||||
name: HCI.VM,
|
||||
namespaced: true,
|
||||
weight: 299,
|
||||
weight: 498,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VM }
|
||||
@ -361,7 +360,7 @@ export function init($plugin, store) {
|
||||
ifHaveType: PVC,
|
||||
name: HCI.VOLUME,
|
||||
namespaced: true,
|
||||
weight: 199,
|
||||
weight: 497,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VOLUME }
|
||||
@ -387,7 +386,7 @@ export function init($plugin, store) {
|
||||
group: 'root',
|
||||
name: HCI.IMAGE,
|
||||
namespaced: true,
|
||||
weight: 198,
|
||||
weight: 496,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.IMAGE }
|
||||
@ -402,7 +401,7 @@ export function init($plugin, store) {
|
||||
group: 'root',
|
||||
namespaced: true,
|
||||
name: 'projects-namespaces',
|
||||
weight: 98,
|
||||
weight: 495,
|
||||
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
||||
exact: true,
|
||||
});
|
||||
@ -414,7 +413,7 @@ export function init($plugin, store) {
|
||||
labelKey: 'harvester.namespace.label',
|
||||
name: NAMESPACE,
|
||||
namespaced: true,
|
||||
weight: 89,
|
||||
weight: 495,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: NAMESPACE }
|
||||
@ -592,9 +591,8 @@ export function init($plugin, store) {
|
||||
'backupAndSnapshot'
|
||||
);
|
||||
|
||||
weightGroup('networks', 300, true);
|
||||
weightType(NAMESPACE, 299, true);
|
||||
weightGroup('backupAndSnapshot', 289, true);
|
||||
weightGroup('networks', 494, true);
|
||||
weightGroup('backupAndSnapshot', 493, true);
|
||||
|
||||
basicType(
|
||||
[
|
||||
@ -688,7 +686,7 @@ export function init($plugin, store) {
|
||||
},
|
||||
resource: NETWORK_ATTACHMENT,
|
||||
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
||||
resourceEdit: HCI.NETWORK_ATTACHMENT
|
||||
resourceEdit: HCI.NETWORK_ATTACHMENT,
|
||||
});
|
||||
|
||||
virtualType({
|
||||
|
||||
@ -41,7 +41,8 @@ export const HCI_SETTING = {
|
||||
RANCHER_CLUSTER: 'rancher-cluster',
|
||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
||||
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
|
||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
|
||||
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
|
||||
};
|
||||
|
||||
export const HCI_ALLOWED_SETTINGS = {
|
||||
@ -130,6 +131,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
},
|
||||
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
||||
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
||||
},
|
||||
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
|
||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
154
pkg/harvester/dialog/HarvesterEnableNvidiaDriverToolkit.vue
Normal file
154
pkg/harvester/dialog/HarvesterEnableNvidiaDriverToolkit.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<script>
|
||||
import merge from 'lodash/merge';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Card } from '@components/Card';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
|
||||
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEnableNvidiaDriverToolkit',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
LabeledInput,
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const addon = this.resources[0];
|
||||
let valuesContentJson;
|
||||
|
||||
try {
|
||||
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(addon.spec.valuesContent));
|
||||
} catch (e) {
|
||||
valuesContentJson = { ...DEFAULT_VALUE };
|
||||
}
|
||||
|
||||
return { valuesContentJson };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
buttonDisabled() {
|
||||
const { image, driverLocation } = this.valuesContentJson;
|
||||
|
||||
return !(image?.repository || '').trim() || !(image?.tag || '').trim() || !(driverLocation || '').trim();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async enable(buttonCb) {
|
||||
const addon = this.resources[0];
|
||||
|
||||
try {
|
||||
addon.spec.valuesContent = jsyaml.dump(this.valuesContentJson);
|
||||
addon.spec.enabled = true;
|
||||
await addon.save();
|
||||
buttonCb(true);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
addon.spec.enabled = false;
|
||||
this.$store.dispatch('growl/fromError', {
|
||||
title: this.t('generic.notification.title.error', { name: escapeHtml(addon.metadata.name) }),
|
||||
err,
|
||||
}, { root: true });
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.addons.nvidiaDriverToolkit.enable.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="body">
|
||||
<div class="row mb-15">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.image.repository"
|
||||
:required="true"
|
||||
:label="t('harvester.addons.nvidiaDriverToolkit.image.repository')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.image.tag"
|
||||
:required="true"
|
||||
:label="t('harvester.addons.nvidiaDriverToolkit.image.tag')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-15">
|
||||
<div class="col span-12">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.driverLocation"
|
||||
:required="true"
|
||||
:label="t('harvester.addons.nvidiaDriverToolkit.driver.location')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="buttons actions">
|
||||
<button
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="enable"
|
||||
:disabled="buttonDisabled"
|
||||
@click="enable"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -1,15 +1,12 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { NODE } from '@shell/config/types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
emits: ['close'],
|
||||
@ -62,28 +59,46 @@ export default {
|
||||
return this.resources[0];
|
||||
},
|
||||
|
||||
vmi() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
|
||||
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
|
||||
anyCpuPinning() {
|
||||
return this.resources.some((r) => r.isCpuPinning);
|
||||
},
|
||||
|
||||
return resource;
|
||||
vmsByNode() {
|
||||
const groups = {};
|
||||
|
||||
for (const r of this.resources) {
|
||||
const node = r.nodeName || '';
|
||||
const name = r.nameDisplay || r.name || r.id;
|
||||
|
||||
if (!groups[node]) {
|
||||
groups[node] = [];
|
||||
}
|
||||
groups[node].push(name);
|
||||
}
|
||||
|
||||
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
|
||||
},
|
||||
|
||||
cpuPinningAlertMessage() {
|
||||
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
||||
},
|
||||
|
||||
allVmsOnTargetNode() {
|
||||
if (!this.nodeName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.resources.every((r) => r.nodeName === this.nodeName);
|
||||
},
|
||||
|
||||
nodeNameList() {
|
||||
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||
|
||||
return nodes.filter((n) => {
|
||||
const isNotSelfNode = !!this.availableNodes.includes(n.id);
|
||||
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
||||
const isCpuPinning = this.actionResource?.isCpuPinning;
|
||||
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||
|
||||
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
|
||||
return isNotWitnessNode && matchingCpuManagerConfig;
|
||||
}).map((n) => {
|
||||
let label = n?.metadata?.name;
|
||||
const value = n?.metadata?.name;
|
||||
@ -126,7 +141,29 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
||||
// Filter out VMs already running on the selected node
|
||||
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
|
||||
|
||||
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
||||
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
|
||||
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
||||
|
||||
const failedMigrations = results
|
||||
.map((result, index) => ({ resource: toMigrate[index], result }))
|
||||
.filter(({ result }) => result.status === 'rejected');
|
||||
|
||||
if (failedMigrations.length) {
|
||||
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
|
||||
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
|
||||
const error = result.reason?.data || result.reason;
|
||||
const messages = exceptionToErrorsArray(error);
|
||||
|
||||
return messages.map((message) => `${ vmName }: ${ message }`);
|
||||
});
|
||||
buttonDone(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
buttonDone(true);
|
||||
this.close();
|
||||
@ -146,17 +183,35 @@ export default {
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.migration.title') }}
|
||||
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Banner
|
||||
v-if="actionResource?.isCpuPinning"
|
||||
v-if="anyCpuPinning"
|
||||
color="warning"
|
||||
:label="cpuPinningAlertMessage"
|
||||
/>
|
||||
<p>
|
||||
{{ t('harvester.modal.migration.selectedVMs') }}
|
||||
</p>
|
||||
<ul class="vm-list">
|
||||
<li
|
||||
v-for="group in vmsByNode"
|
||||
:key="group.node"
|
||||
>
|
||||
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
|
||||
<span
|
||||
v-if="nodeName && group.node === nodeName"
|
||||
class="already-on-target"
|
||||
>
|
||||
({{ t('harvester.modal.migration.alreadyOnTarget') }})
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<LabeledSelect
|
||||
v-model:value="nodeName"
|
||||
class="mt-15"
|
||||
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||
:options="nodeNameList"
|
||||
@ -183,7 +238,7 @@ export default {
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:disabled="!nodeName"
|
||||
:disabled="!nodeName || allVmsOnTargetNode"
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
@ -201,4 +256,16 @@ export default {
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.already-on-target {
|
||||
color: var(--warning);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.vm-list {
|
||||
list-style: disc;
|
||||
padding-left: 1.5em;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -71,21 +71,38 @@ export default {
|
||||
},
|
||||
|
||||
data() {
|
||||
let provisioner = `${ this.value.provisioner || LONGHORN_DRIVER }`;
|
||||
|
||||
if (provisioner === LONGHORN_DRIVER) {
|
||||
provisioner = `${ provisioner }_${ this.value.provisionerVersion || LONGHORN_VERSION_V1 }`;
|
||||
}
|
||||
|
||||
return {
|
||||
provisioner,
|
||||
volumeGroupDialog: null,
|
||||
randomStr: randomStr(10).toLowerCase(),
|
||||
isOpen: false
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
provisioner: {
|
||||
get() {
|
||||
let provisioner = `${ this.value?.provisioner || LONGHORN_DRIVER }`;
|
||||
|
||||
if (provisioner === LONGHORN_DRIVER) {
|
||||
provisioner = `${ provisioner }_${ this.value?.provisionerVersion || LONGHORN_VERSION_V1 }`;
|
||||
}
|
||||
|
||||
return provisioner;
|
||||
},
|
||||
set(value) {
|
||||
this.randomStr = randomStr(10).toLowerCase();
|
||||
const [provisioner, provisionerVersion] = (value || '').split('_');
|
||||
|
||||
this.value.provisioner = provisioner;
|
||||
|
||||
if (provisioner === LONGHORN_DRIVER) {
|
||||
this.value.provisionerVersion = provisionerVersion || LONGHORN_VERSION_V1;
|
||||
} else {
|
||||
this.value.provisionerVersion = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
provisioners() {
|
||||
const out = [];
|
||||
|
||||
@ -283,20 +300,6 @@ export default {
|
||||
},
|
||||
|
||||
watch: {
|
||||
provisioner(value) {
|
||||
this.randomStr = randomStr(10).toLowerCase();
|
||||
|
||||
const [provisioner, provisionerVersion] = value?.split('_');
|
||||
|
||||
this.value.provisioner = provisioner;
|
||||
|
||||
if (provisioner === LONGHORN_DRIVER) {
|
||||
this.value.provisionerVersion = provisionerVersion || LONGHORN_VERSION_V1;
|
||||
} else {
|
||||
this.value.provisionerVersion = undefined;
|
||||
}
|
||||
},
|
||||
|
||||
'value.lvmVolumeGroup'(neu) {
|
||||
if (neu === _NEW) {
|
||||
this.value.lvmVolumeGroup = null;
|
||||
|
||||
@ -8,6 +8,7 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
|
||||
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { getLoginAwareErrors } from '../utils/error';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditKeypair',
|
||||
@ -63,6 +64,14 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
normalizedErrors() {
|
||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
||||
|
||||
return getLoginAwareErrors(this.errors, message);
|
||||
}
|
||||
},
|
||||
|
||||
methods: { onKeySelected: createOnSelected('publicKey') },
|
||||
};
|
||||
</script>
|
||||
@ -72,10 +81,9 @@ export default {
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:errors="normalizedErrors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<div class="header mb-20">
|
||||
<FileSelector
|
||||
|
||||
@ -10,6 +10,7 @@ import { _VIEW } from '@shell/config/query-params';
|
||||
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import { HCI } from '../../types';
|
||||
import { getLoginAwareErrors } from '../../utils/error';
|
||||
|
||||
const _NEW = '_NEW';
|
||||
|
||||
@ -214,7 +215,9 @@ export default {
|
||||
buttonCb(true);
|
||||
this.cancel();
|
||||
} catch (err) {
|
||||
this.errors = [err.message];
|
||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
||||
|
||||
this.errors = getLoginAwareErrors(err, message);
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
|
||||
@ -64,6 +64,12 @@ export default {
|
||||
value: 'status.productID',
|
||||
sort: ['status.productID', 'status.vendorID']
|
||||
},
|
||||
{
|
||||
name: 'classType',
|
||||
labelKey: 'harvester.usb.classType',
|
||||
value: 'status.classType',
|
||||
sort: ['status.classType']
|
||||
},
|
||||
];
|
||||
|
||||
if (!isSingleProduct) {
|
||||
|
||||
@ -144,6 +144,10 @@ harvester:
|
||||
migration:
|
||||
failedMessage: Latest migration failed!
|
||||
title: Migration
|
||||
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
|
||||
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
|
||||
unknownNode: (unknown node)
|
||||
alreadyOnTarget: Already on Target
|
||||
fields:
|
||||
nodeName:
|
||||
label: Target Node
|
||||
@ -248,6 +252,7 @@ harvester:
|
||||
suspendSchedule: Suspend
|
||||
restoreExistingVM: Replace Existing
|
||||
migrate: Migrate
|
||||
vmMigrate: Virtual Machine Migration
|
||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||
abortMigration: Abort Migration
|
||||
storageMigration: Storage Migration
|
||||
@ -298,6 +303,7 @@ harvester:
|
||||
phase: Phase
|
||||
attachedVM: Attached Virtual Machine
|
||||
cpuManager: CPU Manager
|
||||
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
|
||||
fingerprint: Fingerprint
|
||||
value: Value
|
||||
actions: Actions
|
||||
@ -726,6 +732,7 @@ harvester:
|
||||
other {Start}
|
||||
} Now
|
||||
createSSHKey: Create a New...
|
||||
genericLoginError: Authentication failed. Please re-log in and try again.
|
||||
installAgent: Install guest agent
|
||||
enableUsb: Enable USB Tablet
|
||||
advancedOptions:
|
||||
@ -1285,6 +1292,13 @@ harvester:
|
||||
rancherCluster:
|
||||
kubeConfig: Rancher KubeConfig
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
||||
clusterPodSecurityStandard:
|
||||
whitelistedNamespaces:
|
||||
label: 'Whitelisted Namespaces'
|
||||
privilegedNamespaces:
|
||||
label: 'Privileged Namespaces'
|
||||
restrictedNamespaces:
|
||||
label: 'Restricted Namespaces'
|
||||
storageNetwork:
|
||||
range:
|
||||
placeholder: e.g. 172.16.0.0/24
|
||||
@ -1764,6 +1778,8 @@ harvester:
|
||||
repository: Image Repository
|
||||
driver:
|
||||
location: Driver Location
|
||||
enable:
|
||||
title: Enable NVIDIA Driver Toolkit
|
||||
parsingSpecError:
|
||||
The field 'spec.valuesContent' has invalid format.
|
||||
usbController:
|
||||
@ -1955,6 +1971,7 @@ harvester:
|
||||
title: Cannot Disable Passthrough
|
||||
message: Please detach the device from the VM and save it first before disabling passthrough.
|
||||
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
||||
classType: Class Type
|
||||
|
||||
harvesterVlanConfigMigrateDialog:
|
||||
targetClusterNetwork:
|
||||
@ -2039,6 +2056,7 @@ advancedSettings:
|
||||
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
||||
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
||||
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
|
||||
|
||||
typeLabel:
|
||||
kubevirt.io.virtualmachine: |-
|
||||
@ -2161,11 +2179,7 @@ typeLabel:
|
||||
one { PCI Device }
|
||||
other { PCI Devices }
|
||||
}
|
||||
persistentvolumeclaim: |-
|
||||
{count, plural,
|
||||
one { Volume }
|
||||
other { Volumes }
|
||||
}
|
||||
|
||||
network.harvesterhci.io.clusternetwork: |-
|
||||
{count, plural,
|
||||
one { Cluster Network }
|
||||
|
||||
@ -3,7 +3,6 @@ import { Banner } from '@components/Banner';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
|
||||
|
||||
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
||||
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
@ -113,6 +112,7 @@ export default {
|
||||
value: 'connectivity',
|
||||
labelKey: 'tableHeaders.routeConnectivity',
|
||||
formatter: 'NetworkRouteConnectivity',
|
||||
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
|
||||
formatterOpts: { arbitrary: true },
|
||||
width: 130,
|
||||
},
|
||||
|
||||
@ -12,6 +12,11 @@ import { HCI } from '../types';
|
||||
import HarvesterVmState from '../formatters/HarvesterVmState';
|
||||
import ConsoleBar from '../components/VMConsoleBar';
|
||||
|
||||
const ENCRYPTED_VOLUME_TOOLTIP_KEYS = {
|
||||
all: 'harvester.virtualMachine.volume.lockTooltip.all',
|
||||
partial: 'harvester.virtualMachine.volume.lockTooltip.partial',
|
||||
};
|
||||
|
||||
export const VM_HEADERS = [
|
||||
STATE,
|
||||
{
|
||||
@ -163,6 +168,12 @@ export default {
|
||||
*/
|
||||
hasBackUpRestoreInProgress() {
|
||||
return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
|
||||
},
|
||||
|
||||
vmRestartRequiredNames() {
|
||||
return this.allVMs
|
||||
.filter((vm) => vm.isRestartRequired)
|
||||
.map((vm) => vm.metadata.name);
|
||||
}
|
||||
},
|
||||
|
||||
@ -181,53 +192,35 @@ export default {
|
||||
},
|
||||
|
||||
watch: {
|
||||
allVMs: {
|
||||
handler(neu) {
|
||||
const vmNames = [];
|
||||
vmRestartRequiredNames(vmNames) {
|
||||
const count = vmNames.length;
|
||||
|
||||
neu.forEach((vm) => {
|
||||
if (vm.isRestartRequired) {
|
||||
vmNames.push(vm.metadata.name);
|
||||
}
|
||||
});
|
||||
const count = vmNames.length;
|
||||
if (count === 0 && this.restartNotificationDisplayed) {
|
||||
this.restartNotificationDisplayed = false;
|
||||
|
||||
if ( count === 0 && this.restartNotificationDisplayed) {
|
||||
this.restartNotificationDisplayed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
if (count > 0) {
|
||||
// clear old notification before showing new one
|
||||
if (this.restartNotificationDisplayed) {
|
||||
this.$store.dispatch('growl/clear');
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
// clear old notification before showing new one
|
||||
if (this.restartNotificationDisplayed) {
|
||||
this.$store.dispatch('growl/clear');
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0 && vmNames.length > 0) {
|
||||
this.$store.dispatch('growl/warning', {
|
||||
title: this.t('harvester.notification.restartRequired.title', { count }),
|
||||
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
||||
timeout: 10000,
|
||||
}, { root: true });
|
||||
this.restartNotificationDisplayed = true;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
this.$store.dispatch('growl/warning', {
|
||||
title: this.t('harvester.notification.restartRequired.title', { count }),
|
||||
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
||||
timeout: 10000,
|
||||
}, { root: true });
|
||||
this.restartNotificationDisplayed = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
lockIconTooltipMessage(row) {
|
||||
const message = '';
|
||||
const key = ENCRYPTED_VOLUME_TOOLTIP_KEYS[row.encryptedVolumeType];
|
||||
|
||||
if (row.encryptedVolumeType === 'all') {
|
||||
return this.t('harvester.virtualMachine.volume.lockTooltip.all');
|
||||
} else if (row.encryptedVolumeType === 'partial') {
|
||||
return this.t('harvester.virtualMachine.volume.lockTooltip.partial');
|
||||
}
|
||||
|
||||
return message;
|
||||
return key ? this.t(key) : '';
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -267,7 +260,7 @@ export default {
|
||||
>
|
||||
{{ scope.row.metadata.name }}
|
||||
<i
|
||||
v-if="lockIconTooltipMessage(scope.row)"
|
||||
v-if="scope.row.encryptedVolumeType !== 'none'"
|
||||
v-tooltip="lockIconTooltipMessage(scope.row)"
|
||||
class="icon icon-lock"
|
||||
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
|
||||
|
||||
@ -30,11 +30,12 @@ const STATUS_DISPLAY = {
|
||||
export default class PCIDevice extends SteveModel {
|
||||
get _availableActions() {
|
||||
const out = super._availableActions;
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
out.push(
|
||||
{
|
||||
action: 'enablePassthroughBulk',
|
||||
enabled: !this.isEnabling && !this.isvGPUDevice,
|
||||
enabled: !this.isEnabling && !this.isvGPUDevice && canUpdate,
|
||||
icon: 'icon icon-fw icon-dot',
|
||||
label: 'Enable Passthrough',
|
||||
bulkable: true,
|
||||
@ -43,7 +44,7 @@ export default class PCIDevice extends SteveModel {
|
||||
},
|
||||
{
|
||||
action: 'disablePassthrough',
|
||||
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
|
||||
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && canUpdate,
|
||||
icon: 'icon icon-fw icon-dot-open',
|
||||
label: 'Disable Passthrough',
|
||||
bulkable: true,
|
||||
|
||||
@ -24,7 +24,7 @@ const OBSCURE_NAMESPACE_PREFIX = [
|
||||
|
||||
export default class HciNamespace extends namespace {
|
||||
get _availableActions() {
|
||||
const out = super._availableActions;
|
||||
let out = super._availableActions;
|
||||
const remove = out.findIndex((a) => a.action === 'promptRemove');
|
||||
|
||||
const promptRemove = {
|
||||
@ -53,6 +53,16 @@ export default class HciNamespace extends namespace {
|
||||
insertAt(out, out.length - 1, promptRemove);
|
||||
insertAt(out, out.length - 5, editQuotaAction);
|
||||
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
out = out.map((action) => {
|
||||
if (['move'].includes(action.action)) {
|
||||
return { ...action, enabled: action.enabled && canUpdate };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,20 @@ import Secret from '@shell/models/secret';
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
|
||||
export default class HciSecret extends Secret {
|
||||
get _availableActions() {
|
||||
let out = super._availableActions;
|
||||
|
||||
out = out.map((action) => {
|
||||
if (['download'].includes(action.action)) {
|
||||
return { ...action, enabled: !!this.linkFor('update') };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// prevent harvester secret detail page be overridden.
|
||||
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
|
||||
get fullDetailPageOverride() {
|
||||
|
||||
@ -106,6 +106,15 @@ export default class HciStorageClass extends StorageClass {
|
||||
|
||||
get availableActions() {
|
||||
let out = super.availableActions || [];
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
out = out.map((action) => {
|
||||
if (['setDefault', 'setAsDefault', 'resetDefault'].includes(action.action)) {
|
||||
return { ...action, enabled: canUpdate };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
if (this.isInternalStorageClass()) {
|
||||
out = out.filter((action) => {
|
||||
|
||||
@ -4,6 +4,8 @@ import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
||||
import HarvesterResource from './harvester';
|
||||
import { HCI } from '../types';
|
||||
|
||||
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
|
||||
|
||||
export default class HciAddonConfig extends HarvesterResource {
|
||||
get availableActions() {
|
||||
const out = super._availableActions;
|
||||
@ -19,9 +21,10 @@ export default class HciAddonConfig extends HarvesterResource {
|
||||
out.push(rancherDashboard);
|
||||
}
|
||||
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
const toggleAddon = {
|
||||
action: 'toggleAddon',
|
||||
enabled: true,
|
||||
enabled: canUpdate,
|
||||
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
|
||||
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
|
||||
};
|
||||
@ -45,6 +48,15 @@ export default class HciAddonConfig extends HarvesterResource {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.spec.enabled && this.id === HARVESTER_NVIDIA_DRIVER_TOOLKIT) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources: [this],
|
||||
component: 'HarvesterEnableNvidiaDriverToolkit',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.spec.enabled = !this.spec.enabled;
|
||||
await this.save();
|
||||
} catch (err) {
|
||||
|
||||
@ -3,6 +3,20 @@ import { findBy } from '@shell/utils/array';
|
||||
import HarvesterResource from './harvester';
|
||||
|
||||
export default class HciKeypair extends HarvesterResource {
|
||||
get _availableActions() {
|
||||
let out = super._availableActions;
|
||||
|
||||
out = out.map((action) => {
|
||||
if (['download'].includes(action.action)) {
|
||||
return { ...action, enabled: !!this.linkFor('update') };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
get stateDisplay() {
|
||||
const conditions = get(this, 'status.conditions');
|
||||
const status = (findBy(conditions, 'type', 'validated') || {}).status ;
|
||||
|
||||
@ -19,16 +19,18 @@ export default class ScheduleVmBackup extends HarvesterResource {
|
||||
}
|
||||
});
|
||||
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
return [
|
||||
{
|
||||
action: 'resumeSchedule',
|
||||
enabled: ucFirst(this.state) === STATES.suspended.label,
|
||||
enabled: canUpdate && ucFirst(this.state) === STATES.suspended.label,
|
||||
icon: 'icons icon-play',
|
||||
label: this.t('harvester.action.resumeSchedule'),
|
||||
},
|
||||
{
|
||||
action: 'suspendSchedule',
|
||||
enabled: ucFirst(this.state) === STATES.active.label,
|
||||
enabled: canUpdate && ucFirst(this.state) === STATES.active.label,
|
||||
icon: 'icons icon-pause',
|
||||
label: this.t('harvester.action.suspendSchedule'),
|
||||
},
|
||||
|
||||
@ -39,9 +39,6 @@ function isReady() {
|
||||
export default class HciVmImage extends HarvesterResource {
|
||||
get availableActions() {
|
||||
let out = super._availableActions;
|
||||
const toFilter = ['goToEditYaml'];
|
||||
|
||||
out = out.filter( (A) => !toFilter.includes(A.action));
|
||||
|
||||
// show `Clone` only when imageSource is `download`
|
||||
if (this.imageSource !== 'download') {
|
||||
@ -55,6 +52,7 @@ export default class HciVmImage extends HarvesterResource {
|
||||
canCreateVM = false;
|
||||
}
|
||||
|
||||
const canCreateImage = !!this.$getters?.['schemaFor']?.(HCI.IMAGE)?.collectionMethods?.some((method) => method.toLowerCase() === 'post');
|
||||
const customActions = this.isReady ? [
|
||||
{
|
||||
action: 'createFromImage',
|
||||
@ -64,13 +62,13 @@ export default class HciVmImage extends HarvesterResource {
|
||||
},
|
||||
{
|
||||
action: 'encryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted && canCreateImage,
|
||||
icon: 'icon icon-lock',
|
||||
label: this.t('harvester.action.encryptImage'),
|
||||
},
|
||||
{
|
||||
action: 'decryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted && canCreateImage,
|
||||
icon: 'icon icon-unlock',
|
||||
label: this.t('harvester.action.decryptImage'),
|
||||
},
|
||||
|
||||
@ -83,6 +83,42 @@ const VMIPhase = {
|
||||
|
||||
let productInStore;
|
||||
|
||||
let _podOwnerMap = null;
|
||||
let _podOwnerMapSource = null;
|
||||
|
||||
function getPodByOwnerName(rootGetters, inStore, ownerName) {
|
||||
const podList = rootGetters[`${ inStore }/all`](POD);
|
||||
|
||||
if (!Array.isArray(podList)) {
|
||||
return undefined;
|
||||
}
|
||||
// if not equals (usually means the pod list has been updated), we need to rebuild the map, otherwise we can reuse the map for better performance
|
||||
if (_podOwnerMapSource !== podList) {
|
||||
_podOwnerMap = new Map(); // use Map to store ownerReference name and pod mapping
|
||||
for (const pod of podList) {
|
||||
const refName = pod.metadata?.ownerReferences?.[0]?.name;
|
||||
|
||||
if (refName) {
|
||||
_podOwnerMap.set(refName, pod);
|
||||
}
|
||||
}
|
||||
_podOwnerMapSource = podList;
|
||||
}
|
||||
|
||||
return _podOwnerMap.get(ownerName);
|
||||
}
|
||||
|
||||
function getPvcsByNames(rootGetters, inStore, names) {
|
||||
const pvcList = rootGetters[`${ inStore }/all`](PVC);
|
||||
|
||||
if (!Array.isArray(pvcList)) {
|
||||
return [];
|
||||
}
|
||||
const uniqueNames = new Set(names);
|
||||
|
||||
return pvcList.filter((pvc) => uniqueNames.has(pvc.metadata?.name));
|
||||
}
|
||||
|
||||
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
|
||||
|
||||
export default class VirtVm extends HarvesterResource {
|
||||
@ -94,6 +130,8 @@ export default class VirtVm extends HarvesterResource {
|
||||
clone.action = 'goToCloneVM';
|
||||
}
|
||||
|
||||
const canCreateVMSSchedule = !!this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
|
||||
|
||||
return [
|
||||
{
|
||||
action: 'stopVM',
|
||||
@ -126,6 +164,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
},
|
||||
{
|
||||
action: 'restartVM',
|
||||
altAction: 'altRestartVM',
|
||||
enabled: !!this.actions?.restart,
|
||||
icon: 'icon icon-refresh',
|
||||
label: this.t('harvester.action.restart'),
|
||||
@ -133,10 +172,11 @@ export default class VirtVm extends HarvesterResource {
|
||||
bulkAction: 'restartVM'
|
||||
},
|
||||
{
|
||||
action: 'softrebootVM',
|
||||
enabled: !!this.actions?.softreboot,
|
||||
icon: 'icon icon-pipeline',
|
||||
label: this.t('harvester.action.softreboot')
|
||||
action: 'softrebootVM',
|
||||
altAction: 'doSoftReboot',
|
||||
enabled: !!this.actions?.softreboot,
|
||||
icon: 'icon icon-pipeline',
|
||||
label: this.t('harvester.action.softreboot')
|
||||
},
|
||||
{
|
||||
action: 'startVM',
|
||||
@ -171,7 +211,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
},
|
||||
{
|
||||
action: 'createSchedule',
|
||||
enabled: this.schedulingVMBackupFeatureEnabled,
|
||||
enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled,
|
||||
icon: 'icon icon-history',
|
||||
label: this.t('harvester.action.createSchedule')
|
||||
},
|
||||
@ -188,10 +228,12 @@ export default class VirtVm extends HarvesterResource {
|
||||
label: this.t('harvester.action.ejectCDROM')
|
||||
},
|
||||
{
|
||||
action: 'migrateVM',
|
||||
enabled: !!this.actions?.migrate,
|
||||
icon: 'icon icon-copy',
|
||||
label: this.t('harvester.action.migrate')
|
||||
action: 'migrateVM',
|
||||
enabled: !!this.actions?.migrate,
|
||||
icon: 'icon icon-copy',
|
||||
label: this.t('harvester.action.vmMigrate'),
|
||||
bulkable: true,
|
||||
bulkAction: 'migrateVM'
|
||||
},
|
||||
{
|
||||
action: 'abortMigrationVM',
|
||||
@ -331,6 +373,10 @@ export default class VirtVm extends HarvesterResource {
|
||||
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||
}
|
||||
|
||||
altRestartVM() {
|
||||
this.doActionGrowl('restart', {});
|
||||
}
|
||||
|
||||
restartVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
@ -660,16 +706,13 @@ export default class VirtVm extends HarvesterResource {
|
||||
|
||||
get podResource() {
|
||||
const inStore = this.productInStore;
|
||||
|
||||
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
||||
const podList = this.$rootGetters[`${ inStore }/all`](POD);
|
||||
|
||||
return podList.find((P) => {
|
||||
return (
|
||||
vmiResource?.metadata?.name &&
|
||||
vmiResource?.metadata?.name === P.metadata?.ownerReferences?.[0].name
|
||||
);
|
||||
});
|
||||
if (!vmiResource?.metadata?.name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getPodByOwnerName(this.$rootGetters, inStore, vmiResource.metadata.name);
|
||||
}
|
||||
|
||||
get isPaused() {
|
||||
@ -710,17 +753,13 @@ export default class VirtVm extends HarvesterResource {
|
||||
get vmi() {
|
||||
const inStore = this.productInStore;
|
||||
|
||||
const vmis = this.$rootGetters[`${ inStore }/all`](HCI.VMI);
|
||||
|
||||
return vmis.find((VMI) => VMI.id === this.id);
|
||||
return this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
||||
}
|
||||
|
||||
get volumes() {
|
||||
const pvcs = this.$rootGetters[`${ this.productInStore }/all`](PVC);
|
||||
|
||||
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
|
||||
|
||||
return pvcs.filter((pvc) => volumeClaimNames.includes(pvc.metadata.name));
|
||||
return getPvcsByNames(this.$rootGetters, this.productInStore, volumeClaimNames);
|
||||
}
|
||||
|
||||
get lvmVolumes() {
|
||||
@ -753,17 +792,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
|
||||
}
|
||||
|
||||
if ((this.vmi || this.isVMCreated) && this.podResource) {
|
||||
// const podStatus = this.podResource.getPodStatus;
|
||||
// if (POD_STATUS_ALL_ERROR.includes(podStatus?.status)) {
|
||||
// return {
|
||||
// ...podStatus,
|
||||
// status: 'LAUNCHER_POD_ERROR',
|
||||
// pod: this.podResource,
|
||||
// };
|
||||
// }
|
||||
}
|
||||
|
||||
return this?.vmi?.status?.phase;
|
||||
}
|
||||
|
||||
@ -901,9 +929,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
|
||||
const inStore = this.productInStore;
|
||||
|
||||
const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE);
|
||||
|
||||
const res = allRestore.find((O) => O.id === id);
|
||||
const res = this.$rootGetters[`${ inStore }/byId`](HCI.RESTORE, id);
|
||||
|
||||
if (res) {
|
||||
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
|
||||
@ -1073,42 +1099,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
return out;
|
||||
}
|
||||
|
||||
get warningCount() {
|
||||
return this.resourcesStatus.warningCount;
|
||||
}
|
||||
|
||||
get errorCount() {
|
||||
return this.resourcesStatus.errorCount;
|
||||
}
|
||||
|
||||
get resourcesStatus() {
|
||||
const inStore = this.productInStore;
|
||||
const vmList = this.$rootGetters[`${ inStore }/all`](HCI.VM);
|
||||
let warningCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
vmList.forEach((vm) => {
|
||||
const status = vm.actualState;
|
||||
|
||||
if (status === VM_ERROR) {
|
||||
errorCount += 1;
|
||||
} else if (
|
||||
status === 'Stopping' ||
|
||||
status === 'Waiting' ||
|
||||
status === 'Pending' ||
|
||||
status === 'Starting' ||
|
||||
status === 'Terminating'
|
||||
) {
|
||||
warningCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
warningCount,
|
||||
errorCount
|
||||
};
|
||||
}
|
||||
|
||||
get volumeClaimTemplates() {
|
||||
return parseVolumeClaimTemplates(this);
|
||||
}
|
||||
@ -1126,7 +1116,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
get rootImageId() {
|
||||
let imageId = '';
|
||||
const inStore = this.productInStore;
|
||||
const pvcs = this.$rootGetters[`${ inStore }/all`](PVC) || [];
|
||||
|
||||
const volumes = this.spec.template.spec.volumes || [];
|
||||
|
||||
@ -1136,9 +1125,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
});
|
||||
|
||||
if (!isNoExistingVolume) {
|
||||
const existingVolume = pvcs.find(
|
||||
(P) => P.id === `${ this.metadata.namespace }/${ firstVolumeName }`
|
||||
);
|
||||
const existingVolume = this.$rootGetters[`${ inStore }/byId`](PVC, `${ this.metadata.namespace }/${ firstVolumeName }`);
|
||||
|
||||
if (existingVolume) {
|
||||
return existingVolume?.metadata?.annotations?.[
|
||||
@ -1316,8 +1303,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
}
|
||||
|
||||
get isBackupTargetUnavailable() {
|
||||
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
|
||||
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
|
||||
const backupTargetSetting = this.$rootGetters['harvester/byId'](HCI.SETTING, 'backup-target');
|
||||
|
||||
return isBackupTargetSettingUnavailable(backupTargetSetting);
|
||||
}
|
||||
|
||||
19
pkg/harvester/models/management.cattle.io.project.js
Normal file
19
pkg/harvester/models/management.cattle.io.project.js
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
import shellProject from '@shell/models/management.cattle.io.project';
|
||||
|
||||
// This model controls `Project / Namespace` page in rancher integration mode
|
||||
// Extend management.cattle.io.project model from shell
|
||||
export default class Project extends shellProject {
|
||||
get _availableActions() {
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
// disable `Edit Config` action if user does not have update permission.
|
||||
return super._availableActions.map((action) => {
|
||||
if (action.action === 'goToEdit') {
|
||||
return { ...action, enabled: canUpdate };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -57,7 +57,11 @@ export default class HciVlanConfig extends HarvesterResource {
|
||||
get _availableActions() {
|
||||
const out = super._availableActions;
|
||||
|
||||
insertAt(out, 0, this.migrateAction);
|
||||
const canMigrate = !!this.$getters?.['schemaFor']?.(HCI.VLAN_CONFIG)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
|
||||
|
||||
if (canMigrate) {
|
||||
insertAt(out, 0, this.migrateAction);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "harvester",
|
||||
"description": "Rancher UI Extension for Harvester",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0-dev",
|
||||
"private": false,
|
||||
"rancher": {
|
||||
"annotations": {
|
||||
@ -17,12 +17,12 @@
|
||||
"nuxt": "./node_modules/.bin/nuxt"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=24.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0"
|
||||
"@vue/cli-plugin-babel": "~5.0.9",
|
||||
"@vue/cli-service": "~5.0.9",
|
||||
"@vue/cli-plugin-typescript": "~5.0.9"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
@ -123,5 +123,26 @@ export default {
|
||||
const clusterId = currentCluster.id;
|
||||
|
||||
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
|
||||
}
|
||||
},
|
||||
|
||||
// Few harvester resources name and REAL resource are different. E.g. HCI.NETWORK_ATTACHMENT page resource is NETWORK_ATTACHMENT.
|
||||
// Check in config/harvester-cluster.js for more details.
|
||||
// We need to look up the schema by resource name, and fallback to find using real resource name
|
||||
schemaFor: (state, getters, rootState, rootGetters) => (type, _fuzzy = false, _allowThrow = true) => {
|
||||
// follow the same logic as type-map/schemaFor in /dashboard/shell/plugins/dashboard-store/getters.js
|
||||
const normalizedType = getters.normalizeType(type);
|
||||
const schemas = state.types['schema'];
|
||||
const out = schemas?.map?.get(normalizedType);
|
||||
|
||||
if (out) return out;
|
||||
|
||||
// if not found, use the resource mapping in configureType for a second try
|
||||
const resourceType = rootGetters['type-map/optionsFor'](type)?.resource;
|
||||
if (resourceType && resourceType !== type) {
|
||||
const normalizedResource = getters.normalizeType(resourceType);
|
||||
return schemas?.map?.get(normalizedResource) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
21
pkg/harvester/utils/error.js
Normal file
21
pkg/harvester/utils/error.js
Normal file
@ -0,0 +1,21 @@
|
||||
const AUTH_ERROR_CODES = [401, 403, 404];
|
||||
|
||||
export function getLoginAwareErrors(err, message = '') {
|
||||
const errors = Array.isArray(err) ? err : (err ? [err] : []);
|
||||
|
||||
if (!errors.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const generic = message;
|
||||
|
||||
if (errors.some((e) => AUTH_ERROR_CODES.includes(e?._status || e?.response?.status))) {
|
||||
return [generic];
|
||||
}
|
||||
|
||||
const msgs = errors
|
||||
.map((e) => (typeof e === 'string' ? e : (e?.message || e?._statusText || '')))
|
||||
.filter(Boolean);
|
||||
|
||||
return msgs.length ? msgs : [generic];
|
||||
}
|
||||
@ -27,23 +27,6 @@ OUTPUT_DIR=dist/${DIR}-embedded
|
||||
echo "Building..."
|
||||
COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard/' RESOURCE_BASE='/dashboard/' RANCHER_ENV=harvester yarn run build
|
||||
|
||||
if [ -v EMBED_PKG ]; then
|
||||
echo "Build and embed plugin from: $EMBED_PKG"
|
||||
PKG_FILE_NAME=${EMBED_PKG##*/}
|
||||
echo PKG_FILE_NAME: $PKG_FILE_NAME
|
||||
|
||||
PKG_NAME="${PKG_FILE_NAME/.tar.gz/""}"
|
||||
echo "Plugin name: '$PKG_NAME'"
|
||||
|
||||
# Fetch file, unpack and move to dist
|
||||
curl $EMBED_PKG --output $PKG_FILE_NAME
|
||||
OUTPUT_DIR_PKG=$OUTPUT_DIR/$PKG_NAME
|
||||
mkdir -p $OUTPUT_DIR_PKG
|
||||
tar xvfz $PKG_FILE_NAME -C $OUTPUT_DIR/$PKG_NAME
|
||||
echo "Plugin contents that will be served from $PKG_NAME"
|
||||
ls -alR $OUTPUT_DIR/$PKG_NAME
|
||||
fi
|
||||
|
||||
echo "Destroying..."
|
||||
find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \;
|
||||
find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \;
|
||||
|
||||
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