Compare commits

..
Author SHA1 Message Date
mergify[bot]andAndy Lee 38ad0fe1a7 upgrade ui-ext version to 1.5.0-rc2 (#208) (#218)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 18667836c2)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-25 11:44:26 +08:00
mergify[bot]andAndy Lee c84d262099 fix volume won't be dismiss after deleting (#216) (#217)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 2940f25fe0)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-24 15:33:20 +08:00
mergify[bot]andAndy Lee dbb53eae00 fix image.repo to image.repository (#214) (#215)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit a24d01717a)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-21 17:43:13 +08:00
mergify[bot]andAndy Lee 4afacd754a fix the doneOverride path for harvesterhci.io.virtualmachinetemplate (#211) (#213)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 22a032e56c)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-20 17:13:45 +08:00
mergify[bot]andAndy Lee 10d159e2c7 Add volum mode options in create VM volume tab (#209) (#210)
* add volume mode option for non-longhorn volume

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

* fix default storage class in create VM page

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

---------

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

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-19 17:44:58 +08:00
mergify[bot]andAndy Lee de755a662a upgrade rancher/shell to 3.0.2-rc6 (#206) (#207)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 2dff7b0a93)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-14 16:50:13 +08:00
mergify[bot]andJack Yu f1200f4508 chore: add building catalog GA (#181) (#204)
* chore: add building catalog GA

Signed-off-by: Jack Yu <jack.yu@suse.com>

* feat: should include prerelease

Signed-off-by: Jack Yu <jack.yu@suse.com>

---------

Signed-off-by: Jack Yu <jack.yu@suse.com>
(cherry picked from commit 1880043a80)

Co-authored-by: Jack Yu <jack.yu@suse.com>
2025-03-14 15:04:39 +08:00
mergify[bot]andAndy Lee 0ca2b0591a Bring status object in harvesterhci.io.upgrade dismiss PUT API (#201) (#202)
* bring status object in PUT dismiss API

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

* add cleanForSave in harvester model

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

---------

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

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-13 14:15:59 +08:00
mergify[bot]andAndy Lee 44f003309c Add volumeMode dropdown for third-party storage (#197) (#200)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 90c923b480)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-12 14:29:40 +08:00
mergify[bot]andAndy Lee 7c1596c965 replace tag format v1.5.0 to harvester-1.5.0 (#190) (#199)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 374b904191)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-12 12:35:01 +08:00
mergify[bot]andAndy Lee a5130986a8 fix percent typo (#198)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 487f9abc10)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-03-11 17:24:23 +08:00
Andy Lee e4ae7cec5e Merge pull request #194 from harvester/mergify/bp/release-harvester-v1.5/pr-191
Fix missing remove button in Infobox (backport #191)
2025-03-11 12:02:41 +08:00
Andy Lee 2c7b59daed Fix infobox style
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 4e831cbc5f)
2025-03-11 03:58:53 +00:00
Andy Lee 224e34315b Merge pull request #193 from harvester/mergify/bp/release-harvester-v1.5/pr-192
remove require dot for non-longhorn v1 (backport #192)
2025-03-11 11:37:06 +08:00
Andy Lee 9f56f9eccc remove require dot for non-longhorn v1
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit b939df9b7d)
2025-03-11 03:35:42 +00:00
Andy Lee dd76956972 Merge pull request #189 from harvester/mergify/bp/release-harvester-v1.5/pr-183
Bump harvester-ui-extension to v1.5.0-rc1 (backport #183)
2025-03-06 15:58:01 +08:00
Andy Lee d92a53d9a7 bump to v1.5.0-rc1
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 8ad7a57ab4)
2025-03-06 07:56:15 +00:00
Andy Lee afc3f0d87f Merge pull request #188 from harvester/mergify/bp/release-harvester-v1.5/pr-185
fix: allow set empty for backupVolumeSnapshotClassName in Setting csi-driver-config (backport #185)
2025-03-06 15:52:16 +08:00
Yi-Ya Chen a607b604f8 refactor: default should selectable
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit 64b59a0c27)
2025-03-06 07:49:27 +00:00
Yi-Ya Chen 4d7b0f7350 feat: gray out class name selector
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit 72415622d9)
2025-03-06 07:49:27 +00:00
Andy Lee c570b5870b Merge pull request #187 from harvester/mergify/bp/release-harvester-v1.5/pr-186
Filter exported SC for non-longhorn V1 volume (backport #186)
2025-03-06 15:37:17 +08:00
Andy Lee af36163b29 Filter export SC for non-longhorn V1 volume
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 104e98e390)
2025-03-06 07:35:24 +00:00
Andy Lee b3dbc2f0d3 Merge pull request #184 from harvester/mergify/bp/release-harvester-v1.5/pr-182
Add HarvesterImageDownloader for cdi image (backport #182)
2025-03-06 14:54:04 +08:00
Andy Lee fed86d4c24 Remove converting sentence
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit cbd5e45200)
2025-03-06 06:50:24 +00:00
Andy Lee d18ebfb26b Add HarvesterImageDownloader
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 966d4d6709)
2025-03-06 06:50:24 +00:00
Andy Lee 477206d8d0 Merge pull request #180 from harvester/mergify/bp/release-harvester-v1.5/pr-175
Add label tab to vm template (backport #175)
2025-03-04 17:42:29 +08:00
Andy Lee fd0836d489 Add label tab to vm template
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit efdfdbf0b8)
2025-03-04 09:36:33 +00:00
Andy Lee 2e3536abc6 Merge pull request #179 from harvester/mergify/bp/release-harvester-v1.5/pr-178
Fix  undefined reading harvesterhci.io/storageClassName (backport #178)
2025-03-04 11:03:20 +08:00
Andy Lee 45c0dce61b fix undefined reading harvesterhci.io/storageClassName
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 54f85963ea)
2025-03-04 03:02:06 +00:00
Andy Lee 6b8b1f68eb Merge pull request #177 from harvester/mergify/bp/release-harvester-v1.5/pr-173
Introduce VM Pending status (backport #173)
2025-03-04 10:55:29 +08:00
Andy Lee bb645c00db introduce VM pending status
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 05ff8e4f19)
2025-03-04 02:49:45 +00:00
Andy Lee ed8c6e81ca Merge pull request #176 from harvester/mergify/bp/release-harvester-v1.5/pr-174
fix: align all memory / storage / quota units to Gi/Mi (backport #174)
2025-03-04 10:49:03 +08:00
Andy Lee ed692b579d update harvesterhci.io.host
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 8fbbfc90f1)
2025-03-04 02:47:43 +00:00
Andy Lee f02074a25e Extract variables to utils/unit
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 8db4de1c4a)
2025-03-04 02:47:42 +00:00
Andy Lee a8f8f0d288 fix: align all memory / storage / quota units to Gi/Mi
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 8f76d5ad30)
2025-03-04 02:47:42 +00:00
Andy Lee 9fa9b3837b Merge pull request #172 from harvester/mergify/bp/release-harvester-v1.5/pr-171
Add refreshIntervalInSecond in 1.4.2 feature flag (backport #171)
2025-02-27 17:29:36 +08:00
andy.lee 4b3bdf8a59 add refreshIntervalInSecond in 1.4.2 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 30dc56cad5)
2025-02-27 09:24:58 +00:00
Andy Lee 7bdf40d737 Merge pull request #170 from harvester/mergify/bp/release-harvester-v1.5/pr-169
fix golden image error when creating LVM volume (backport #169)
2025-02-27 13:51:29 +08:00
andy.lee a1e7345437 fix golden image error when creating LVM volume
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit ca7eccd688)
2025-02-27 05:46:58 +00:00
Andy Lee 9322a77b20 Merge pull request #168 from harvester/mergify/bp/release-harvester-v1.5/pr-167
Hide non-longhorn volume external link (backport #167)
2025-02-26 17:08:30 +08:00
andy.lee ed7d7ffd2e hide non longhorn volume external link
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 046947de32)
2025-02-26 09:01:23 +00:00
Andy Lee 2ef8e2e0aa Merge pull request #166 from harvester/mergify/bp/release-harvester-v1.5/pr-164
Add refreshIntervalInSeconds in backup targe edit page (backport #164)
2025-02-26 16:19:19 +08:00
andy.lee a0ea6beedb Add refreshIntervalInSeconds in backup targe edit page
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 7e00a08e35)
2025-02-26 08:18:13 +00:00
Andy Lee 85a723e20c Merge pull request #163 from harvester/mergify/bp/release-harvester-v1.5/pr-162
Fix upgrade cluster model styles (backport #162)
2025-02-26 09:35:19 +08:00
andy.lee bbe6b1bdf0 change overallMessage color to red
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 50901a2eaf)
2025-02-26 01:32:24 +00:00
andy.lee 81a572e5b5 fix upgrade model style
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit ff439e66a9)
2025-02-26 01:32:24 +00:00
Andy Lee 1a727f0001 Merge pull request #161 from harvester/mergify/bp/release-harvester-v1.5/pr-159
Add Labels Tab in VM edit page (backport #159)
2025-02-24 17:44:35 +08:00
andy.lee 91862244e1 Add labels tab in VM edit pae
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 9cd3cd6492)
2025-02-24 09:42:11 +00:00
andy.lee c7b0e9ee7a update catalog.cattle.io/rancher-version to 2.11.0
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit e07042b9c9)
2025-02-24 09:42:10 +00:00
Andy Lee d9337ff188 Merge pull request #160 from harvester/mergify/bp/release-harvester-v1.5/pr-152
Fix usb and pci devices unable to passthrough (backport #152)
2025-02-24 16:48:46 +08:00
andy.lee c16922c228 add pcideviceclaim and usbdeviceclaim models
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 188f058df8)
2025-02-24 08:41:07 +00:00
Andy Lee bbade7e50c Merge pull request #158 from harvester/mergify/bp/release-harvester-v1.5/pr-157
Limit harvester-ui-extension to install on Rancher 2.11.0+ (backport #157)
2025-02-21 16:40:28 +08:00
andy.lee 193bf092d5 Update catalog.cattle.io/rancher-version to 2.11.0
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 36c68eefb1)
2025-02-21 08:39:18 +00:00
Andy Lee 308b6ddbd7 Merge pull request #156 from harvester/mergify/bp/release-harvester-v1.5/pr-154
Bump harvester-ui-extension version to v1.5.0-dev (backport #154)
2025-02-21 15:07:19 +08:00
andy.lee c850b78da2 bump to v1.5.0-dev
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 0a782fa4d5)
2025-02-21 07:01:38 +00:00
Andy Lee 0fe9557eb8 Merge pull request #155 from harvester/mergify/bp/release-harvester-v1.5/pr-148
Update installation flow in README.md (backport #148)
2025-02-21 14:43:19 +08:00
andy.lee b6953d73f7 Update installation flow in README.md
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit f855a0add5)
2025-02-21 06:13:47 +00:00
Andy Lee 8a89a9c502 Merge pull request #153 from harvester/mergify/bp/release-harvester-v1.5/pr-151
feat: set up a way to test UI changes in Harvester UI when accessed via Rancher (backport #151)
2025-02-21 13:37:14 +08:00
Yi-Ya Chen d5f17bfdf1 ci: rename file
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit 7f98dfe9c8)
2025-02-21 05:34:10 +00:00
Yi-Ya Chen 9d0bb6e1f4 ci: revert change
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit 6a3153a7ec)
2025-02-21 05:34:10 +00:00
Yi-Ya Chen 2ec37d81d9 ci: rename the workflow
(cherry picked from commit 629cb0c601)
2025-02-21 05:34:10 +00:00
Yi-Ya Chen e50bff8b5e ci: add ci pipeline
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit 0c3fe22df7)
2025-02-21 05:34:09 +00:00
Andy Lee e898181834 Merge pull request #150 from harvester/mergify/bp/release-harvester-v1.5/pr-149
More third party storage UI change (backport #149)
2025-02-20 14:53:03 +08:00
andy.lee a0f877a23e More third party storage UI change
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 66f53c8a00)
2025-02-20 06:45:48 +00:00
Andy Lee a805490930 Merge pull request #147 from harvester/mergify/bp/release-harvester-v1.5/pr-145
External/third-party storage UI change (backport #145)
2025-02-19 17:06:44 +08:00
andy.lee 887a122358 allow sorting storage class column in volume list page
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 4a303e514c)
2025-02-19 09:05:28 +00:00
andy.lee 1f48e8aea7 add thirdPartyStorage feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit b828c2f66d)
2025-02-19 09:05:28 +00:00
andy.lee b2c1a3b068 add third party storage UI change
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 40794d89a0)
2025-02-19 09:05:28 +00:00
Andy Lee df1566f394 Merge pull request #146 from harvester/mergify/bp/release-harvester-v1.5/pr-143
Fix sshkey is not added to cloud init user data immediately (backport #143)
2025-02-19 16:45:40 +08:00
andy.lee 0a3b122316 fix sshkey is not added to cloud init user data immediately
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit e2daa1ea0c)
2025-02-19 08:23:47 +00:00
Yiya Chen d497613cc4 Merge pull request #144 from harvester/mergify/bp/release-harvester-v1.5/pr-142
fix: no progress circle on the top after triggering upgrade (backport #142)
2025-02-19 09:30:31 +08:00
Yi-Ya Chen 91494f5832 fix: register upgrade component in header
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit b499f62c56)
2025-02-19 01:16:26 +00:00
65 changed files with 1392 additions and 401 deletions
@@ -0,0 +1,36 @@
name: Build Harvester Catalog Image and Publish on Release
on:
workflow_dispatch:
release:
types: [published]
defaults:
run:
shell: bash
working-directory: ./
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run tests
uses: ./.github/actions/lint
build-and-push-extension-catalog:
needs: lint
uses: ./.github/workflows/build-extension-catalog.yml
permissions:
actions: write
contents: read
packages: write
id-token: write
with:
registry_target: docker.io
registry_user: rancher
repository_name: harvester-ui-catalog
secrets: inherit
@@ -1,4 +1,4 @@
name: Build Harvester Dashboard and Upload
name: Build Harvester Dashboard and Publish
on:
workflow_call:
@@ -0,0 +1,81 @@
name: Build and release Extension Catalog Image to registry
on:
workflow_call:
inputs:
registry_target:
required: true
type: string
repository_name:
required: true
type: string
registry_user:
required: true
type: string
outputs:
build-extension-catalog-job-status:
value: ${{ jobs.build-extension-catalog.outputs.build-status }}
jobs:
build-and-push-extension-catalog:
name: Build container image
if: github.ref_type == 'tag' || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
packages: write
id-token: write
outputs:
build-status: ${{ job.status }}
steps:
- name: Read Secrets
uses: rancher-eio/read-vault-secrets@main
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
- name: Checkout repository (normal flow)
uses: actions/checkout@v4
- name: Enable Corepack
run: corepack enable
- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}
- name: Setup Helm
uses: azure/setup-helm@v3
with:
version: v3.8.0
- name: Setup Nodejs with yarn caching
uses: actions/setup-node@v4
with:
node-version: '20'
cache: yarn
- name: Install dependencies
run: yarn
- name: Build and push UI image
run: |
publish="yarn publish-pkgs -c -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
$publish
REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2)
IMAGE_VERSION=$(cat package.json | jq .version | sed 's/"//g')
FULL_IMAGE_NAME=$(echo ${{ inputs.registry_target }}/${{ inputs.registry_user }}/ui-extension-$REPO_NAME:$IMAGE_VERSION)
IMAGE_ID=$(docker inspect --format='{{.Id}}' $FULL_IMAGE_NAME)
docker tag $IMAGE_ID ${{ inputs.registry_target }}/${{ inputs.registry_user }}/${{ inputs.repository_name }}:$IMAGE_VERSION
docker push ${{ inputs.registry_target }}/${{ inputs.registry_user }}/${{ inputs.repository_name }}:$IMAGE_VERSION
@@ -1,34 +0,0 @@
name: Build and Release Extension Charts
on:
workflow_dispatch:
release:
types: [released]
defaults:
run:
shell: bash
working-directory: ./
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run tests
uses: ./.github/actions/lint
build-extension-charts:
needs:
- lint
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
permissions:
actions: write
contents: write
deployments: write
pages: write
with:
target_branch: gh-pages
tagged_release: ${{ github.ref_name }}
@@ -0,0 +1,136 @@
name: Build and Release Extension Charts on PR Merge
on:
push:
branches:
- 'release-harvester-v*'
jobs:
setup-target-branch:
runs-on: ubuntu-latest
outputs:
target_branch: ${{ steps.get-version.outputs.target_branch }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine target branch
id: get-version
run: |
if [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
else
echo "Error: invalid branch format." && exit 1
fi
- name: Ensure target branch exists
run: |
git fetch --all
if ! git ls-remote --exit-code --heads origin "${{ env.target_branch }}"; then
git checkout gh-pages
git checkout -b "${{ env.target_branch }}"
git push origin "${{ env.target_branch }}"
fi
extract-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract version from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' pkg/harvester/package.json)
echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "version=${VERSION}" >> $GITHUB_OUTPUT
build-extension-charts:
needs:
- setup-target-branch
- extract-version
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup environment
run: |
corepack enable
yarn install --frozen-lockfile
- name: Setup Helm
uses: azure/setup-helm@v3
with:
version: v3.8.0
- name: Build Helm charts
run: |
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
- name: Upload charts artifact
uses: actions/upload-artifact@v4
with:
name: charts
path: tmp
release:
needs:
- setup-target-branch
- extract-version
- build-extension-charts
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout release branch
uses: actions/checkout@v4
with:
ref: '${{ github.ref_name }}'
- name: Get last commit hash
id: last_commit
run: |
LAST_COMMIT=$(git rev-parse HEAD)
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
- name: Checkout target branch
uses: actions/checkout@v4
with:
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
- name: Remove old artifacts
run: |
rm -rf extensions/harvester/${{ needs.extract-version.outputs.version }}
rm -rf charts/harvester/${{ needs.extract-version.outputs.version }}
rm -f assets/harvester/harvester-${{ needs.extract-version.outputs.version }}.tgz
- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: charts
- name: Commit and push artifacts
run: |
git add ./{assets,charts,extensions,index.yaml}
git commit -m "CI Build Artifacts (commit: ${{ env.LAST_COMMIT }}, version: ${{ needs.extract-version.outputs.version }})"
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
- name: Run Helm chart releaser
uses: helm/chart-releaser-action@v1.4.1
with:
charts_dir: ./charts
env:
CR_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
CR_SKIP_EXISTING: true
@@ -0,0 +1,53 @@
name: Build and Release Extension Charts on Release
on:
workflow_dispatch:
release:
types: [released]
defaults:
run:
shell: bash
working-directory: ./
jobs:
setup-release-tag:
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine release tag
id: determine_tag
run: |
if [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
echo "${RELEASE_TAG}"
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
else
echo "Error: invalid tag format." && exit 1
fi
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run tests
uses: ./.github/actions/lint
build-extension-charts:
needs:
- setup-release-tag
- lint
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
permissions:
actions: write
contents: write
deployments: write
pages: write
with:
target_branch: gh-pages
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
@@ -1,4 +1,4 @@
name: Build Dashboard (Branch)
name: Build Standalone on PR Merge
on:
push:
@@ -11,16 +11,16 @@ on:
- main
- 'release-harvester-v*'
- '*-dev'
types:
types:
- merged
jobs:
build-validation:
name: Build Test
uses: ./.github/workflows/test.yaml
uses: ./.github/workflows/run-lint.yaml
build:
name: Build and Upload Package
uses: ./.github/workflows/build-and-upload.yaml
uses: ./.github/workflows/build-and-publish-standalone.yaml
needs:
- build-validation
permissions:
@@ -1,4 +1,4 @@
name: Build Dashboard (Release)
name: Build Standalone on Release
on:
push:
tags:
@@ -7,10 +7,10 @@ on:
jobs:
build-validation:
name: Build Test
uses: ./.github/workflows/test.yaml
uses: ./.github/workflows/run-lint.yaml
build:
name: Build and Upload Package
uses: ./.github/workflows/build-and-upload.yaml
uses: ./.github/workflows/build-and-publish-standalone.yaml
needs:
- build-validation
permissions:
+1 -14
View File
@@ -8,20 +8,7 @@ From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI
## Installation
To install the Harvester UI Extension in Rancher 2.10.0 and newer:
- Go to 'Extensions' in the Rancher UI (click on the jigsaw icon in the side bar)
- From the three-dots menu in the upper right of the page, open the menu and select 'Manage Repositories'
- From the Repositories page, click 'Create'
- For name, enter 'harvester-ui-extension'
- For Target, choose 'Git repository'
- For Git Repo URL, enter `https://github.com/harvester/harvester-ui-extension`
- For Git Branch, enter `gh-pages`
- Click 'Create`
- From the Repositories list, wait until the new Repository added updates to the 'Active' state
- Go back to 'Extensions'
- Click on the 'Available' tab if not already selected
- Click on 'Install' for the Harvester UI Extension
To install the Harvester UI Extension in Rancher 2.10.0 and newer, please refer to [installation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210) on harvester doc.
## Running for Development
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "harvester-ui-extension",
"version": "0.1.0",
"version": "1.5.0-rc2",
"private": false,
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@rancher/shell": "3.0.2-rc.3",
"@rancher/shell": "3.0.2-rc.6",
"cache-loader": "^4.1.0",
"color": "4.2.3",
"ip": "2.0.1",
@@ -47,4 +47,4 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^7.1.0"
}
}
}
@@ -190,6 +190,7 @@ export default {
v-clean-tooltip="{
placement: 'bottom-left',
}"
popper-class="upgrade-header-dropdown"
class="hand"
>
<slot name="button-content">
@@ -272,7 +273,7 @@ export default {
<p
v-if="overallMessage"
class="text-warning mb-20"
class="text-error mb-20"
>
{{ overallMessage }}
</p>
@@ -300,14 +301,14 @@ export default {
<ProgressBarList
:title="t('harvester.upgradePage.upgradeNode')"
:precent="nodesPercent"
:percent="nodesPercent"
:list="nodesStatus"
/>
<p class="bordered-section"></p>
<ProgressBarList
:title="t('harvester.upgradePage.upgradeSysService')"
:precent="sysServiceTotal"
:percent="sysServiceTotal"
:list="sysServiceUpgradeMessage"
/>
</div>
@@ -338,6 +339,12 @@ export default {
</div>
</template>
<style lang="scss">
.upgrade-header-dropdown .v-popper__arrow-container {
display: none;
}
</style>
<style lang="scss" scoped>
a {
float: right;
@@ -12,7 +12,7 @@ export default {
default: ''
},
precent: {
percent: {
type: Number,
default: 0
},
@@ -39,16 +39,18 @@ export default {
<template>
<div class="bar-list">
<h4>{{ title }} <span class="float-r text-info">{{ precent }}%</span></h4>
<h4>{{ title }} <span class="float-r text-info">{{ percent }}%</span></h4>
<div>
<div>
<Collapse v-model:open="open">
<template #title>
<div class="total-bar">
<span class="bar"><PercentageBar
:model-value="precent"
preferred-direction="MORE"
/></span>
<span class="bar">
<PercentageBar
:model-value="percent"
preferred-direction="MORE"
/>
</span>
<span
class="on-off"
@click="handleSwitch"
@@ -56,28 +58,26 @@ export default {
</div>
</template>
<template>
<div class="custom-content">
<div
v-for="(item, i) in list"
:key="i"
>
<p>
{{ item.name }} <span
class="status"
:class="{ [item.state]: true }"
>{{ item.state }}</span>
</p>
<PercentageBar
:model-value="item.percent"
preferred-direction="MORE"
/>
<p class="warning">
{{ item.message }}
</p>
</div>
<div class="custom-content">
<div
v-for="(item, i) in list"
:key="i"
>
<p>
{{ item.name }} <span
class="status"
:class="{ [item.state]: true }"
>{{ item.state }}</span>
</p>
<PercentageBar
:model-value="item.percent"
preferred-direction="MORE"
/>
<p class="warning">
{{ item.message }}
</p>
</div>
</template>
</div>
</Collapse>
</div>
</div>
@@ -93,7 +93,7 @@ export default {
.total-bar {
display: flex;
user-select: none;
.bar {
> .bar {
width: 85%;
}
.on-off {
@@ -102,25 +102,22 @@ export default {
}
}
.custom-content {
.item {
margin-bottom: 14px;
p {
margin-bottom: 4px;
}
.status {
float: right;
}
.Succeeded, .Upgrading, .Pending {
color: var(--success);
}
.failed {
color: var(--error)
}
.warning {
color: var(--error);
}
margin-bottom: 14px;
p {
margin-bottom: 4px;
}
.status {
float: right;
}
.Succeeded, .Upgrading, .Pending {
color: var(--success);
}
.failed {
color: var(--error)
}
.warning {
color: var(--error);
}
}
}
</style>
@@ -6,6 +6,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI_SETTING } from '../../config/settings';
import UnitInput from '@shell/components/form/UnitInput';
const DEFAULT_TYPE = 's3';
@@ -13,7 +14,7 @@ export default {
name: 'HarvesterEditBackupTarget',
components: {
LabeledInput, LabeledSelect, Tip, Password, MessageLink
LabeledInput, LabeledSelect, Tip, Password, MessageLink, UnitInput
},
mixins: [CreateEditView],
@@ -24,7 +25,9 @@ export default {
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = { type: '', endpoint: '' };
parseDefaultValue = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
}
// set default type to s3
@@ -62,6 +65,10 @@ export default {
}];
},
refreshIntervalInSecondEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('refreshIntervalInSecond');
},
isS3() {
return this.parseDefaultValue.type === DEFAULT_TYPE;
},
@@ -83,7 +90,9 @@ export default {
try {
parseDefaultValue = JSON.parse(neu.value);
} catch (err) {
parseDefaultValue = { type: '', endpoint: '' };
parseDefaultValue = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
}
this['parseDefaultValue'] = parseDefaultValue;
@@ -111,7 +120,6 @@ export default {
if (this.isS3 && !this.parseDefaultValue.virtualHostedStyle) {
this.parseDefaultValue.virtualHostedStyle = false;
}
if (!this.parseDefaultValue.type) {
delete this.value['value'];
} else {
@@ -120,7 +128,9 @@ export default {
},
useDefault() {
this['parseDefaultValue'] = { type: '', endpoint: '' };
this['parseDefaultValue'] = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
}
}
};
@@ -139,6 +149,23 @@ export default {
:options="typeOption"
@update:value="update"
/>
<UnitInput
v-if="refreshIntervalInSecondEnabled"
v-model:value="parseDefaultValue.refreshIntervalInSeconds"
:suffix="parseDefaultValue.refreshIntervalInSeconds <= 1 ? 'Second' : 'Seconds'"
:label="t('harvester.backup.refreshInterval.label')"
:mode="mode"
:positive="true"
class="mb-5"
required
@update:value="update"
/>
<Tip
v-if="refreshIntervalInSecondEnabled"
class="mb-20"
icon="icon icon-info"
:text="t('harvester.backup.refreshInterval.tip')"
/>
<LabeledInput
v-model:value="parseDefaultValue.endpoint"
class="mb-5"
@@ -75,7 +75,11 @@ export default {
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
return this.configArr.length >= csiDrivers.length;
}
},
allowEmptySnapshotClassNameFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('allowEmptySnapshotClassName');
},
},
methods: {
@@ -139,7 +143,7 @@ export default {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.volumeSnapshotClassName') }, true));
}
if (!config.value.backupVolumeSnapshotClassName) {
if (!this.allowEmptySnapshotClassNameFeatureEnabled && !config.value.backupVolumeSnapshotClassName) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName') }, true));
}
});
@@ -158,10 +162,18 @@ export default {
this.configArr.splice(idx, 1);
},
isBackupVolumeSnapshotRequired(driver) {
return driver === LONGHORN_DRIVER;
},
disableEdit(driver) {
return driver === LONGHORN_DRIVER;
},
isBackupVolumeSnapshotClassNameDisabled(driver) {
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
},
add() {
this.configArr.push({
key: '',
@@ -184,6 +196,7 @@ export default {
<InfoBox
v-for="(driver, idx) in configArr"
:key="idx"
class="box"
>
<button
:disabled="disableEdit(driver.key)"
@@ -226,9 +239,9 @@ export default {
<LabeledSelect
v-model:value="driver.value.backupVolumeSnapshotClassName"
:mode="mode"
required
:disabled="disableEdit(driver.key)"
:disabled="isBackupVolumeSnapshotClassNameDisabled(driver.key)"
:options="getVolumeSnapshotOptions(driver.key)"
:required="isBackupVolumeSnapshotRequired(driver.key)"
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
@keydown.native.enter.prevent="()=>{}"
@update:value="update"
+5 -2
View File
@@ -42,7 +42,9 @@ const featuresV141 = [
// TODO: add v1.4.2 official release note
const featuresV142 = [
...featuresV141
...featuresV141,
'refreshIntervalInSecond',
'allowEmptySnapshotClassName'
];
// TODO: add v1.5.0 official release note
@@ -51,7 +53,8 @@ const featuresV150 = [
'tpmPersistentState',
'efiPersistentState',
'untaggedNetworkSetting',
'skipSingleReplicaDetachedVol'
'skipSingleReplicaDetachedVol',
'thirdPartyStorage'
];
export const RELEASE_FEATURES = {
+3 -1
View File
@@ -31,7 +31,8 @@ import {
FINGERPRINT,
IMAGE_PROGRESS,
SNAPSHOT_TARGET_VOLUME,
IMAGE_VIRTUAL_SIZE
IMAGE_VIRTUAL_SIZE,
IMAGE_STORAGE_CLASS
} from './table-headers';
const TEMPLATE = HCI.VM_VERSION;
@@ -221,6 +222,7 @@ export function init($plugin, store) {
STATE,
NAME_COL,
NAMESPACE_COL,
IMAGE_STORAGE_CLASS,
IMAGE_PROGRESS,
IMAGE_DOWNLOAD_SIZE,
IMAGE_VIRTUAL_SIZE,
+1
View File
@@ -1,3 +1,4 @@
// TODO: delete this not used variable
export const MemoryUnit = [{
label: 'Mi',
value: 'Mi'
@@ -15,6 +15,7 @@ export const HCI = {
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os',
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
@@ -49,6 +50,7 @@ export const HCI = {
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_MANUFACTURER: 'manufacturer',
+10
View File
@@ -88,3 +88,13 @@ export const MACHINE_POOLS = {
align: 'center',
width: 100,
};
// The STORAGE_CLASS column in VM image list page
export const IMAGE_STORAGE_CLASS = {
name: 'imageStorageClass',
labelKey: 'harvester.tableHeaders.storageClass',
sort: 'imageStorageClass',
value: 'imageStorageClass',
align: 'left',
width: 200,
};
+5
View File
@@ -7,3 +7,8 @@ export const NETWORK_TYPE = {
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork'
};
export const VOLUME_MODE = {
BLOCK: 'Block',
FILE_SYSTEM: 'Filesystem'
};
@@ -4,6 +4,7 @@ import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { LONGHORN, METRIC } from '@shell/config/types';
import { Banner } from '@components/Banner';
import { UNIT_SUFFIX } from '../../utils/unit';
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
@@ -121,7 +122,7 @@ export default {
memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }iB`;
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
},
nodeType() {
@@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
import Loading from '@shell/components/Loading.vue';
import SortableTable from '@shell/components/SortableTable';
import Banner from '@components/Banner/Banner.vue';
import { UNIT_SUFFIX } from '../../utils/unit';
import metricPoller from '@shell/mixins/metric-poller';
import {
METRIC, NODE, LONGHORN, POD, EVENT
@@ -178,7 +178,7 @@ export default {
minExponent: 3,
maxExponent: 3,
maxPrecision: 2,
suffix: 'iB',
suffix: UNIT_SUFFIX,
};
const longhornDisks = Object.keys(diskStatus).map((key) => {
@@ -34,9 +34,14 @@ export default {
await allHash(hash);
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
const allStorages = this.allStorageClasses;
const defaultStorage = allStorages.find((s) => s.isDefault);
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
if (this.isLonghornV1Volume) {
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
} else {
this['storageClassName'] = this.nonLonghornV1StorageClasses[0]?.metadata?.name || '';
}
},
data() {
@@ -54,7 +59,7 @@ export default {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
return this.resources[0] || {};
},
namespaces() {
@@ -73,15 +78,33 @@ export default {
return out;
},
isLonghornV1Volume() {
return this.actionResource?.isLonghornV1 === true;
},
disableSave() {
return !(this.name && this.namespace && this.storageClassName);
},
storageClassOptions() {
allStorageClasses() {
const inStore = this.$store.getters['currentProduct'].inStore;
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
const out = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS) || [];
},
nonLonghornV1StorageClasses() {
return this.allStorageClasses.filter((s) => s.isLonghornV1 === false) || [];
},
storageClassOptions() {
let storages = this.allStorageClasses;
// Volume with non-longhorn v1 sc can't be exported to longhorn v1 sc image
if (!this.isLonghornV1Volume) {
storages = this.allStorageClasses.filter((s) => s.isLonghornV1 === false);
}
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
return {
@@ -90,7 +113,7 @@ export default {
};
}) || [];
return out;
return options;
},
},
@@ -0,0 +1,166 @@
<script>
import { mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI } from '../types';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import AppModal from '@shell/components/AppModal';
export default {
name: 'HarvesterImageDownloaderDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, AppModal
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return { errors: [], isOpen: false };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
downloadImageInProgress() {
return this.$store.getters['harvester-common/isDownloadImageInProgress'];
},
image() {
return this.resources[0] || {};
},
imageName() {
return this.image?.name || '';
},
imageVirtualSize() {
return this.image?.virtualSize || this.image?.downSize || '';
}
},
methods: {
async cancelDownload() {
const url = this.image?.links?.downloadcancel;
if (url) {
await this.$store.dispatch('harvester/request', { url });
}
},
async close() {
if (this.downloadImageInProgress) {
this.$store.commit('harvester-common/setDownloadImageCancel', true);
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
await this.cancelDownload();
}
this.$emit('close');
},
async startDownload(buttonCb) {
// clean the download image CRD first.
await this.cancelDownload();
this.$store.commit('harvester-common/setDownloadImageCancel', false);
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
this.errors = [];
const name = this.image?.name || '';
const namespace = this.image?.namespace || '';
const imageCrd = {
apiVersion: 'harvesterhci.io/v1beta1',
type: HCI.VM_IMAGE_DOWNLOADER,
kind: 'VirtualMachineImageDownloader',
metadata: {
name,
namespace
},
spec: { imageName: name }
};
const inStore = this.$store.getters['currentProduct'].inStore;
const imageCreate = await this.$store.dispatch(`${ inStore }/create`, imageCrd);
try {
await imageCreate.save();
this.$store.commit('harvester-common/setDownloadImageId', `${ namespace }/${ name }`, { root: true });
this.$store.dispatch('harvester-common/downloadImageProgress', { root: true });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
},
};
</script>
<template>
<app-modal
class="image-downloader-modal"
name="image-download-dialog"
height="auto"
:width="600"
:click-to-close="false"
@close="close"
>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.downloadImage.title') }}
</template>
<template #body>
<Banner color="info">
{{ t('harvester.modal.downloadImage.banner', { size: imageVirtualSize }) }}
</Banner>
{{ t('harvester.modal.downloadImage.startMessage') }}
<br /><br />
</template>
<template #actions>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
>
{{ err }}
</Banner>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
type="submit"
mode="download"
:disabled="downloadImageInProgress"
@click="startDownload"
/>
</div>
</div>
</template>
</Card>
</app-modal>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -5,6 +5,7 @@ import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import UnitInput from '@shell/components/form/UnitInput';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { GIBIBYTE } from '../utils/unit';
export default {
name: 'HarvesterVMQuotaDialog',
@@ -29,6 +30,7 @@ export default {
data() {
return {
GIBIBYTE,
totalSnapshotSize: '',
errors: []
};
@@ -44,7 +46,6 @@ export default {
},
methods: {
close() {
this.totalSnapshotSize = '';
this.$emit('close');
@@ -92,7 +93,7 @@ export default {
:input-exponent="3"
:increment="1024"
:output-modifier="true"
suffix="GiB"
:suffix="GIBIBYTE"
class="mb-20"
/>
</template>
@@ -10,7 +10,7 @@ import { Banner } from '@components/Banner';
import CreateEditView from '@shell/mixins/create-edit-view';
const DEFAULT_VALUE = { image: { repo: 'rancher/harvester-nvidia-driver-toolkit' } };
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
export default {
name: 'EditAddonNvidiaDriverToolkit',
@@ -122,7 +122,7 @@ export default {
>
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.image.repo"
v-model:value="valuesContentJson.image.repository"
:mode="mode"
:required="true"
label-key="harvester.addons.nvidiaDriverToolkit.image.repository"
@@ -20,7 +20,7 @@ import Loading from '@shell/components/Loading.vue';
import MessageLink from '@shell/components/MessageLink';
import { ADD_ONS } from '@pkg/harvester/config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
import { UNIT_SUFFIX } from '../../utils/unit';
import { _EDIT } from '@shell/config/query-params';
import { sortBy } from '@shell/utils/sort';
import { Banner } from '@components/Banner';
@@ -185,7 +185,7 @@ export default {
minExponent: 3,
maxExponent: 3,
maxPrecision: 2,
suffix: 'iB',
suffix: UNIT_SUFFIX,
};
const longhornDisks = Object.keys(diskStatus).map((key) => {
@@ -457,7 +457,7 @@ export default {
const devPath = d.spec?.devPath;
const deviceType = d.status?.deviceStatus?.details?.deviceType;
const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes;
const size = formatSi(sizeBytes, { increment: 1024 });
const size = formatSi(sizeBytes, { increment: 1024, suffix: UNIT_SUFFIX });
const parentDevice = d.status?.deviceStatus?.parentDevice;
const isChildAdded = this.newDisks.find((newDisk) => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath);
const name = d.displayName;
@@ -23,10 +23,9 @@ import Tags from '../../components/DiskTags';
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
const VOLUME_BINDING_MODE_IMMEDIATE = 'Immediate';
const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer';
@@ -63,7 +63,7 @@ export default {
diskSelector: null,
nodeSelector: null,
encrypted: 'false',
migratable: 'false',
migratable: this.value.thirdPartyStorageFeatureEnabled ? 'true' : 'false',
dataEngine: DATA_ENGINE_V2
};
}
@@ -318,7 +318,7 @@ export default {
:label="t('harvester.storage.parameters.migratable.label')"
:mode="mode"
:options="migratableOptions"
:disabled="true"
:disabled="!value.thirdPartyStorageFeatureEnabled"
/>
</div>
<div class="row mt-20">
@@ -61,9 +61,12 @@ export default {
images: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IMAGE }),
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
});
this['storageClassName'] = this.storageClassName || this.defaultStorageClassName();
this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE);
this.storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
const { securityParameters } = this.value.spec;
// edit and view mode should show the source image
@@ -98,14 +101,15 @@ export default {
}
return {
selectedImage: null,
images: [],
url: this.value.spec.url,
files: [],
resource: '',
headers: {},
fileUrl: '',
file: '',
selectedImage: null,
storageClasses: [],
images: [],
url: this.value.spec.url,
files: [],
resource: '',
headers: {},
fileUrl: '',
file: '',
};
},
@@ -158,9 +162,10 @@ export default {
storageClassOptions() {
const storages = this.value.spec?.securityParameters?.cryptoOperation === ENCRYPT ? this.encryptedStorageClasses : this.nonEncryptedStorageClasses;
const filteredStorages = this.value.thirdPartyStorageFeatureEnabled ? storages.filter((s) => !s.parameters?.backingImage) : storages
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) ;
return storages
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) // Lvm storage is not supported.
return filteredStorages
.map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
@@ -178,6 +183,9 @@ export default {
set(nue) {
this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue;
if (this.value.thirdPartyStorageFeatureEnabled) {
this.value.spec.targetStorageClassName = nue;
}
}
},
sourceImageOptions() {
@@ -256,6 +264,13 @@ export default {
} else { // URL / FILE / DECRYPT should use default storage class
this.storageClassName = this.defaultStorageClassName();
}
},
'storageClassName'(neu) {
const storageClass = this.storages.find((s) => s.id === neu);
if (storageClass && this.value.thirdPartyStorageFeatureEnabled) {
this.value.spec.backend = storageClass.isLonghornV1 ? 'backingimage' : 'cdi';
}
}
},
@@ -6,12 +6,11 @@ import { Checkbox } from '@components/Form/Checkbox';
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import Labels from '@shell/components/form/Labels';
import { Banner } from '@components/Banner';
import KeyValue from '@shell/components/form/KeyValue';
import NodeScheduling from '@shell/components/form/NodeScheduling';
import PodAffinity from '@shell/components/form/PodAffinity';
import UnitInput from '@shell/components/form/UnitInput';
import { randomStr } from '@shell/utils/string';
import { _CONFIG, _EDIT, _VIEW } from '@shell/config/query-params';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
@@ -47,7 +46,7 @@ export default {
PodAffinity,
Reserved,
UnitInput,
Labels,
Banner,
KeyValue,
},
@@ -321,34 +320,44 @@ export default {
</Tab>
<Tab
:name="t('generic.labels')"
:label="t('harvester.tab.instanceLabel')"
:weight="-5"
name="labels"
:label="t('generic.labels')"
:weight="-9"
>
<Labels
:default-container-class="'labels-and-annotations-container'"
:value="value"
<Banner color="info">
<t k="harvester.virtualMachine.labels.banner" />
</Banner>
<KeyValue
key="labels"
:value="value.labels"
:add-label="t('labels.addLabel')"
:mode="mode"
:display-side-by-side="false"
:show-annotations="false"
:show-label-title="false"
>
<template #labels="{toggler}">
<KeyValue
key="labels"
:value="value.instanceLabels"
:protected-keys="value.systemLabels || []"
:toggle-filter="toggler"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@input="value.setInstanceLabels($event)"
/>
</template>
</Labels>
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setLabels($event)"
/>
</Tab>
<Tab
name="instanceLabel"
:label="t('harvester.tab.instanceLabel')"
:weight="-10"
>
<Banner color="info">
<t k="harvester.virtualMachine.instanceLabels.banner" />
</Banner>
<KeyValue
key="instance-labels"
:value="value.instanceLabels"
:protected-keys="value.systemLabels || []"
:toggle-filter="toggler"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setInstanceLabels($event)"
/>
</Tab>
<Tab
name="advanced"
:label="t('harvester.tab.advanced')"
+79 -3
View File
@@ -21,6 +21,8 @@ import { InterfaceOption, VOLUME_DATA_SOURCE_KIND } from '../config/harvester-ma
import { HCI, VOLUME_SNAPSHOT } from '../types';
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
import { GIBIBYTE } from '../utils/unit';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
export default {
name: 'HarvesterVolume',
@@ -72,7 +74,8 @@ export default {
data() {
if (this.mode === _CREATE) {
this.value.spec.volumeMode = 'Block';
// default volumeMode to Block
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
this.value.spec.accessModes = ['ReadWriteMany'];
}
@@ -86,6 +89,7 @@ export default {
imageId,
snapshots: [],
images: [],
GIBIBYTE
};
},
@@ -124,6 +128,10 @@ export default {
return InterfaceOption;
},
volumeModeOptions() {
return Object.values(VOLUME_MODE);
},
imageOption() {
return sortBy(
this.images
@@ -177,6 +185,25 @@ export default {
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
},
isLonghornStorageClass() {
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.spec?.storageClassName) || {};
return selectedSC && selectedSC.isLonghorn;
},
showVolumeMode() {
// we won't let user choose volumeMode if source = vmimage
if (!this.value.thirdPartyStorageFeatureEnabled || this.isVMImage || !!this.value?.spec?.storageClassName === false) {
return false;
}
if (this.isLonghornStorageClass) {
return false;
}
return true;
},
storageClassOptions() {
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
@@ -235,7 +262,43 @@ export default {
}
},
watch: {
'source'(neu) {
if (neu === 'url') {
this.setBlockVolumeMode();
this.deleteVolumeForVmAnnotation();
}
},
'value.spec.storageClassName'() {
if (this.isLonghornStorageClass) {
this.setBlockVolumeMode();
this.deleteVolumeForVmAnnotation();
}
},
'value.spec.volumeMode'(neu) {
if (neu === VOLUME_MODE.FILE_SYSTEM) {
this.setVolumeForVmAnnotation();
} else if (neu === VOLUME_MODE.BLOCK ) {
this.deleteVolumeForVmAnnotation();
}
}
},
methods: {
setBlockVolumeMode() {
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
},
setVolumeForVmAnnotation() {
this.value.setAnnotation(HCI_ANNOTATIONS.VOLUME_FOR_VM, 'true');
},
deleteVolumeForVmAnnotation() {
if (this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_FOR_VM]) {
delete this.value.metadata.annotations[HCI_ANNOTATIONS.VOLUME_FOR_VM];
}
},
getAccessMode() {
if (!this.longhornV2LVMSupport) {
return ['ReadWriteMany'];
@@ -288,7 +351,7 @@ export default {
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
if (imageSize) {
this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }Gi`;
this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }${ GIBIBYTE }`;
}
}
this.update();
@@ -357,7 +420,7 @@ export default {
/>
<LabeledSelect
v-if="source === 'blank'"
v-if="isBlank"
v-model:value="value.spec.storageClassName"
:options="storageClassOptions"
:label="t('harvester.storage.storageClass.label')"
@@ -367,6 +430,18 @@ export default {
@update:value="update"
/>
<LabeledSelect
v-if="showVolumeMode"
v-model:value="value.spec.volumeMode"
:label="t('harvester.volume.volumeMode')"
:options="volumeModeOptions"
required
:disabled="!isCreate"
:mode="mode"
class="mb-20"
@update:value="update"
/>
<UnitInput
v-model:value="storage"
:label="t('harvester.volume.size')"
@@ -377,6 +452,7 @@ export default {
:disabled="value?.isLonghornV2 && isEdit"
required
class="mb-20"
:suffix="GIBIBYTE"
@update:value="update"
/>
@@ -1,6 +1,7 @@
<script>
import UnitInput from '@shell/components/form/UnitInput';
import InputOrDisplay from '@shell/components/InputOrDisplay';
import { GIBIBYTE } from '../../utils/unit';
export default {
name: 'HarvesterEditCpuMemory',
@@ -30,6 +31,7 @@ export default {
data() {
return {
GIBIBYTE,
localCpu: this.cpu,
localMemory: this.memory
};
@@ -63,7 +65,7 @@ export default {
if (String(this.localMemory).includes('Gi')) {
memory = this.localMemory;
} else {
memory = `${ this.localMemory }Gi`;
memory = `${ this.localMemory }${ GIBIBYTE }`;
}
if (memory.includes('null')) {
memory = null;
@@ -113,6 +115,7 @@ export default {
:output-modifier="true"
:disabled="disabled"
required
:suffix="GIBIBYTE"
class="mb-20"
@update:value="change"
/>
@@ -1,6 +1,6 @@
<script>
import UnitInput from '@shell/components/form/UnitInput';
import { MEBIBYTE } from '../../utils/unit';
export default {
name: 'HarvesterReserved',
@@ -20,7 +20,7 @@ export default {
},
data() {
return { memory: this.reservedMemory };
return { MEBIBYTE, memory: this.reservedMemory };
},
watch: {
@@ -47,6 +47,7 @@ export default {
:input-exponent="2"
:increment="1024"
:output-modifier="true"
:suffix="MEBIBYTE"
@update:value="change"
/>
</template>
@@ -6,8 +6,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ModalWithCard from '@shell/components/ModalWithCard';
import { clone } from '@shell/utils/object';
import { _VIEW } from '@shell/config/query-params';
import { _VIEW, _EDIT } from '@shell/config/query-params';
import { NAMESPACE } from '@shell/config/types';
import { HCI } from '../../types';
@@ -91,6 +90,9 @@ export default {
},
sshOption() {
if (this.mode === _VIEW || this.mode === _EDIT) {
return [];
}
const out = this.$store.getters['harvester/all'](HCI.SSH).map( (O) => {
return {
label: O.id,
@@ -126,10 +128,9 @@ export default {
this.checkedSsh = neu;
},
checkedSsh(val, old) {
if ( val.includes(_NEW)) {
this['checkedSsh'] = old;
this.update();
checkedSsh(val) {
// if click on Create a New...
if (val.includes(_NEW)) {
this.show();
}
}
@@ -175,6 +176,7 @@ export default {
if (res.id) {
this.checkedSsh.push(`${ this.namespace }/${ this.sshName }`);
this.update();
}
},
@@ -232,7 +234,9 @@ export default {
},
update() {
this.$emit('update:sshKey', clone(this.checkedSsh));
const sshKeys = this.checkedSsh.filter((key) => key !== _NEW);
this.$emit('update:sshKey', sshKeys);
},
}
};
@@ -243,13 +247,13 @@ export default {
<LabeledSelect
v-model:value="checkedSsh"
:label="t('harvester.virtualMachine.input.sshKey')"
:taggable="true"
:taggable="!disabled"
:mode="mode"
:multiple="true"
:searchable="searchable"
:disabled="disabled"
:options="sshOption"
@input="update"
@update:value="update"
/>
<ModalWithCard
@@ -7,8 +7,7 @@ import UnitInput from '@shell/components/form/UnitInput';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ModalWithCard from '@shell/components/ModalWithCard';
import { PVC } from '@shell/config/types';
import { PVC, STORAGE_CLASS } from '@shell/config/types';
import { clone } from '@shell/utils/object';
import { ucFirst, randomStr } from '@shell/utils/string';
import { removeObject } from '@shell/utils/array';
@@ -17,6 +16,7 @@ import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
import { SOURCE_TYPE } from '../../../config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
import { HCI } from '../../../types';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
export default {
emits: ['update:value'],
@@ -62,7 +62,7 @@ export default {
customVolumeMode: {
type: String,
default: 'Block'
default: VOLUME_MODE.Block
},
customAccessMode: {
@@ -116,6 +116,12 @@ export default {
return this.mode === _CREATE;
},
defaultStorageClass() {
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
return defaultStorage;
},
showVolumeTip() {
const imageName = this.getImageDisplayName(this.rows[0]?.image);
@@ -11,6 +11,7 @@ import { _CREATE } from '@shell/config/query-params';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../../../../types';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { GIBIBYTE } from '../../../../utils/unit';
export default {
name: 'HarvesterEditExisting',
@@ -59,6 +60,7 @@ export default {
}
return {
GIBIBYTE,
VOLUME_TYPE,
InterfaceOption,
loading: false,
@@ -109,6 +111,10 @@ export default {
return false;
}
if (pvc.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true') {
return false;
}
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
isBeingUsed = false;
} else if (pvc.attachVM) {
@@ -250,6 +256,7 @@ export default {
:label="t('harvester.fields.size')"
:mode="mode"
:disabled="true"
:suffix="GIBIBYTE"
@update:value="update"
/>
</InputOrDisplay>
@@ -12,6 +12,7 @@ import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { _VIEW } from '@shell/config/query-params';
import LabelValue from '@shell/components/LabelValue';
import { ucFirst } from '@shell/utils/string';
import { GIBIBYTE } from '../../../../utils/unit';
export default {
name: 'HarvesterEditVMImage',
@@ -67,6 +68,7 @@ export default {
data() {
return {
GIBIBYTE,
VOLUME_TYPE,
InterfaceOption,
loading: false,
@@ -90,7 +92,7 @@ export default {
imagesOption() {
return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
return {
label: `${ I.metadata.namespace }/${ I.spec.displayName }`,
label: this.imageOptionLabel(I),
value: I.id
};
});
@@ -116,8 +118,46 @@ export default {
});
},
thirdPartyStorageEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
},
isLonghornV2() {
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
},
selectedImage() {
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
},
imageVirtualSize() {
if (this.selectedImage?.virtualSize) {
return this.selectedImage.virtualSize.replace(' ', '');
}
return '0';
},
diskSize() {
const size = this.value?.size || '0';
return size;
},
imageVirtualSizeInByte() {
return Math.max(this.selectedImage?.status?.size, this.selectedImage?.status?.virtualSize);
},
diskSizeInByte() {
return parseSi(this.value?.size || '0');
},
showDiskTooSmallError() {
if (!this.thirdPartyStorageEnabled ) {
return false;
}
return this.imageVirtualSizeInByte > this.diskSizeInByte;
}
},
@@ -149,7 +189,7 @@ export default {
minExponent: 3,
});
this.value.size = `${ formatSize }Gi`;
this.value.size = `${ formatSize }${ GIBIBYTE }`;
}
},
deep: true,
@@ -158,7 +198,17 @@ export default {
},
methods: {
imageOptionLabel(image) {
let label = `${ image.metadata.namespace }/${ image.spec.displayName }`;
if (this.thirdPartyStorageEnabled) {
label += ` (${ image.imageStorageClass } / ${ image.virtualSize })`;
}
return label;
},
update() {
this.value.hasDiskError = this.showDiskTooSmallError;
this.$emit('update');
},
@@ -181,7 +231,7 @@ export default {
if (!isIsoImage) {
imageSizeGiB = Math.max(imageSizeGiB, 10);
}
this.value['size'] = `${ imageSizeGiB }Gi`;
this.value['size'] = `${ imageSizeGiB }${ GIBIBYTE }`;
}
this.update();
@@ -288,7 +338,7 @@ export default {
:mode="mode"
:required="validateRequired"
:disable="isLonghornV2"
suffix="GiB"
:suffix="GIBIBYTE"
@update:value="update"
/>
</InputOrDisplay>
@@ -341,5 +391,10 @@ export default {
class="mb-20"
:label="value.volumeBackups.error.message"
/>
<Banner
v-if="!isView && showDiskTooSmallError"
color="error"
:label="t('harvester.virtualMachine.volume.vmImageVolumeTip', {diskSize: diskSize, imageVirtualSize: imageVirtualSize})"
/>
</div>
</template>
@@ -12,6 +12,8 @@ import LabelValue from '@shell/components/LabelValue';
import { ucFirst } from '@shell/utils/string';
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
import { GIBIBYTE } from '../../../../utils/unit';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
export default {
name: 'HarvesterEditVolume',
@@ -58,6 +60,7 @@ export default {
data() {
return {
GIBIBYTE,
VOLUME_TYPE,
InterfaceOption,
loading: false,
@@ -69,6 +72,10 @@ export default {
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
},
thirdPartyStorageClassEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
},
encryptionValue() {
return ucFirst(String(this.value.isEncrypted));
},
@@ -89,6 +96,18 @@ export default {
return allPVCs.find((P) => P.id === `${ this.namespace }/${ this.value.volumeName }`);
},
showVolumeMode() {
if (!this.thirdPartyStorageClassEnabled || !!this.value?.storageClassName === false) {
return false;
}
if (this.isLonghornStorageClass) {
return false;
}
return true;
},
isDisabled() {
return !this.value.newCreateId && this.isEdit && this.isVirtualType;
},
@@ -110,7 +129,17 @@ export default {
isLonghornV2() {
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
}
},
isLonghornStorageClass() {
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
return selectedSC && selectedSC.isLonghorn;
},
volumeModeOptions() {
return Object.values(VOLUME_MODE);
},
},
watch: {
@@ -118,6 +147,8 @@ export default {
immediate: true,
handler(neu) {
this.value.accessMode = this.getAccessMode(neu);
this.value.volumeMode = this.getVolumeMode(neu, this.value.volumeMode);
this.update();
}
},
@@ -140,7 +171,7 @@ export default {
minExponent: 3,
});
this.value.size = `${ formatSize }Gi`;
this.value.size = `${ formatSize }${ GIBIBYTE }`;
}
},
deep: true,
@@ -149,6 +180,20 @@ export default {
},
methods: {
getVolumeMode(storageClassName, originalVolumeMode) {
if (!this.thirdPartyStorageClassEnabled) {
return VOLUME_MODE.BLOCK;
}
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
// longhorn v1, v2 use block volumeMode
if (storageClass && storageClass.isLonghorn) {
return VOLUME_MODE.BLOCK;
}
return originalVolumeMode;
},
getAccessMode(storageClassName) {
if (!this.longhornV2LVMSupport) {
return 'ReadWriteMany';
@@ -258,6 +303,7 @@ export default {
:required="validateRequired"
:label="t('harvester.fields.size')"
:disabled="isLonghornV2"
:suffix="GIBIBYTE"
@update:value="update"
/>
</InputOrDisplay>
@@ -283,6 +329,29 @@ export default {
/>
</InputOrDisplay>
</div>
<div
v-if="showVolumeMode"
data-testid="input-volume-mode"
class="col span-6"
>
<InputOrDisplay
:name="t('harvester.volume.volumeMode')"
:value="value.volumeMode"
:mode="mode"
>
<LabeledSelect
v-model:value="value.volumeMode"
:label="t('harvester.volume.volumeMode')"
:mode="mode"
:options="volumeModeOptions"
:disabled="isEdit"
required
@update:value="update"
/>
</InputOrDisplay>
</div>
</div>
<div class="row mb-20">
<div
v-if="value.volumeEncryptionFeatureEnabled && isView"
class="col span-6"
@@ -292,11 +361,9 @@ export default {
:value="encryptionValue"
/>
</div>
</div>
<div class="row mb-20">
<div
v-if="value.volumeBackups && isView"
class="col span-3"
class="col span-6"
>
<LabelValue
:name="t('harvester.virtualMachine.volume.readyToUse')"
@@ -10,8 +10,6 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import UnitInput from '@shell/components/form/UnitInput';
import Labels from '@shell/components/form/Labels';
import NodeScheduling from '@shell/components/form/NodeScheduling';
import PodAffinity from '@shell/components/form/PodAffinity';
import VGpuDevices from './VirtualMachineVGpuDevices/index';
@@ -65,7 +63,6 @@ export default {
PodAffinity,
AccessCredentials,
Reserved,
Labels,
PciDevices,
RestartVMDialog,
UnitInput,
@@ -195,7 +192,12 @@ export default {
return true;
},
isValidationPassed() {
// check if any disk hasDiskError is true
const hasError = this.diskRows.some((disk) => disk.hasDiskError === true);
return !hasError;
},
showCpuPinningBanner() {
if (!this.value.cpuPinningFeatureEnabled) {
return false;
@@ -489,6 +491,7 @@ export default {
:resource="value"
:cancel-event="true"
:mode="mode"
:validation-passed="isValidationPassed"
:can-yaml="isSingle ? true : false"
:errors="errors"
:generate-yaml="generateYaml"
@@ -579,7 +582,7 @@ export default {
:create-namespace="true"
:namespace="value.metadata.namespace"
:mode="mode"
:disabled="isWindows"
:disabled="isWindows || isEdit"
@update:sshKey="updateSSHKey"
@register-after-hook="registerAfterHook"
/>
@@ -697,39 +700,50 @@ export default {
/>
</Tab>
<Tab
name="labels"
:label="t('generic.labels')"
:weight="-9"
>
<Banner color="info">
<t k="harvester.virtualMachine.labels.banner" />
</Banner>
<KeyValue
key="labels"
:value="value.labels"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setLabels($event)"
/>
</Tab>
<Tab
name="instanceLabel"
:label="t('harvester.tab.instanceLabel')"
:weight="-8"
:weight="-10"
>
<Labels
:default-container-class="'labels-and-annotations-container'"
:value="value"
<Banner color="info">
<t k="harvester.virtualMachine.instanceLabels.banner" />
</Banner>
<KeyValue
key="instance-labels"
:value="value.instanceLabels"
:protected-keys="value.systemLabels || []"
:toggle-filter="toggler"
:add-label="t('labels.addLabel')"
:mode="mode"
:display-side-by-side="false"
:show-annotations="false"
:show-label-title="false"
>
<template #labels="{toggler}">
<KeyValue
key="labels"
:value="value.instanceLabels"
:protected-keys="value.systemLabels || []"
:toggle-filter="toggler"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setInstanceLabels($event)"
/>
</template>
</Labels>
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setInstanceLabels($event)"
/>
</Tab>
<Tab
name="advanced"
:label="t('harvester.tab.advanced')"
:weight="-9"
:weight="-11"
>
<div class="row mb-20">
<div class="col span-6">
@@ -2,7 +2,7 @@
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
import { METRIC, NODE } from '@shell/config/types';
import { formatSi, exponentNeeded, UNITS, parseSi } from '@shell/utils/units';
import { UNIT_SUFFIX } from '../utils/unit';
export default {
name: 'HarvesterMemoryUsed',
components: { ConsumptionGauge },
@@ -51,7 +51,7 @@ export default {
memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }iB`;
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
},
node() {
@@ -2,6 +2,7 @@
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
import { LONGHORN } from '@shell/config/types';
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { UNIT_SUFFIX } from '../utils/unit';
export default {
name: 'HarvesterStorageUsed',
@@ -75,13 +76,13 @@ export default {
allocatedUnits() {
const exponent = exponentNeeded(this.storageStats.total, 1024);
return `${ UNITS[exponent] }iB`;
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
},
usedUnits() {
const exponent = exponentNeeded(this.storageStats.maximum, 1024);
return `${ UNITS[exponent] }iB`;
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
},
formatUsed() {
+6 -1
View File
@@ -6,6 +6,7 @@ import harvesterCommonStore from './store/harvester-common';
import harvesterStore from './store/harvester-store';
import customValidators from './validators';
import { PRODUCT_NAME } from './config/harvester';
import { defineAsyncComponent } from 'vue';
// Init the package
export default function (plugin: IPlugin) {
@@ -20,7 +21,7 @@ export default function (plugin: IPlugin) {
// Built-in icon
plugin.metadata.icon = require('./icon.svg');
plugin.addProduct(require('./config/harvester-cluster'));
plugin.addDashboardStore(harvesterCommonStore.config.namespace, harvesterCommonStore.specifics, harvesterCommonStore.config);
@@ -28,4 +29,8 @@ export default function (plugin: IPlugin) {
plugin.validators = customValidators;
plugin.addRoutes(extensionRoutes);
plugin.register('component', 'NavHeaderRight', defineAsyncComponent(() =>
import('./components/HarvesterUpgradeHeader.vue')
));
}
+14
View File
@@ -99,6 +99,11 @@ harvester:
tip: Please enter a virtual machine name!
success: 'Virtual machine { name } cloned successfully.'
failed: 'Failed clone virtual machine!'
downloadImage:
title: Download Image
banner: 'This action takes a while depending on the image size ({ size }). Please be patient.'
startMessage : 'The download process will auto start once the conversion is complete.'
download: Download
exportImage:
title: Export to Image
name: Name
@@ -630,6 +635,7 @@ harvester:
setFirst: Set as root volume
saveVolume: Update Volume
encryption: Encryption
vmImageVolumeTip: Disk size ({diskSize}) should greater than selected image virtual size ({imageVirtualSize})
lockTooltip:
all: All volumes are encrypted.
partial: Some volumes are encrypted.
@@ -772,6 +778,10 @@ harvester:
inNamespaces: "Workloads in these namespaces"
namespaces:
label: Namespaces
instanceLabels:
banner: These labels are automatically synchronized to the virtual machine instance.
labels:
banner: These key values are added as labels to the virtual machine.
volume:
label: Volumes
@@ -781,6 +791,7 @@ harvester:
datasource: Data Source
details: Details
size: Size
volumeMode: Volume Mode
source: Source
kind: Kind
sourceOptions:
@@ -920,6 +931,9 @@ harvester:
label: Virtual Machine Backups
createText: Restore Backup
title: Restore Virtual Machine
refreshInterval:
label: Refresh Interval
tip: The interval for the backup target to sync data to harvester cluster. Set to 0 to disable refresh mechanism.
backupTargetTip: The endpoint used to access the backupstore. NFS and S3 are supported.
message:
noSetting:
@@ -8,9 +8,7 @@ import Banner from '@components/Banner/Banner.vue';
import MessageLink from '@shell/components/MessageLink';
import SortableTable from '@shell/components/SortableTable';
import { allHash, setPromiseResult } from '@shell/utils/promise';
import {
parseSi, formatSi, exponentNeeded, UNITS, createMemoryValues
} from '@shell/utils/units';
import { parseSi, formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { REASON } from '@shell/config/table-headers';
import {
EVENT, METRIC, NODE, SERVICE, PVC, LONGHORN, POD, COUNT, NETWORK_ATTACHMENT
@@ -27,22 +25,21 @@ import { isEmpty } from '@shell/utils/object';
import { HCI } from '../types';
import HarvesterUpgrade from '../components/HarvesterUpgrade';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { UNIT_SUFFIX } from '../utils/unit';
dayjs.extend(utc);
dayjs.extend(minMax);
const PARSE_RULES = {
memory: {
format: {
addSuffix: true,
firstSuffix: 'B',
increment: 1024,
maxExponent: 99,
maxPrecision: 2,
minExponent: 0,
startingExponent: 0,
suffix: 'iB',
}
format: {
addSuffix: true,
firstSuffix: UNIT_SUFFIX,
increment: 1024,
maxExponent: 99,
maxPrecision: 2,
minExponent: 0,
startingExponent: 0,
suffix: UNIT_SUFFIX,
}
};
@@ -402,13 +399,13 @@ export default {
storageUsed() {
const stats = this.storageStats;
return this.createMemoryValues(stats.maximum, stats.used);
return this.createDisplayValues(stats.maximum, stats.used);
},
storageAllocated() {
const stats = this.storageStats;
return this.createMemoryValues(stats.total, stats.scheduled);
return this.createDisplayValues(stats.total, stats.scheduled);
},
vmEvents() {
@@ -454,7 +451,7 @@ export default {
return total + node.memoryReserved;
}, 0);
return createMemoryValues(this.memoryTotal, useful);
return this.createDisplayValues(this.memoryTotal, useful);
},
availableNodes() {
@@ -494,7 +491,7 @@ export default {
},
ramUsed() {
return createMemoryValues(this.memoryTotal, this.metricAggregations?.memory);
return this.createDisplayValues(this.memoryTotal, this.metricAggregations?.memory);
},
hasMetricNodeSchema() {
@@ -516,10 +513,12 @@ export default {
},
methods: {
createMemoryValues(total, useful) {
createDisplayValues(total, useful) {
const parsedTotal = parseSi((total || '0').toString());
const parsedUseful = parseSi((useful || '0').toString());
const format = this.createMemoryFormat(parsedTotal);
const format = this.createFormat(parsedTotal);
const formattedTotal = formatSi(parsedTotal, format);
let formattedUseful = formatSi(parsedUseful, {
...format,
@@ -538,24 +537,24 @@ export default {
useful: Number(parsedUseful),
formattedTotal,
formattedUseful,
units: this.createMemoryUnits(parsedTotal),
units: this.createUnits(parsedTotal),
};
},
createMemoryFormat(n) {
const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
createFormat(n) {
const exponent = exponentNeeded(n, PARSE_RULES.format.increment);
return {
...PARSE_RULES.memory.format,
...PARSE_RULES.format,
maxExponent: exponent,
minExponent: exponent,
};
},
createMemoryUnits(n) {
const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
createUnits(n) {
const exponent = exponentNeeded(n, PARSE_RULES.format.increment);
return `${ UNITS[exponent] }${ PARSE_RULES.memory.format.suffix }`;
return `${ UNITS[exponent] }${ PARSE_RULES.format.suffix }`;
},
async fetchClusterResources(type, opt = {}, store) {
@@ -9,6 +9,7 @@ import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
import { allSettled } from '../utils/promise';
import { HCI, VOLUME_SNAPSHOT } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
const schema = {
id: HCI.VOLUME,
@@ -58,7 +59,6 @@ export default {
if (!pvcSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.VOLUME, isCreatable: false });
}
this.rows = hash.pvcs;
},
@@ -70,7 +70,10 @@ export default {
schema() {
return schema;
},
filterRows() {
// we only show the non golden image PVCs in the list
return this.rows.filter((pvc) => pvc?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] !== 'true');
},
headers() {
return [
STATE,
@@ -92,7 +95,8 @@ export default {
{
name: 'storageClass',
labelKey: 'tableHeaders.storageClass',
value: 'spec.storageClassName'
value: 'spec.storageClassName',
sort: 'spec.storageClassName'
},
{
name: 'AttachedVM',
@@ -146,7 +150,7 @@ export default {
:groupable="true"
default-sort-by="age"
:namespaced="true"
:rows="rows"
:rows="filterRows"
:schema="schema"
key-field="_key"
>
+15 -16
View File
@@ -25,6 +25,8 @@ import { HCI_SETTING } from '../../config/settings';
import { HCI } from '../../types';
import { parseVolumeClaimTemplates } from '../../utils/vm';
import impl, { QGA_JSON, USB_TABLET } from './impl';
import { GIBIBYTE } from '../../utils/unit';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
@@ -243,12 +245,6 @@ export default {
});
},
defaultStorageClass() {
const defaultStorage = this.$store.getters[`${ this.inStore }/all`](STORAGE_CLASS).find( (O) => O.isDefault);
return defaultStorage;
},
storageClassSetting() {
try {
const storageClassValue = this.$store.getters[`${ this.inStore }/all`](HCI.SETTING).find( (O) => O.id === HCI_SETTING.DEFAULT_STORAGE_CLASS)?.value;
@@ -260,7 +256,7 @@ export default {
},
customVolumeMode() {
return this.storageClassSetting.volumeMode || 'Block';
return this.storageClassSetting.volumeMode || VOLUME_MODE.BLOCK;
},
customAccessMode() {
@@ -448,7 +444,7 @@ export default {
if (!isIsoImage) {
imageSizeGiB = Math.max(imageSizeGiB, 10);
}
size = `${ imageSizeGiB }Gi`;
size = `${ imageSizeGiB }${ GIBIBYTE }`;
}
out.push({
@@ -462,7 +458,7 @@ export default {
type,
storageClassName: '',
image: this.imageId,
volumeMode: 'Block',
volumeMode: VOLUME_MODE.BLOCK,
isEncrypted,
volumeBackups,
});
@@ -521,7 +517,7 @@ export default {
accessMode = pvcResource?.spec?.accessModes?.[0] || 'ReadWriteMany';
size = pvcResource?.spec?.resources?.requests?.storage || '10Gi';
storageClassName = pvcResource?.spec?.storageClassName;
volumeMode = pvcResource?.spec?.volumeMode || 'Block';
volumeMode = pvcResource?.spec?.volumeMode || VOLUME_MODE.BLOCK;
volumeName = pvcResource?.metadata?.name || '';
}
@@ -558,7 +554,7 @@ export default {
volumeName,
container,
accessMode,
size: `${ formatSize }Gi`,
size: `${ formatSize }${ GIBIBYTE }`,
volumeMode: volumeMode || this.customVolumeMode,
image,
type,
@@ -900,8 +896,8 @@ export default {
},
/**
* Generate user data yaml which is decide by the "Install guest agent",
* "OS type", "SSH Keys" and user input.
* Generate user data yaml which is decided by the
* "Install guest agent", "OS type", "SSH keys" and user input.
* @param config
*/
getUserData(config) {
@@ -973,7 +969,7 @@ export default {
parseVolumeClaimTemplate(R, dataVolumeName) {
if (!String(R.size).includes('Gi') && R.size) {
R.size = `${ R.size }Gi`;
R.size = `${ R.size }${ GIBIBYTE }`;
}
const out = {
@@ -1218,7 +1214,6 @@ export default {
let secret = this.getSecret(vm.spec);
// const userData = this.getUserData({ osType: this.osType, installAgent: this.installAgent });
if (!secret && this.isEdit && this.secretRef) {
// When editing the vm, if the userData and networkData are deleted, we also need to clean up the secret values
secret = this.secretRef;
@@ -1620,9 +1615,13 @@ export default {
},
sshKey(neu, old) {
// refresh yaml editor to get the latest userScript
this.userScript = this.getUserData({ installAgent: this.installAgent, osType: this.osType });
this.refreshYamlEditor();
const _diff = difference(old, neu);
if (_diff.length && this.isEdit) {
if (_diff.length > 0 && this.isCreate) {
this.deleteSSHFromUserData(_diff);
}
}
@@ -0,0 +1,11 @@
import SteveModel from '@shell/plugins/steve/steve-class';
/**
* Class representing PCI Device Claim resource.
* @extends SteveModal
*/
export default class PCIDeviceClaim extends SteveModel {
cleanForSave(data, _forNew) {
return data;
}
}
@@ -0,0 +1,11 @@
import SteveModel from '@shell/plugins/steve/steve-class';
/**
* Class representing USB Device Claim resource.
* @extends SteveModal
*/
export default class USBDeviceClaim extends SteveModel {
cleanForSave(data, _forNew) {
return data;
}
}
+4
View File
@@ -66,4 +66,8 @@ export default class HarvesterResource extends SteveModel {
return this.hasLink('self') && this.$rootGetters['prefs/get'](DEV);
}
}
cleanForSave(data, _forNew) {
return data;
}
}
@@ -46,22 +46,37 @@ export default class HciPv extends HarvesterResource {
}
}
if (!this.isLonghorn || !this.isLonghornV2) {
const exportImageAction = {
action: 'exportImage',
enabled: this.hasAction('export') && !this.isEncrypted,
icon: 'icon icon-copy',
label: this.t('harvester.action.exportImage')
};
const takeSnapshotAction = {
action: 'snapshot',
enabled: this.hasAction('snapshot'),
icon: 'icon icon-backup',
label: this.t('harvester.action.snapshot'),
};
if (this.thirdPartyStorageFeatureEnabled) { // v1.5.0
out = [
{
action: 'exportImage',
enabled: this.hasAction('export') && !this.isEncrypted,
icon: 'icon icon-copy',
label: this.t('harvester.action.exportImage')
},
{
action: 'snapshot',
enabled: this.hasAction('snapshot'),
icon: 'icon icon-backup',
label: this.t('harvester.action.snapshot'),
},
exportImageAction,
takeSnapshotAction,
...out
];
// TODO: remove this block if Longhorn V2 engine supports restore volume snapshot
if (this.isLonghornV2) {
out = out.filter((action) => action.action !== takeSnapshotAction.action);
}
} else { // v1.4 / v1.3
if (!this.isLonghorn || !this.isLonghornV2) {
out = [
exportImageAction,
takeSnapshotAction,
...out
];
}
}
return [
@@ -321,15 +336,27 @@ export default class HciPv extends HarvesterResource {
return this.volumeProvider === LONGHORN_DRIVER;
}
get isLonghornV1() {
return this.isLonghorn && !this.isLonghornV2;
}
get isLonghornV2() {
return this.dataEngine === DATA_ENGINE_V2;
}
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
get resourceExternalLink() {
const host = window.location.host;
const { params } = this.currentRoute();
const volumeName = this.spec?.volumeName;
if (!this.isLonghorn) {
return null;
}
if (!volumeName) {
return null;
}
@@ -62,6 +62,14 @@ export default class HciStorageClass extends StorageClass {
return this.parameters?.encrypted === 'true';
}
get isLonghorn() {
return this.provisioner === LONGHORN_DRIVER;
}
get isLonghornV1() {
return this.provisioner === LONGHORN_DRIVER && this.longhornVersion === DATA_ENGINE_V1;
}
get isLonghornV2() {
return this.provisioner === LONGHORN_DRIVER && this.longhornVersion === DATA_ENGINE_V2;
}
@@ -73,4 +81,8 @@ export default class HciStorageClass extends StorageClass {
get volumeEncryptionFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
}
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
}
@@ -61,6 +61,10 @@ export default class HciUpgrade extends HarvesterResource {
return this?.status?.imageID;
}
cleanForSave(data, _forNew) {
return data;
}
get upgradeMessage() {
const upgradeMessage = [];
const nodeStatuses = this?.status?.nodeStatuses || {};
@@ -12,6 +12,7 @@ import { HCI } from '../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import HarvesterResource from './harvester';
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
import { UNIT_SUFFIX } from '../utils/unit';
const {
CSI_PROVISIONER_SECRET_NAME,
@@ -72,7 +73,7 @@ export default class HciVmImage extends HarvesterResource {
disabled: !this.isReady,
},
{
action: 'download',
action: 'imageDownload',
enabled: this.links?.download,
icon: 'icon icon-download',
label: this.t('asyncButton.download.action'),
@@ -199,6 +200,10 @@ export default class HciVmImage extends HarvesterResource {
return `${ this.metadata.namespace }/${ this.spec.displayName }`;
}
get imageStorageClass() {
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS] || '';
}
get imageMessage() {
if (this.uploadError) {
return ucFirst(this.uploadError);
@@ -239,8 +244,8 @@ export default class HciVmImage extends HarvesterResource {
return formatSi(size, {
increment: 1024,
maxPrecision: 2,
suffix: 'B',
firstSuffix: 'B',
suffix: UNIT_SUFFIX,
firstSuffix: UNIT_SUFFIX,
});
}
@@ -254,8 +259,8 @@ export default class HciVmImage extends HarvesterResource {
return formatSi(virtualSize, {
increment: 1024,
maxPrecision: 2,
suffix: 'B',
firstSuffix: 'B',
suffix: UNIT_SUFFIX,
firstSuffix: UNIT_SUFFIX,
});
}
@@ -385,7 +390,23 @@ export default class HciVmImage extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
}
download() {
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
imageDownload(resources = this) {
// spec.backend is introduced in v1.5.0. If it's not set, it's an old image can be downloaded via link
if (this.spec?.backend === 'cdi') {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterImageDownloader'
});
} else {
this.downloadViaLink();
}
}
downloadViaLink() {
window.location.href = this.links.download;
}
}
@@ -2,6 +2,7 @@ import { MODE, _CREATE } from '@shell/config/query-params';
import { HCI } from '../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import HarvesterResource from './harvester';
import { clone } from '@shell/utils/object';
export default class HciVmTemplate extends HarvesterResource {
get availableActions() {
@@ -71,4 +72,12 @@ export default class HciVmTemplate extends HarvesterResource {
get defaultVersion() {
return this.status?.defaultVersion;
}
get doneOverride() {
const detailLocation = clone(this.listLocation);
detailLocation.params.resource = HCI.VM_VERSION;
return detailLocation;
}
}
@@ -700,8 +700,25 @@ export default class VirtVm extends HarvesterResource {
return null;
}
get isBeingStopped() {
if (this && !this.isVMExpectedRunning && this.isVMCreated && this.vmi?.status?.phase !== VMIPhase.Succeeded) {
get isPending() {
if (this &&
!this.isVMExpectedRunning &&
this.isVMCreated &&
this.vmi?.status?.phase === VMIPhase.Pending
) {
return { status: VMIPhase.Pending };
}
return null;
}
get isStopping() {
if (this &&
!this.isVMExpectedRunning &&
this.isVMCreated &&
this.vmi?.status?.phase !== VMIPhase.Succeeded &&
this.vmi?.status?.phase !== VMIPhase.Pending
) {
return { status: STOPPING };
}
@@ -736,7 +753,7 @@ export default class VirtVm extends HarvesterResource {
}
get isUnschedulable() {
if (this.isBeingStopped || this.isStarting) {
if (this.isStopping || this.isStarting) {
const condition = this.status?.conditions?.find((c) => c.reason === UNSCHEDULABLE);
if (!!condition) {
@@ -852,7 +869,8 @@ export default class VirtVm extends HarvesterResource {
this.isUnschedulable?.status ||
this.isPaused?.status ||
this.isVMError?.status ||
this.isBeingStopped?.status ||
this.isPending?.status ||
this.isStopping?.status ||
this.isOff?.status ||
this.isError?.status ||
this.isRunning?.status ||
@@ -1183,6 +1201,10 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('efiPersistentState');
}
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
setInstanceLabels(val) {
if ( !this.spec?.template?.metadata?.labels ) {
set(this, 'spec.template.metadata.labels', {});
+2 -2
View File
@@ -1,13 +1,13 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.0.3",
"version": "1.5.0-rc2",
"private": false,
"rancher": {
"annotations": {
"catalog.cattle.io/display-name": "Harvester",
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
"catalog.cattle.io/rancher-version": ">= 2.10.1-0",
"catalog.cattle.io/rancher-version": ">= 2.11.0-0",
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
}
},
@@ -16,14 +16,12 @@ export default {
return [];
}
},
names: {
type: Array,
default: () => {
return [];
}
},
type: {
type: String,
required: true
@@ -69,12 +67,6 @@ export default {
return out;
},
plusMore() {
const remaining = this.toRemove.length - this.names.length;
return this.t('promptRemove.andOthers', { count: remaining });
},
},
watch: {
@@ -143,7 +135,7 @@ export default {
<div>
<div class="mt-10">
{{ t('promptRemove.attemptingToRemove', {type}) }}
<span v-clean-html="resourceNames(names, plusMore, t)"></span>
<span v-clean-html="resourceNames(names, t)"></span>
<div class="mt-10">
{{ t('harvester.virtualMachine.promptRemove.title') }}
+96 -7
View File
@@ -5,16 +5,33 @@ import { featureEnabled, getVersion } from '../utils/feature-flags';
const state = function() {
return {
latestBundleId: '',
bundlePending: false,
showBundleModal: false,
bundlePercentage: 0,
uploadingImages: [],
uploadingImageError: {},
// support bundle
latestBundleId: '',
bundlePending: false,
showBundleModal: false,
bundlePercentage: 0,
uploadingImages: [],
uploadingImageError: {},
// download cdi image
downloadImageId: '',
downloadImageInProgress: false,
isDownloadImageCancel: false,
};
};
const mutations = {
setDownloadImageId(state, id) {
state.downloadImageId = id;
},
setDownloadImageCancel(state, value) {
state.isDownloadImageCancel = value;
},
setDownloadImageInProgress(state, value) {
state.downloadImageInProgress = value;
},
setLatestBundleId(state, bundleId) {
state.latestBundleId = bundleId;
},
@@ -51,6 +68,14 @@ const getters = {
return state.latestBundleId;
},
isDownloadImageCancel(state) {
return state.isDownloadImageCancel;
},
isDownloadImageInProgress(state) {
return state.downloadImageInProgress;
},
isBundlePending(state) {
return state.bundlePending;
},
@@ -98,6 +123,70 @@ const getters = {
};
const actions = {
async downloadImageProgress({
state, dispatch, commit, rootGetters
}) {
const parse = Parse(window.history.href);
const id = state.downloadImageId; // id is image_ns / image_name
let imageCrd = await dispatch(
'harvester/find',
{ type: HCI.VM_IMAGE_DOWNLOADER, id },
{ root: true }
);
await commit('setDownloadImageInProgress', true);
let count = 0;
const timer = setInterval(async() => {
count = count + 1;
if (count % 3 === 0) {
// ws maybe disconnect, force to get the latest status
imageCrd = await dispatch(
'harvester/find',
{
type: HCI.VM_IMAGE_DOWNLOADER,
id,
opt: { force: true }
},
{ root: true }
);
}
// If is cancel, clear the timer
if (state.isDownloadImageCancel === true) {
clearInterval(timer);
return;
}
// converting image status becomes ready
if (imageCrd?.status?.status === 'Ready') {
imageCrd = rootGetters['harvester/byId'](HCI.VM_IMAGE_DOWNLOADER, id);
setTimeout(() => {
commit('setDownloadImageInProgress', false);
dispatch('promptModal'); // bring undefined data will close the promptModal
}, 600);
if (rootGetters['isMultiCluster']) {
const clusterId = rootGetters['clusterId'];
const prefix = `/k8s/clusters/${ clusterId }`;
window.location.href = `${ parse.origin }${ prefix }/v1/harvester/${ HCI.IMAGE }/${ id }/download`;
} else {
const link = `${ parse.origin }/v1/harvester/${ HCI.IMAGE }/${ id }/download`;
window.location.href = link;
}
clearInterval(timer);
}
}, 1000);
},
async bundleProgress({
state, dispatch, commit, rootGetters
}) {
@@ -117,7 +206,7 @@ const actions = {
const timer = setInterval(async() => {
count = count + 1;
if (count % 3 === 0) {
// ws mayby disconnect
// ws maybe disconnect
bundleCrd = await dispatch(
'harvester/find',
{
+53 -52
View File
@@ -1,56 +1,57 @@
export const HCI = {
VM: 'kubevirt.io.virtualmachine',
VMI: 'kubevirt.io.virtualmachineinstance',
VMIM: 'kubevirt.io.virtualmachineinstancemigration',
VM_TEMPLATE: 'harvesterhci.io.virtualmachinetemplate',
VM_VERSION: 'harvesterhci.io.virtualmachinetemplateversion',
IMAGE: 'harvesterhci.io.virtualmachineimage',
SSH: 'harvesterhci.io.keypair',
VOLUME: 'harvesterhci.io.volume',
USER: 'harvesterhci.io.user',
SETTING: 'harvesterhci.io.setting',
UPGRADE: 'harvesterhci.io.upgrade',
UPGRADE_LOG: 'harvesterhci.io.upgradelog',
SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup',
BACKUP: 'harvesterhci.io.virtualmachinebackup',
RESTORE: 'harvesterhci.io.virtualmachinerestore',
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
CLUSTER: 'harvesterhci.io.management.cluster',
DASHBOARD: 'harvesterhci.io.dashboard',
BLOCK_DEVICE: 'harvesterhci.io.blockdevice',
CLOUD_TEMPLATE: 'harvesterhci.io.cloudtemplate',
HOST: 'harvesterhci.io.host',
VERSION: 'harvesterhci.io.version',
SNAPSHOT: 'harvesterhci.io.volumesnapshot',
VM_SNAPSHOT: 'harvesterhci.io.vmsnapshot',
ALERTMANAGERCONFIG: 'harvesterhci.io.monitoring.alertmanagerconfig',
CLUSTER_FLOW: 'harvesterhci.io.logging.clusterflow',
CLUSTER_OUTPUT: 'harvesterhci.io.logging.clusteroutput',
FLOW: 'harvesterhci.io.logging.flow',
OUTPUT: 'harvesterhci.io.logging.output',
STORAGE: 'harvesterhci.io.storage',
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
KSTUNED: 'node.harvesterhci.io.ksmtuned',
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice',
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
ADD_ONS: 'harvesterhci.io.addon',
LINK_MONITOR: 'network.harvesterhci.io.linkmonitor',
SECRET: 'harvesterhci.io.secret',
INVENTORY: 'metal.harvesterhci.io.inventory',
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
VM: 'kubevirt.io.virtualmachine',
VMI: 'kubevirt.io.virtualmachineinstance',
VMIM: 'kubevirt.io.virtualmachineinstancemigration',
VM_TEMPLATE: 'harvesterhci.io.virtualmachinetemplate',
VM_VERSION: 'harvesterhci.io.virtualmachinetemplateversion',
IMAGE: 'harvesterhci.io.virtualmachineimage',
SSH: 'harvesterhci.io.keypair',
VOLUME: 'harvesterhci.io.volume',
USER: 'harvesterhci.io.user',
SETTING: 'harvesterhci.io.setting',
UPGRADE: 'harvesterhci.io.upgrade',
UPGRADE_LOG: 'harvesterhci.io.upgradelog',
SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup',
BACKUP: 'harvesterhci.io.virtualmachinebackup',
RESTORE: 'harvesterhci.io.virtualmachinerestore',
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
CLUSTER: 'harvesterhci.io.management.cluster',
DASHBOARD: 'harvesterhci.io.dashboard',
BLOCK_DEVICE: 'harvesterhci.io.blockdevice',
CLOUD_TEMPLATE: 'harvesterhci.io.cloudtemplate',
HOST: 'harvesterhci.io.host',
VERSION: 'harvesterhci.io.version',
SNAPSHOT: 'harvesterhci.io.volumesnapshot',
VM_SNAPSHOT: 'harvesterhci.io.vmsnapshot',
ALERTMANAGERCONFIG: 'harvesterhci.io.monitoring.alertmanagerconfig',
CLUSTER_FLOW: 'harvesterhci.io.logging.clusterflow',
CLUSTER_OUTPUT: 'harvesterhci.io.logging.clusteroutput',
FLOW: 'harvesterhci.io.logging.flow',
OUTPUT: 'harvesterhci.io.logging.output',
STORAGE: 'harvesterhci.io.storage',
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
KSTUNED: 'node.harvesterhci.io.ksmtuned',
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice',
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
ADD_ONS: 'harvesterhci.io.addon',
LINK_MONITOR: 'network.harvesterhci.io.linkmonitor',
SECRET: 'harvesterhci.io.secret',
INVENTORY: 'metal.harvesterhci.io.inventory',
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
};
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
+3
View File
@@ -0,0 +1,3 @@
export const UNIT_SUFFIX = 'i';
export const GIBIBYTE = 'Gi';
export const MEBIBYTE = 'Mi';
+2 -1
View File
@@ -2,6 +2,7 @@ import { PVC } from '@shell/config/types';
import { isValidMac, isValidDNSLabelName } from '@pkg/utils/regular';
import { SOURCE_TYPE } from '@pkg/config/harvester-map';
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
import { GIBIBYTE } from '../utils/unit';
const maxNameLength = 63;
@@ -88,7 +89,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
}
if (typeValue?.spec?.resources?.requests?.storage && !/^([0-9][0-9]{0,8})[a-zA-Z]+$/.test(typeValue?.spec?.resources?.requests?.storage)) {
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: `999999999 ${ GIBIBYTE }` });
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
}
+3 -1
View File
@@ -1,3 +1,5 @@
import { GIBIBYTE } from '../utils/unit';
export function volumeSize(size, getters, errors, validatorArgs, displayKey, value) {
if (!size) {
const key = getters['i18n/t']('harvester.volume.size');
@@ -6,7 +8,7 @@ export function volumeSize(size, getters, errors, validatorArgs, displayKey, val
}
if (size && !/^([0-9][0-9]{0,8})[a-zA-Z]+$/.test(size)) {
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: `999999999 ${ GIBIBYTE }` });
errors.push(message);
}
+9 -22
View File
@@ -3200,10 +3200,10 @@
resolved "https://registry.yarnpkg.com/@rancher/icons/-/icons-2.0.29.tgz#6546d69768c706bebd66bfa73b36aef3d105987a"
integrity sha512-qBBqfazS9y5VjV7fJDPNXmxd9AP/2uiE05mKFWP41kpbO+tEb62RnUBXCm14XLeScDZQcOuiAKVHMmvCFzF0BA==
"@rancher/shell@3.0.2-rc.3":
version "3.0.2-rc.3"
resolved "https://registry.yarnpkg.com/@rancher/shell/-/shell-3.0.2-rc.3.tgz#745d4970425ba0bddf1a8003ff7f82d408566da8"
integrity sha512-K5jCaNfyltjCSTQUgzFFntmO5HT1Uu+vM1aJsMCjiwiyrNPBsf0Z3ic9Is6HSvOs9DB+W5j7jvHwouuq0sxUlw==
"@rancher/shell@3.0.2-rc.6":
version "3.0.2-rc.6"
resolved "https://registry.yarnpkg.com/@rancher/shell/-/shell-3.0.2-rc.6.tgz#bde65a3da391c4aa0352e2adf8180900a2575000"
integrity sha512-UpNCv0iZH16nlo+x/4xVisIX20v1JrQC47GUUtXcGDw9pde6HnnBPxpN0cF32r+Kg7F0QRp4Yvjoljmpp1HS7g==
dependencies:
"@aws-sdk/client-ec2" "3.658.1"
"@aws-sdk/client-eks" "3.1.0"
@@ -3258,7 +3258,7 @@
entities "4.5.0"
eslint "7.32.0"
eslint-config-standard "16.0.3"
eslint-import-resolver-node "0.3.4"
eslint-import-resolver-node "0.3.9"
eslint-module-utils "2.6.1"
eslint-plugin-cypress "2.12.1"
eslint-plugin-import "2.31.0"
@@ -3279,7 +3279,7 @@
jest-serializer-vue "2.0.2"
jexl "2.3.0"
jquery "3.5.1"
js-cookie "2.2.1"
js-cookie "3.0.5"
js-yaml "4.1.0"
js-yaml-loader "1.2.2"
jsdiff "1.1.1"
@@ -6999,7 +6999,7 @@ debounce@^1.2.1:
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
debug@2.6.9, debug@^2.6.9:
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -7672,15 +7672,7 @@ eslint-config-standard@16.0.3:
resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz#6c8761e544e96c531ff92642eeb87842b8488516"
integrity sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==
eslint-import-resolver-node@0.3.4:
version "0.3.4"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==
dependencies:
debug "^2.6.9"
resolve "^1.13.1"
eslint-import-resolver-node@^0.3.9:
eslint-import-resolver-node@0.3.9, eslint-import-resolver-node@^0.3.9:
version "0.3.9"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==
@@ -10275,12 +10267,7 @@ js-beautify@^1.14.9, js-beautify@^1.6.12:
js-cookie "^3.0.5"
nopt "^7.2.0"
js-cookie@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
js-cookie@^3.0.5:
js-cookie@3.0.5, js-cookie@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==