mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 11:46:06 +08:00
Compare commits
60
Commits
new_vnc
...
v1.6.1-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20696f6801 | ||
|
|
e5b487ebcb | ||
|
|
f058698fb5 | ||
|
|
208496d6f7 | ||
|
|
967966e852 | ||
|
|
f19809da04 | ||
|
|
bc3d9389f2 | ||
|
|
6fbdf5244e | ||
|
|
7b21f6bb2e | ||
|
|
0f6faed2dc | ||
|
|
021de41e68 | ||
|
|
65f4405d29 | ||
|
|
4bae62d046 | ||
|
|
4cc60e22a8 | ||
|
|
19b6842b21 | ||
|
|
fa78229d3a | ||
|
|
c3bfaf00ea | ||
|
|
1a176960bc | ||
|
|
e82ba9d9f5 | ||
|
|
f7fc001abd | ||
|
|
d1c676ee2c | ||
|
|
c562dd73ba | ||
|
|
1cd5e5ca7d | ||
|
|
1aaa8f71c4 | ||
|
|
003a09d0a9 | ||
|
|
f08e632aad | ||
|
|
df1d7001bc | ||
|
|
41f78c83a7 | ||
|
|
f1a53311ce | ||
|
|
e0c6e905a9 | ||
|
|
1852e434f1 | ||
|
|
77c085d3a7 | ||
|
|
9dbe7d9015 | ||
|
|
3f19494d98 | ||
|
|
296601d38c | ||
|
|
5d9ba584b8 | ||
|
|
eab4ba3d0f | ||
|
|
4e5320dc72 | ||
|
|
ba7092f400 | ||
|
|
eec7c3573d | ||
|
|
f0014cc627 | ||
|
|
9aa311a7e3 | ||
|
|
013d9f1d7e | ||
|
|
8c044ba44c | ||
|
|
7b8592e07d | ||
|
|
53d2e67b4f | ||
|
|
1d146a40bd | ||
|
|
11d6943596 | ||
|
|
7b754457ec | ||
|
|
2cb838eded | ||
|
|
bdcb34ab14 | ||
|
|
8daf44b3f6 | ||
|
|
8044e74102 | ||
|
|
264d65cc6d | ||
|
|
342a82c8e0 | ||
|
|
e7cd5cc4b0 | ||
|
|
cf405be71e | ||
|
|
911d30d7ba | ||
|
|
1b4bfa0962 | ||
|
|
d0f6f18a8a |
@@ -3,5 +3,4 @@ addReviewers: true
|
||||
numberOfReviewers: 0
|
||||
reviewers:
|
||||
- a110605
|
||||
- houhoucoop
|
||||
- NickChungSUSE
|
||||
- houhoucoop
|
||||
@@ -0,0 +1,40 @@
|
||||
# .github/release.yml
|
||||
changelog:
|
||||
categories:
|
||||
- title: Features
|
||||
labels:
|
||||
- feat
|
||||
- title: Bug Fixes
|
||||
labels:
|
||||
- fix
|
||||
- title: Dependencies
|
||||
labels:
|
||||
- deps
|
||||
- title: Performance
|
||||
labels:
|
||||
- perf
|
||||
- title: Documentation
|
||||
labels:
|
||||
- docs
|
||||
- title: Style
|
||||
labels:
|
||||
- style
|
||||
- title: Build & Continuous Integration
|
||||
labels:
|
||||
- build
|
||||
- ci
|
||||
- title: Security
|
||||
labels:
|
||||
- security
|
||||
- title: Tests
|
||||
labels:
|
||||
- test
|
||||
- title: Refactoring
|
||||
labels:
|
||||
- refactor
|
||||
- title: Chores
|
||||
labels:
|
||||
- chore
|
||||
- title: Others
|
||||
labels:
|
||||
- other
|
||||
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":semanticCommitTypeAll(deps)",
|
||||
":semanticCommitScopeDisabled"
|
||||
],
|
||||
"baseBranches": [
|
||||
"master",
|
||||
"main",
|
||||
"/^release-harvester-v\\d+\\.\\d+$/"
|
||||
],
|
||||
"automergeMajor": false,
|
||||
"semanticCommits": "enabled",
|
||||
"semanticCommitType": "chore",
|
||||
"semanticCommitScope": "deps",
|
||||
"semanticCommitType": "deps",
|
||||
"prHourlyLimit": 12,
|
||||
"timezone": "Asia/Taipei",
|
||||
"schedule": ["after 10am on sunday"],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "[PR Management] Auto Assign Reviewer"
|
||||
name: "[PR Management] Auto Assign Reviewer & Assignee"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -9,8 +9,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
auto-assign:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: rancher/gh-issue-mgr/auto-assign-action@main
|
||||
with:
|
||||
configuration-path: .github/auto-assign-config.yaml
|
||||
- uses: rancher/gh-issue-mgr/auto-assign-action@main
|
||||
with:
|
||||
configuration-path: .github/auto-assign-config.yaml
|
||||
@@ -0,0 +1,94 @@
|
||||
name: "[PR Management] Add Labels"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
- 'release-harvester-v*'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
add-require-backport-label:
|
||||
if: github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.base.ref == 'main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
- name: Fetch release branches and PR labels
|
||||
id: fetch_info
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
repo="${{ github.repository }}"
|
||||
pr_number=${{ github.event.pull_request.number }}
|
||||
|
||||
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
||||
|
||||
if [[ -z "$release_branches" ]]; then
|
||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
|
||||
version="${latest_branch#release-harvester-v}"
|
||||
release_tag="v${version}.0"
|
||||
|
||||
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
||||
if echo "$tags" | grep -Fxq "$release_tag"; then
|
||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
label="require backport/v${version}"
|
||||
echo "should_label=true" >> "$GITHUB_OUTPUT"
|
||||
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
|
||||
|
||||
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
||||
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
|
||||
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Add label if needed
|
||||
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
|
||||
|
||||
add-backport-label:
|
||||
if: github.event.pull_request.draft == false &&
|
||||
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check conditions for backport label
|
||||
id: check
|
||||
run: |
|
||||
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
|
||||
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
|
||||
|
||||
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Add label if needed
|
||||
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
|
||||
version="${TARGET_BRANCH#release-harvester-v}"
|
||||
label="backport/v${version}"
|
||||
echo "Adding label $label"
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--add-label "$label"
|
||||
@@ -11,18 +11,19 @@ defaults:
|
||||
working-directory: ./
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
build-and-push-extension-catalog:
|
||||
needs: lint
|
||||
needs: check-version
|
||||
uses: ./.github/workflows/build-extension-catalog.yml
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||
|
||||
- name: Run Helm chart releaser
|
||||
uses: helm/chart-releaser-action@v1.4.1
|
||||
uses: helm/chart-releaser-action@v1.7.0
|
||||
with:
|
||||
charts_dir: ./charts
|
||||
env:
|
||||
|
||||
@@ -22,26 +22,27 @@ jobs:
|
||||
- name: Determine release tag
|
||||
id: determine_tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
if [[ "${{ github.event.release.tag_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:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
build-extension-charts:
|
||||
needs:
|
||||
- setup-release-tag
|
||||
- lint
|
||||
- check-version
|
||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
@@ -5,14 +5,20 @@ on:
|
||||
- v[1-9].*
|
||||
|
||||
jobs:
|
||||
build-validation:
|
||||
name: Build Test
|
||||
uses: ./.github/workflows/run-lint.yaml
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{github.ref_name}}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
build:
|
||||
name: Build and Upload Package
|
||||
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
||||
needs:
|
||||
- build-validation
|
||||
needs: check-version
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
name: "[PR Management] Add PR Label"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, edited]
|
||||
branches:
|
||||
- main
|
||||
- 'release-harvester-v*'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-assign-pr-label:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
- name: Setup Nodejs and yarn install
|
||||
uses: ./.github/actions/setup
|
||||
- name: Set PR label
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||
echo "PR_LABEL = $PR_LABEL"
|
||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- main
|
||||
- 'release-*'
|
||||
pull_request:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
branches:
|
||||
- main
|
||||
- 'release-*'
|
||||
@@ -22,6 +23,11 @@ jobs:
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
- name: Validate PR title lint
|
||||
if: github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
|
||||
|
||||
- name: Validate commit messages
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -138,6 +138,7 @@ To validate changes in a release series, switch to the appropriate `vX.Y-head` b
|
||||
- Examples:
|
||||
- Test `1.0.x` series → `v1.0-head`
|
||||
- Test `1.5.x` series → `v1.5-head`
|
||||
- Test `1.6.x` series → `v1.6-head`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||
|
||||
@@ -24,10 +24,10 @@ module.exports = {
|
||||
],
|
||||
'type-case': [2, 'always', 'lower-case'],
|
||||
'type-empty': [2, 'never'],
|
||||
'subject-case': [2, 'always', 'lower-case'],
|
||||
'subject-case': [0, 'never'],
|
||||
'subject-empty': [2, 'never'],
|
||||
'subject-full-stop': [2, 'never', '.'],
|
||||
'subject-max-length': [2, 'always', 72],
|
||||
'subject-max-length': [0, 'never'],
|
||||
'body-leading-blank': [2, 'always'],
|
||||
'body-max-line-length': [2, 'always', 100],
|
||||
'footer-leading-blank': [2, 'always'],
|
||||
|
||||
+11
-10
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "harvester-ui-extension",
|
||||
"version": "1.6.0-dev",
|
||||
"version": "1.6.1-rc1",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-class-static-block": "7.26.0",
|
||||
"@rancher/shell": "3.0.5-rc.3",
|
||||
"@babel/plugin-transform-class-static-block": "7.28.3",
|
||||
"@rancher/shell": "3.0.5-rc.8",
|
||||
"cache-loader": "^4.1.0",
|
||||
"color": "4.2.3",
|
||||
"ip": "2.0.1",
|
||||
@@ -16,21 +16,21 @@
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "~20.10.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"@types/node": "~20.19.0",
|
||||
"cronstrue": "2.59.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.10",
|
||||
"follow-redirects": "1.15.6",
|
||||
"follow-redirects": "1.15.11",
|
||||
"glob": "7.2.3",
|
||||
"glob-parent": "6.0.2",
|
||||
"json5": "2.2.3",
|
||||
"@types/lodash": "4.17.5",
|
||||
"@types/lodash": "4.17.20",
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.3.1",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.11.1",
|
||||
"roarr": "7.0.4",
|
||||
"semver": "7.5.4",
|
||||
"qs": "6.14.0",
|
||||
"roarr": "7.21.1",
|
||||
"semver": "7.7.2",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -46,6 +46,7 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/load": "^19.8.1",
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
|
||||
@@ -3,7 +3,9 @@ import { mapGetters } from 'vuex';
|
||||
import ActionMenu from '@shell/components/ActionMenuShell';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||
import { DOC } from '../config/doc-links';
|
||||
import { docLink } from '../utils/feature-flags';
|
||||
|
||||
const CATEGORY = {
|
||||
ui: [
|
||||
@@ -96,8 +98,12 @@ export default {
|
||||
return true;
|
||||
}
|
||||
|
||||
const description = this.t(setting.description, {}, true)?.toLowerCase() || '';
|
||||
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
||||
|
||||
// remove <a> tags from description
|
||||
if (description.includes('<a')) {
|
||||
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
|
||||
}
|
||||
// filter by description
|
||||
if (description.includes(searchQuery)) {
|
||||
return true;
|
||||
@@ -193,6 +199,19 @@ export default {
|
||||
}
|
||||
buttonDone(false);
|
||||
}
|
||||
},
|
||||
|
||||
getDocLinkParams(setting) {
|
||||
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
|
||||
|
||||
if (settingConfig?.docPath) {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
const url = docLink(DOC[settingConfig.docPath], version);
|
||||
|
||||
return { url };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -223,7 +242,7 @@ export default {
|
||||
Experimental
|
||||
</span>
|
||||
</h1>
|
||||
<h2 v-clean-html="t(setting.description, {}, true)">
|
||||
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -258,7 +258,7 @@ export default {
|
||||
},
|
||||
|
||||
softReboot() {
|
||||
this.vmResource.softrebootVM();
|
||||
this.vmResource.doSoftReboot();
|
||||
},
|
||||
|
||||
showKeysModal() {
|
||||
@@ -372,3 +372,12 @@ export default {
|
||||
background: rgb(40, 40, 40);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.vm-console .v-popper__arrow-container {
|
||||
display: none;
|
||||
}
|
||||
.vm-console .v-popper__popper{
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
<script>
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import InfoBox from '@shell/components/InfoBox';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { CSI_DRIVER } from '../../types';
|
||||
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||
|
||||
export default {
|
||||
name: 'CSIOnlineExpandValidation',
|
||||
|
||||
components: {
|
||||
Banner,
|
||||
InfoBox,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
try {
|
||||
await allHash({ csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) });
|
||||
this.fetchError = null;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch CSI drivers:', error); // eslint-disable-line no-console
|
||||
this.fetchError = this.t(
|
||||
'harvester.setting.csiOnlineExpandValidation.failedToLoadDrivers',
|
||||
{ error: error.message || error },
|
||||
true
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
configArr: [],
|
||||
parseError: null,
|
||||
fetchError: null,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
const initValue = this.value.value || this.value.default || '{}';
|
||||
|
||||
this.configArr = this.parseValue(initValue);
|
||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||
},
|
||||
|
||||
computed: {
|
||||
allErrors() {
|
||||
const errors = [];
|
||||
|
||||
if (this.fetchError) {
|
||||
errors.push(this.fetchError);
|
||||
}
|
||||
if (this.parseError) {
|
||||
errors.push(this.parseError);
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
csiDrivers() {
|
||||
if (this.fetchError) return [];
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
||||
},
|
||||
|
||||
provisioners() {
|
||||
const usedKeys = this.configArr.map(({ key }) => key);
|
||||
|
||||
return this.csiDrivers
|
||||
.filter(({ name }) => !usedKeys.includes(name))
|
||||
.map(({ name }) => name);
|
||||
},
|
||||
|
||||
provisionerValue() {
|
||||
return [
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false },
|
||||
];
|
||||
},
|
||||
|
||||
disableAdd() {
|
||||
return this.parseError || this.fetchError || this.configArr.length >= this.csiDrivers.length;
|
||||
},
|
||||
|
||||
disableConfigEditing() {
|
||||
return this.parseError || this.fetchError;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.value'(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.configArr = this.parseValue(newVal || '{}');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
_convertToBoolean(value) {
|
||||
if (typeof value === 'boolean') return value;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const lowerCaseValue = value.toLowerCase();
|
||||
|
||||
if (lowerCaseValue === 'true') return true;
|
||||
if (lowerCaseValue === 'false') return false;
|
||||
}
|
||||
|
||||
return false; // default to false for any other string or non-boolean type
|
||||
},
|
||||
parseValue(raw) {
|
||||
try {
|
||||
const json = JSON.parse(raw);
|
||||
|
||||
this.parseError = null;
|
||||
|
||||
return Object.entries(json).map(([key, value]) => ({
|
||||
key,
|
||||
value: this._convertToBoolean(value),
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('[CSIOnlineExpandValidation] JSON Parsing Error:', raw, e); // eslint-disable-line no-console
|
||||
this.parseError = this.t(
|
||||
'harvester.setting.csiOnlineExpandValidation.invalidJsonFormat',
|
||||
{ error: e.message },
|
||||
true
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
stringifyConfig() {
|
||||
const obj = {};
|
||||
|
||||
this.configArr.forEach(({ key, value }) => {
|
||||
obj[key] = value;
|
||||
});
|
||||
|
||||
return this.configArr.length ? JSON.stringify(obj) : '';
|
||||
},
|
||||
|
||||
update() {
|
||||
this.value.value = this.stringifyConfig();
|
||||
},
|
||||
|
||||
willSave() {
|
||||
const errors = [];
|
||||
|
||||
this.configArr.forEach(({ key }) => {
|
||||
if (!key) {
|
||||
errors.push(
|
||||
this.t('validation.required', { key: this.t('harvester.setting.csiOnlineExpandValidation.provisioner') }, true)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.value.value = this.stringifyConfig();
|
||||
|
||||
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.configArr = this.parseValue(this.value.default || '{}');
|
||||
this.update();
|
||||
},
|
||||
|
||||
disableEdit(driverKey) {
|
||||
return this.fetchError || driverKey === LONGHORN_DRIVER;
|
||||
},
|
||||
|
||||
add() {
|
||||
if (this.disableConfigEditing) return;
|
||||
|
||||
this.configArr.push({ key: '', value: true });
|
||||
},
|
||||
|
||||
remove(index) {
|
||||
if (this.disableConfigEditing) return;
|
||||
|
||||
this.configArr.splice(index, 1);
|
||||
this.update();
|
||||
},
|
||||
|
||||
onValueChange(idx, newVal) {
|
||||
if (this.disableConfigEditing) return;
|
||||
|
||||
const val = newVal === 'true' ? true : newVal === 'false' ? false : newVal;
|
||||
|
||||
this.configArr[idx].value = val;
|
||||
this.update();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Banner
|
||||
v-for="(errorMsg, index) in allErrors"
|
||||
:key="index"
|
||||
color="error"
|
||||
>
|
||||
{{ errorMsg }}
|
||||
</Banner>
|
||||
<InfoBox
|
||||
v-for="(driver, idx) in configArr"
|
||||
:key="idx"
|
||||
class="box"
|
||||
>
|
||||
<button
|
||||
class="role-link btn btn-sm remove"
|
||||
type="button"
|
||||
:disabled="disableEdit(driver.key)"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="driver.key"
|
||||
label-key="harvester.setting.csiOnlineExpandValidation.provisioner"
|
||||
required
|
||||
searchable
|
||||
:mode="mode"
|
||||
:disabled="disableEdit(driver.key)"
|
||||
:options="provisioners"
|
||||
@update:value="update"
|
||||
@keydown.native.enter.prevent
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="driver.value"
|
||||
:value="driver.value.toString()"
|
||||
label-key="harvester.setting.csiOnlineExpandValidation.value"
|
||||
required
|
||||
searchable
|
||||
:mode="mode"
|
||||
:disabled="disableEdit(driver.key)"
|
||||
:options="provisionerValue"
|
||||
@update:value="val => onValueChange(idx, val)"
|
||||
@keydown.native.enter.prevent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoBox>
|
||||
|
||||
<button
|
||||
class="btn btn-sm role-primary"
|
||||
:disabled="disableAdd"
|
||||
@click="add"
|
||||
>
|
||||
{{ t('generic.add') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
position: relative;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.remove {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { SECRET } from '@shell/config/types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
|
||||
import YamlEditor from '@shell/components/YamlEditor';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterRancherCluster',
|
||||
|
||||
components: {
|
||||
RadioGroup,
|
||||
FileSelector,
|
||||
YamlEditor
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
const data = this.value.value || this.value.default || '{}';
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
parseDefaultValue = {
|
||||
kubeConfig: '',
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: parsed.removeUpstreamClusterWhenNamespaceIsDeleted || false
|
||||
};
|
||||
} catch (error) {
|
||||
parseDefaultValue = {
|
||||
kubeConfig: '',
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
parseDefaultValue,
|
||||
errors: [],
|
||||
existingSecret: null,
|
||||
};
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.checkExistingSecret();
|
||||
if (this.registerBeforeHook) {
|
||||
this.registerBeforeHook(this.willSave, 'willSave');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeySelected: createOnSelected('parseDefaultValue.kubeConfig'),
|
||||
|
||||
update() {
|
||||
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||
this.value['value'] = JSON.stringify({ removeUpstreamClusterWhenNamespaceIsDeleted: true });
|
||||
} else {
|
||||
this.value['value'] = this.value.default || '{}';
|
||||
}
|
||||
},
|
||||
|
||||
async checkExistingSecret() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||
const secrets = this.$store.getters[`${ inStore }/all`](SECRET) || [];
|
||||
|
||||
this.existingSecret = secrets.find((secret) => secret.metadata.name === 'rancher-cluster-config' &&
|
||||
secret.metadata.namespace === 'harvester-system'
|
||||
);
|
||||
|
||||
// If the secret exists and has data, populate the kubeConfig
|
||||
if (this.existingSecret?.data?.kubeConfig) {
|
||||
const decodedContent = atob(this.existingSecret.data.kubeConfig);
|
||||
|
||||
this.parseDefaultValue.kubeConfig = decodedContent;
|
||||
this.$nextTick(() => {
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async createOrUpdateRancherKubeConfigSecret() {
|
||||
this.errors = [];
|
||||
// Check if kubeConfig is provided
|
||||
if (!this.parseDefaultValue.kubeConfig) {
|
||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.setting.rancherCluster.kubeConfig') }, true));
|
||||
|
||||
return Promise.reject(this.errors);
|
||||
}
|
||||
|
||||
try {
|
||||
let secret;
|
||||
|
||||
if (this.existingSecret) {
|
||||
secret = this.existingSecret;
|
||||
secret.setData('kubeConfig', this.parseDefaultValue.kubeConfig);
|
||||
} else {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
secret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
metadata: {
|
||||
name: 'rancher-cluster-config',
|
||||
namespace: 'harvester-system'
|
||||
},
|
||||
type: 'secret',
|
||||
data: { kubeConfig: btoa(this.parseDefaultValue.kubeConfig) }
|
||||
});
|
||||
}
|
||||
await secret.save();
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
|
||||
return Promise.reject(this.errors);
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRancherKubeConfigSecret() {
|
||||
if (this.existingSecret) {
|
||||
this.existingSecret.remove();
|
||||
}
|
||||
},
|
||||
|
||||
async willSave() {
|
||||
// Only create or update secret if enabled
|
||||
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||
await this.createOrUpdateRancherKubeConfigSecret();
|
||||
} else {
|
||||
await this.deleteRancherKubeConfigSecret();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.parseDefaultValue = {
|
||||
kubeConfig: '',
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted'(val, oldVal) {
|
||||
if (val && !oldVal && this.existingSecret?.data?.kubeConfig) {
|
||||
// Populate kubeConfig with the existing secret value
|
||||
this.parseDefaultValue.kubeConfig = atob(this.existingSecret.data.kubeConfig);
|
||||
}
|
||||
},
|
||||
'parseDefaultValue.kubeConfig'(val) {
|
||||
this.$refs.yaml?.updateValue(val);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-12">
|
||||
<RadioGroup
|
||||
v-model:value="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||
:label="t('harvester.setting.rancherCluster.removeUpstreamClusterWhenNamespaceIsDeleted')"
|
||||
name="removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||
:options="[true, false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||
class="row mt-20"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<FileSelector
|
||||
class="btn btn-sm bg-primary mb-10"
|
||||
:label="t('generic.readFromFile')"
|
||||
@selected="onKeySelected"
|
||||
/>
|
||||
<YamlEditor
|
||||
ref="yaml"
|
||||
v-model:value="parseDefaultValue.kubeConfig"
|
||||
class="yaml-editor"
|
||||
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$yaml-height: 540px;
|
||||
|
||||
:deep() .yaml-editor{
|
||||
flex: 1;
|
||||
min-height: $yaml-height;
|
||||
& .code-mirror .CodeMirror {
|
||||
position: initial;
|
||||
height: auto;
|
||||
min-height: $yaml-height;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,9 @@ import { NAMESPACE } from '@shell/config/types';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
const SELECT_ALL = 'select_all';
|
||||
const UNSELECT_ALL = 'unselect_all';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterBundleNamespaces',
|
||||
|
||||
@@ -11,47 +14,74 @@ export default {
|
||||
mixins: [CreateEditView],
|
||||
|
||||
async fetch() {
|
||||
this.loading = true;
|
||||
|
||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||
|
||||
try {
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||
const response = await this.$store.dispatch('harvester/request', { url });
|
||||
|
||||
this.defaultNamespaces = response.data || [];
|
||||
} catch (error) {
|
||||
this.defaultNamespaces = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
let namespaces = [];
|
||||
const namespacesStr = this.value?.value || this.value?.default || '';
|
||||
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
|
||||
|
||||
if (namespacesStr) {
|
||||
namespaces = namespacesStr.split(',');
|
||||
}
|
||||
|
||||
return { namespaces };
|
||||
return {
|
||||
namespaces,
|
||||
defaultNamespaces: [],
|
||||
loading: true
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
allNamespaces() {
|
||||
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||
},
|
||||
|
||||
filteredNamespaces() {
|
||||
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||
|
||||
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
||||
},
|
||||
|
||||
namespaceOptions() {
|
||||
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
|
||||
return {
|
||||
label: N.id,
|
||||
value: N.id
|
||||
};
|
||||
});
|
||||
const allSelected =
|
||||
this.namespaces.length === this.filteredNamespaces.length &&
|
||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||
|
||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||
|
||||
return [controlOption, ...this.filteredNamespaces];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
update() {
|
||||
const namespaceStr = this.namespaces.join(',');
|
||||
update(selected) {
|
||||
if (selected.includes(SELECT_ALL)) {
|
||||
this.namespaces = [...this.filteredNamespaces];
|
||||
} else if (selected.includes(UNSELECT_ALL)) {
|
||||
this.namespaces = [];
|
||||
} else {
|
||||
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||
}
|
||||
|
||||
this.value['value'] = namespaceStr;
|
||||
this.value.value = this.namespaces.join(',');
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.value': {
|
||||
handler(neu) {
|
||||
if (neu === this.value.default || !neu) {
|
||||
this.namespaces = [];
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
'value.value'(newVal) {
|
||||
const raw = newVal || this.value.default || '';
|
||||
|
||||
this.namespaces = raw ? raw.split(',') : [];
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -62,6 +92,7 @@ export default {
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="namespaces"
|
||||
:loading="loading"
|
||||
:multiple="true"
|
||||
label-key="nameNsDescription.namespace.label"
|
||||
:mode="mode"
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterUpgradeConfig',
|
||||
|
||||
components: {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup
|
||||
},
|
||||
mixins: [CreateEditView],
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
|
||||
} catch (error) {
|
||||
parseDefaultValue = JSON.parse(this.value.default);
|
||||
}
|
||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||
|
||||
return {
|
||||
parseDefaultValue,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
strategyOptions() {
|
||||
return [
|
||||
{ value: 'sequential', label: 'sequential' },
|
||||
{ value: 'skip', label: 'skip' },
|
||||
{ value: 'parallel', label: 'parallel' }
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.update();
|
||||
},
|
||||
|
||||
methods: {
|
||||
normalizeValue(obj) {
|
||||
if (!obj.imagePreloadOption) {
|
||||
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
||||
}
|
||||
if (!obj.imagePreloadOption.strategy) {
|
||||
obj.imagePreloadOption.strategy = { type: 'sequential' };
|
||||
}
|
||||
if (!obj.imagePreloadOption.strategy.type) {
|
||||
obj.imagePreloadOption.strategy.type = 'sequential';
|
||||
}
|
||||
// Only set concurrency if type is 'parallel'
|
||||
if (obj.imagePreloadOption.strategy.type === 'parallel') {
|
||||
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
|
||||
obj.imagePreloadOption.strategy.concurrency = 0;
|
||||
}
|
||||
} else {
|
||||
delete obj.imagePreloadOption.strategy.concurrency;
|
||||
}
|
||||
if (typeof obj.restoreVM !== 'boolean') {
|
||||
obj.restoreVM = false;
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
update() {
|
||||
try {
|
||||
// Clone to avoid mutating the form state
|
||||
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
|
||||
|
||||
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
|
||||
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
|
||||
delete valueToSave.imagePreloadOption.strategy.concurrency;
|
||||
}
|
||||
}
|
||||
|
||||
this.value['value'] = JSON.stringify(valueToSave, null, 2);
|
||||
this.errors = [];
|
||||
} catch (e) {
|
||||
this.errors = ['Invalid JSON'];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: {
|
||||
handler(neu) {
|
||||
let parseDefaultValue;
|
||||
|
||||
try {
|
||||
parseDefaultValue = JSON.parse(neu.value);
|
||||
} catch (err) {
|
||||
parseDefaultValue = JSON.parse(this.value.default);
|
||||
}
|
||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||
this['parseDefaultValue'] = parseDefaultValue;
|
||||
this.update();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="row"
|
||||
@input="update"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
|
||||
<LabeledSelect
|
||||
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.upgrade.strategyType')"
|
||||
:options="strategyOptions"
|
||||
@update:value="update"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
|
||||
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.upgrade.concurrency')"
|
||||
min="0"
|
||||
type="number"
|
||||
/>
|
||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
|
||||
<RadioGroup
|
||||
v-model:value="parseDefaultValue.restoreVM"
|
||||
class="mb-20"
|
||||
name="restoreVM"
|
||||
:options="[true, false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="update"
|
||||
/>
|
||||
<div
|
||||
v-if="errors.length"
|
||||
class="error"
|
||||
>
|
||||
{{ errors[0] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error {
|
||||
color: #d9534f;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,270 @@
|
||||
<script>
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { Banner } from '@components/Banner';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||
import { NODE } from '@shell/config/types';
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import { HCI } from '../../types';
|
||||
|
||||
const DEFAULT_NETWORK = {
|
||||
clusterNetwork: '',
|
||||
vlan: '',
|
||||
range: '',
|
||||
exclude: [],
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'VMMigrationNetwork',
|
||||
|
||||
components: {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup,
|
||||
Banner,
|
||||
ArrayList,
|
||||
},
|
||||
|
||||
props: {
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({ value: '' }),
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
try {
|
||||
await allHash({
|
||||
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||
});
|
||||
|
||||
this.fetchError = null;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
|
||||
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const { parsed, enabled, parseError } = this.parseInitialValue();
|
||||
|
||||
return {
|
||||
enabled,
|
||||
network: { ...DEFAULT_NETWORK, ...parsed },
|
||||
fetchError: null,
|
||||
parseError,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||
},
|
||||
|
||||
computed: {
|
||||
allErrors() {
|
||||
return [this.fetchError, this.parseError].filter(Boolean);
|
||||
},
|
||||
|
||||
clusterNetworkOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||
|
||||
return networks.map((net) => ({
|
||||
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
|
||||
value: net.id,
|
||||
disabled: !net.isReadyForStorageNetwork,
|
||||
}));
|
||||
},
|
||||
|
||||
disableEdit() {
|
||||
return !!(this.fetchError || this.parseError);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
parseInitialValue() {
|
||||
let parsed = {};
|
||||
let enabled = false;
|
||||
let parseError = null;
|
||||
|
||||
try {
|
||||
if (typeof this.value.value === 'string' && this.value.value.trim()) {
|
||||
parsed = JSON.parse(this.value.value);
|
||||
enabled = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
|
||||
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed.exclude)) {
|
||||
parsed.exclude = [];
|
||||
}
|
||||
|
||||
return {
|
||||
parsed, enabled, parseError
|
||||
};
|
||||
},
|
||||
|
||||
clearErrors() {
|
||||
this.fetchError = null;
|
||||
this.parseError = null;
|
||||
},
|
||||
|
||||
inputVlan(val) {
|
||||
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
|
||||
this.update();
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.network = { ...DEFAULT_NETWORK };
|
||||
this.value.value = '';
|
||||
this.enabled = false;
|
||||
this.clearErrors();
|
||||
},
|
||||
|
||||
update() {
|
||||
try {
|
||||
this.value.value = this.enabled ? JSON.stringify({
|
||||
...this.network,
|
||||
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
|
||||
}) : '';
|
||||
} catch (e) {
|
||||
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
|
||||
this.value.value = '';
|
||||
}
|
||||
},
|
||||
|
||||
validateInputs() {
|
||||
const errors = [];
|
||||
|
||||
if (!this.network.clusterNetwork) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
|
||||
}
|
||||
|
||||
if (!this.network.range) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
|
||||
} else if (!isValidCIDR(this.network.range)) {
|
||||
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
|
||||
}
|
||||
|
||||
if (this.network.vlan === '') {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
|
||||
} else {
|
||||
const vlan = Number(this.network.vlan);
|
||||
|
||||
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
|
||||
errors.push(this.t('validation.between', {
|
||||
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
|
||||
min: 1,
|
||||
max: 4094,
|
||||
}, true));
|
||||
}
|
||||
}
|
||||
|
||||
for (const cidr of this.network.exclude || []) {
|
||||
if (cidr && !isValidCIDR(cidr)) {
|
||||
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
async willSave() {
|
||||
if (!this.enabled) {
|
||||
this.useDefault();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.update();
|
||||
|
||||
const errors = this.validateInputs();
|
||||
|
||||
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Banner
|
||||
v-for="(errorMsg, index) in allErrors"
|
||||
:key="index"
|
||||
color="error"
|
||||
>
|
||||
{{ errorMsg }}
|
||||
</Banner>
|
||||
<RadioGroup
|
||||
v-model:value="enabled"
|
||||
class="mb-20"
|
||||
name="enableMigrationNetwork"
|
||||
:options="[true, false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="update"
|
||||
/>
|
||||
<template v-if="enabled">
|
||||
<LabeledSelect
|
||||
v-model:value="network.clusterNetwork"
|
||||
required
|
||||
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:options="clusterNetworkOptions"
|
||||
:disabled="disableEdit"
|
||||
@update:value="update"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value.number="network.vlan"
|
||||
required
|
||||
type="number"
|
||||
class="mb-20"
|
||||
:min="1"
|
||||
:max="4094"
|
||||
:mode="mode"
|
||||
placeholder="e.g. 1 - 4094"
|
||||
label-key="harvester.setting.vmMigrationNetwork.vlan"
|
||||
:disabled="disableEdit"
|
||||
@update:value="inputVlan"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="network.range"
|
||||
required
|
||||
class="mb-5"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
|
||||
label-key="harvester.setting.vmMigrationNetwork.range.label"
|
||||
:disabled="disableEdit"
|
||||
@update:value="update"
|
||||
/>
|
||||
<ArrayList
|
||||
v-model:value="network.exclude"
|
||||
:show-header="true"
|
||||
:default-add-value="''"
|
||||
:mode="mode"
|
||||
:add-disabled="disableEdit"
|
||||
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
|
||||
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
|
||||
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||
@update:value="update"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,8 +1,10 @@
|
||||
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
||||
export const DOC = {
|
||||
CONSOLE_URL: `/host/#remote-console`,
|
||||
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||
UPGRADE_URL: `/upgrade/index`
|
||||
CONSOLE_URL: `/host/#remote-console`,
|
||||
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||
UPGRADE_URL: `/upgrade/index`,
|
||||
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
|
||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const FEATURE_FLAGS = {
|
||||
'vmSnapshotQuota',
|
||||
'longhornV2LVMSupport',
|
||||
'improveMaintenanceMode',
|
||||
'upgradeConfigSetting'
|
||||
],
|
||||
'v1.4.1': [],
|
||||
'v1.4.2': [
|
||||
@@ -38,8 +39,17 @@ const FEATURE_FLAGS = {
|
||||
],
|
||||
'v1.5.1': [],
|
||||
'v1.6.0': [
|
||||
'vmMachineTypes'
|
||||
]
|
||||
'vmMachineTypes',
|
||||
'customSupportBundle',
|
||||
'csiOnlineExpandValidation',
|
||||
'vmNetworkMigration',
|
||||
'kubeovnVpcSubnet',
|
||||
'rancherClusterSetting',
|
||||
'cpuMemoryHotplug',
|
||||
'cdiSettings',
|
||||
'vmCloneRunStrategy',
|
||||
],
|
||||
'v1.6.1': [],
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
|
||||
@@ -424,6 +424,7 @@ export function init($plugin, store) {
|
||||
[
|
||||
HCI.CLUSTER_NETWORK,
|
||||
HCI.NETWORK_ATTACHMENT,
|
||||
HCI.VPC,
|
||||
HCI.LB,
|
||||
HCI.IP_POOL,
|
||||
],
|
||||
@@ -550,6 +551,21 @@ export function init($plugin, store) {
|
||||
exact: false
|
||||
});
|
||||
|
||||
configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||
|
||||
virtualType({
|
||||
labelKey: 'harvester.vpc.label',
|
||||
name: HCI.VPC,
|
||||
namespaced: true,
|
||||
weight: 187,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VPC }
|
||||
},
|
||||
exact: false,
|
||||
ifHaveType: HCI.VPC,
|
||||
});
|
||||
|
||||
configureType(HCI.SNAPSHOT, {
|
||||
isCreatable: false,
|
||||
location: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { HCI } from '../types';
|
||||
|
||||
// TODO: delete this not used variable
|
||||
export const MemoryUnit = [{
|
||||
label: 'Mi',
|
||||
@@ -70,6 +72,7 @@ export const ADD_ONS = {
|
||||
RANCHER_MONITORING: 'rancher-monitoring',
|
||||
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
||||
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
||||
KUBEOVN_OPERATOR: 'kubeovn-operator',
|
||||
};
|
||||
|
||||
export const CSI_SECRETS = {
|
||||
@@ -80,3 +83,14 @@ export const CSI_SECRETS = {
|
||||
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
||||
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
||||
};
|
||||
|
||||
// Some harvester CRD type is not equal to model file name, define the mapping here
|
||||
export const HARVESTER_CRD_MAP = {
|
||||
node: HCI.HOST,
|
||||
configmap: HCI.CLOUD_TEMPLATE,
|
||||
persistentvolumeclaim: HCI.VOLUME,
|
||||
'snapshot.storage.k8s.io.volumesnapshot': HCI.SNAPSHOT,
|
||||
// specific groupable table detail page
|
||||
'network.harvesterhci.io.vlanconfig': HCI.CLUSTER_NETWORK,
|
||||
'kubeovn.io.subnet': HCI.VPC
|
||||
};
|
||||
|
||||
@@ -69,5 +69,11 @@ export const HCI = {
|
||||
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||
K8S_ARCH: 'kubernetes.io/arch',
|
||||
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
|
||||
CUSTOM_IP: 'harvesterhci.io/custom-ip'
|
||||
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
||||
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
|
||||
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
|
||||
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
|
||||
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
||||
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
||||
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export const CLUSTER_NETWORK = 'clusterNetwork';
|
||||
export const VPC = 'vpc';
|
||||
|
||||
@@ -28,12 +28,17 @@ export const HCI_SETTING = {
|
||||
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
||||
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
||||
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
||||
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
|
||||
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
||||
NTP_SERVERS: 'ntp-servers',
|
||||
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
||||
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
||||
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
||||
UPGRADE_CONFIG: 'upgrade-config',
|
||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||
RANCHER_CLUSTER: 'rancher-cluster',
|
||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
|
||||
};
|
||||
|
||||
export const HCI_ALLOWED_SETTINGS = {
|
||||
@@ -52,12 +57,15 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
from: 'import',
|
||||
featureFlag: 'autoRotateRke2CertsSetting'
|
||||
},
|
||||
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
|
||||
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
|
||||
},
|
||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||
kind: 'multiline', canReset: true, from: 'import'
|
||||
},
|
||||
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
||||
@@ -96,6 +104,19 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
featureFlag: 'longhornV2LVMSupport'
|
||||
},
|
||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||
[HCI_SETTING.UPGRADE_CONFIG]: {
|
||||
kind: 'json',
|
||||
from: 'import',
|
||||
featureFlag: 'upgradeConfigSetting',
|
||||
docPath: 'UPGRADE_CONFIG_URL'
|
||||
},
|
||||
[HCI_SETTING.RANCHER_CLUSTER]: {
|
||||
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
|
||||
},
|
||||
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
||||
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
||||
},
|
||||
};
|
||||
|
||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||
|
||||
@@ -104,3 +104,30 @@ export const HARVESTER_DESCRIPTION = {
|
||||
...DESCRIPTION,
|
||||
width: 150,
|
||||
};
|
||||
|
||||
// The CIDR_BLOCK column in VPC list page
|
||||
export const CIDR_BLOCK = {
|
||||
name: 'cidrBlock',
|
||||
labelKey: 'harvester.subnet.cidrBlock.label',
|
||||
sort: 'cidrBlock',
|
||||
value: 'spec.cidrBlock',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// The Protocol column in VPC list page
|
||||
export const PROTOCOL = {
|
||||
name: 'protocol',
|
||||
labelKey: 'harvester.subnet.protocol.label',
|
||||
sort: 'protocol',
|
||||
value: 'spec.protocol',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// The Provider column in VPC list page
|
||||
export const PROVIDER = {
|
||||
name: 'provider',
|
||||
labelKey: 'harvester.subnet.provider.label',
|
||||
sort: 'provider',
|
||||
value: 'spec.provider',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
@@ -5,10 +5,21 @@ export const BACKUP_TYPE = {
|
||||
|
||||
export const NETWORK_TYPE = {
|
||||
L2VLAN: 'L2VlanNetwork',
|
||||
UNTAGGED: 'UntaggedNetwork'
|
||||
UNTAGGED: 'UntaggedNetwork',
|
||||
OVERLAY: 'OverlayNetwork',
|
||||
};
|
||||
|
||||
export const VOLUME_MODE = {
|
||||
BLOCK: 'Block',
|
||||
FILE_SYSTEM: 'Filesystem'
|
||||
};
|
||||
|
||||
export const NETWORK_PROTOCOL = {
|
||||
IPv4: 'IPv4',
|
||||
IPv6: 'IPv6',
|
||||
};
|
||||
|
||||
export const INTERNAL_STORAGE_CLASS = {
|
||||
VMSTATE_PERSISTENCE: 'vmstate-persistence',
|
||||
LONGHORN_STATIC: 'longhorn-static',
|
||||
};
|
||||
|
||||
@@ -47,20 +47,26 @@ export default {
|
||||
STATE,
|
||||
NAME,
|
||||
{
|
||||
name: 'vmCPU',
|
||||
labelKey: 'tableHeaders.cpu',
|
||||
search: false,
|
||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||
value: 'spec.template.spec.domain.cpu.cores',
|
||||
width: 120
|
||||
name: 'CPU',
|
||||
label: 'CPU',
|
||||
sort: ['displayCPU'],
|
||||
value: 'displayCPU',
|
||||
align: 'center',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
{
|
||||
name: 'vmRAM',
|
||||
labelKey: 'glance.memory',
|
||||
search: false,
|
||||
sort: ['memorySort'],
|
||||
value: 'spec.template.spec.domain.resources.limits.memory',
|
||||
width: 120
|
||||
name: 'Memory',
|
||||
value: 'displayMemory',
|
||||
sort: ['memorySort'],
|
||||
align: 'center',
|
||||
labelKey: 'tableHeaders.memory',
|
||||
formatter: 'Si',
|
||||
formatterOpts: {
|
||||
opts: {
|
||||
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
|
||||
},
|
||||
needParseSi: true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ip',
|
||||
|
||||
@@ -172,6 +172,9 @@ export default {
|
||||
:cpu="cpu"
|
||||
:mode="mode"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
:max-memory="maxMemory"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -172,6 +172,9 @@ export default {
|
||||
:cpu="cpu"
|
||||
:mode="mode"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
:max-memory="maxMemory"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
/>
|
||||
|
||||
<div class="row mb-10">
|
||||
|
||||
+3
-2
@@ -5,6 +5,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
||||
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||
import { HCI } from '../../../types';
|
||||
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
||||
|
||||
const UNDEFINED = 'n/a';
|
||||
|
||||
@@ -91,9 +92,9 @@ export default {
|
||||
},
|
||||
|
||||
flavor() {
|
||||
const domain = this.value?.spec?.template?.spec?.domain;
|
||||
const { cpu, memory } = getVmCPUMemoryValues(this.value);
|
||||
|
||||
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||
},
|
||||
|
||||
kernelRelease() {
|
||||
|
||||
@@ -139,10 +139,18 @@ export default {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
|
||||
|
||||
const migrationUid = this.vmi?.status?.migrationState?.migrationUid;
|
||||
const vmim = vmimList.find((VMIM) => VMIM?.metadata?.uid === migrationUid);
|
||||
const vmiName = this.vmi?.name || '';
|
||||
|
||||
return vmim;
|
||||
// filter the corresponding vmims by vmim.spec.vmiName and find the latest one by creationTimestamp
|
||||
const vmim = vmimList.filter((VMIM) => VMIM?.spec?.vmiName === vmiName).sort((a, b) => {
|
||||
if (a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
|
||||
return vmim.length > 0 && vmim[0] ? vmim[0] : null;
|
||||
},
|
||||
|
||||
migrationEvents() {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
@@ -14,7 +15,7 @@ export default {
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -26,9 +27,11 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: [],
|
||||
runStrategy: runStrategyOptions[1],
|
||||
runStrategyOptions
|
||||
};
|
||||
},
|
||||
|
||||
@@ -38,6 +41,9 @@ export default {
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
vmCloneRunStrategyEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -58,7 +64,8 @@ export default {
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
|
||||
const res = await this.actionResource.doAction('clone', payload, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
@@ -106,6 +113,13 @@ export default {
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-if="vmCloneRunStrategyEnabled"
|
||||
v-model:value="runStrategy"
|
||||
label-key="harvester.virtualMachine.runStrategy"
|
||||
:options="runStrategyOptions"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async create(buttonCb) {
|
||||
// shadow clone
|
||||
if (!this.cloneData) {
|
||||
this.resources[0].goToClone();
|
||||
buttonCb(false);
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('harvester.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this['errors'] = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this['errors'] = message;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.cloneVM.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Checkbox
|
||||
v-model:value="cloneData"
|
||||
class="mb-10"
|
||||
label-key="harvester.modal.cloneVM.type"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-show="cloneData"
|
||||
v-model:value="name"
|
||||
class="mb-20"
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
||||
:disabled="cloneData && !name"
|
||||
@click="create"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { GIBIBYTE } from '../utils/unit';
|
||||
|
||||
export default {
|
||||
name: 'CPUMemoryHotplugModal',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Card, Banner, UnitInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const { cpu, memory } = getVmCPUMemoryValues(this.resources[0] || {});
|
||||
|
||||
return {
|
||||
cpu,
|
||||
memory,
|
||||
errors: [],
|
||||
GIBIBYTE
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0] || {};
|
||||
},
|
||||
maxResourcesMessage() {
|
||||
const { maxCpu, maxMemory } = getVmCPUMemoryValues(this.actionResource);
|
||||
|
||||
if (maxCpu && maxMemory) {
|
||||
return this.t('harvester.modal.cpuMemoryHotplug.maxResourcesMessage', { maxCpu, maxMemory });
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.cpu = '';
|
||||
this.memory = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
change() {
|
||||
if (parseInt(this.memory, 10) < 1 ) {
|
||||
this.memory = '1Gi';
|
||||
}
|
||||
if (this.cpu < 1) {
|
||||
this.cpu = 1;
|
||||
}
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
if (this.actionResource) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction('cpuAndMemoryHotplug', { sockets: this.cpu, memory: this.memory });
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.cpuMemoryHotplug.success', { vm: this.actionResource.nameDisplay })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this['errors'] = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this['errors'] = message;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
ref="modal"
|
||||
name="modal"
|
||||
:show-highlight-border="false"
|
||||
>
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.modal.cpuMemoryHotplug.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Banner
|
||||
v-if="maxResourcesMessage"
|
||||
:label="maxResourcesMessage"
|
||||
color="info"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="cpu"
|
||||
:label="t('harvester.virtualMachine.input.cpu')"
|
||||
:delay="0"
|
||||
required
|
||||
suffix="C"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:min="1"
|
||||
@update:value="change"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="memory"
|
||||
:label="t('harvester.virtualMachine.input.memory')"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:min="1"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterExportImageDialog',
|
||||
@@ -105,11 +106,17 @@ export default {
|
||||
}
|
||||
|
||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
const isInternal = isInternalStorageClass(s.metadata?.name);
|
||||
|
||||
if (isInternal) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
value: s.name,
|
||||
disabled: isInternal,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
@@ -6,7 +7,15 @@ import AsyncButton from '@shell/components/AsyncButton';
|
||||
import GraphCircle from '@shell/components/graph/Circle';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AppModal from '@shell/components/AppModal';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { HCI } from '../types';
|
||||
import { HCI_SETTING } from '../config/settings';
|
||||
import { DOC } from '../config/doc-links';
|
||||
import { docLink } from '../utils/feature-flags';
|
||||
|
||||
const SELECT_ALL = 'select_all';
|
||||
const UNSELECT_ALL = 'unselect_all';
|
||||
|
||||
export default {
|
||||
name: 'SupportBundle',
|
||||
@@ -17,14 +26,36 @@ export default {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
AppModal,
|
||||
LabeledSelect,
|
||||
UnitInput
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||
|
||||
try {
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||
const response = await this.$store.dispatch('harvester/request', { url });
|
||||
|
||||
this.defaultNamespaces = response.data || [];
|
||||
} catch (error) {
|
||||
this.defaultNamespaces = [];
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
url: '',
|
||||
description: '',
|
||||
errors: [],
|
||||
isOpen: false,
|
||||
isOpen: false,
|
||||
errors: [],
|
||||
version: '',
|
||||
clusterName: '',
|
||||
url: '',
|
||||
description: '',
|
||||
namespaces: [],
|
||||
defaultNamespaces: [],
|
||||
timeout: '',
|
||||
expiration: '',
|
||||
nodeTimeout: '',
|
||||
};
|
||||
},
|
||||
|
||||
@@ -39,23 +70,51 @@ export default {
|
||||
|
||||
percentage() {
|
||||
return this.$store.getters['harvester-common/getBundlePercentage'];
|
||||
}
|
||||
},
|
||||
|
||||
availableNamespaces() {
|
||||
const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||
const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||
|
||||
return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns));
|
||||
},
|
||||
|
||||
namespaceOptions() {
|
||||
if (this.availableNamespaces.length === 0) return [];
|
||||
|
||||
const allSelected = this.namespaces.length === this.availableNamespaces.length &&
|
||||
this.availableNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||
|
||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||
|
||||
return [controlOption, ...this.availableNamespaces];
|
||||
},
|
||||
|
||||
docLink() {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
|
||||
return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version);
|
||||
},
|
||||
|
||||
customSupportBundleFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
isShowBundleModal: {
|
||||
immediate: true,
|
||||
handler(show) {
|
||||
if (show) {
|
||||
this.$nextTick(() => {
|
||||
this.isOpen = true;
|
||||
});
|
||||
} else {
|
||||
this.isOpen = false;
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
this.isOpen = show;
|
||||
}
|
||||
},
|
||||
|
||||
isOpen(newVal) {
|
||||
if (newVal) {
|
||||
this.loadDefaultSettings();
|
||||
} else {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -65,33 +124,93 @@ export default {
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||
this.backUpName = '';
|
||||
},
|
||||
|
||||
loadDefaultSettings() {
|
||||
const cluster = this.$store.getters['currentCluster'];
|
||||
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
|
||||
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
|
||||
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
|
||||
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
|
||||
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
|
||||
|
||||
this.version = versionSetting?.currentVersion || '';
|
||||
this.clusterName = cluster?.id || '';
|
||||
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
|
||||
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
|
||||
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
|
||||
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
this.errors = [];
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
this.namespaces = [];
|
||||
this.timeout = '';
|
||||
this.expiration = '';
|
||||
this.nodeTimeout = '';
|
||||
this.errors = [];
|
||||
},
|
||||
|
||||
updateNamespaces(selected) {
|
||||
if (selected.includes(SELECT_ALL)) {
|
||||
this.namespaces = [...this.availableNamespaces];
|
||||
} else if (selected.includes(UNSELECT_ALL)) {
|
||||
this.namespaces = [];
|
||||
} else {
|
||||
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||
}
|
||||
},
|
||||
|
||||
updateNumberValue(field, value) {
|
||||
if (value === '' || value === null || isNaN(value)) {
|
||||
this[field] = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const num = Number(value);
|
||||
const isValid = Number.isInteger(num) && num >= 0;
|
||||
|
||||
this[field] = isValid ? String(num) : '';
|
||||
},
|
||||
|
||||
onKeyDown(e) {
|
||||
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
this.errors = [];
|
||||
|
||||
const name = `bundle-${ randomStr(5).toLowerCase() }`;
|
||||
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
|
||||
const namespace = 'harvester-system';
|
||||
|
||||
const spec = {
|
||||
description: this.description.trim(),
|
||||
...(this.url.trim() && { issueURL: this.url.trim() }),
|
||||
...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }),
|
||||
...(this.timeout !== '' && { timeout: Number(this.timeout) }),
|
||||
...(this.expiration !== '' && { expiration: Number(this.expiration) }),
|
||||
...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }),
|
||||
};
|
||||
|
||||
const bundleCrd = {
|
||||
apiVersion: 'harvesterhci.io/v1beta1',
|
||||
type: HCI.SUPPORT_BUNDLE,
|
||||
kind: 'SupportBundle',
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
},
|
||||
spec: {
|
||||
issueURL: this.url,
|
||||
description: this.description
|
||||
}
|
||||
metadata: { name, namespace },
|
||||
spec,
|
||||
};
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||
|
||||
try {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||
|
||||
await bundleValue.save();
|
||||
|
||||
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
||||
@@ -118,69 +237,110 @@ export default {
|
||||
@close="close"
|
||||
>
|
||||
<div class="p-20">
|
||||
<h2>
|
||||
{{ t('harvester.modal.bundle.title') }}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
v-if="!bundlePending"
|
||||
class="content"
|
||||
>
|
||||
<LabeledInput
|
||||
v-model:value="url"
|
||||
:label="t('harvester.modal.bundle.url')"
|
||||
class="mb-20"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model:value="description"
|
||||
:label="t('harvester.modal.bundle.description')"
|
||||
type="multiline"
|
||||
:min-height="120"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="content"
|
||||
>
|
||||
<div class="circle">
|
||||
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
|
||||
<div class="content">
|
||||
<div
|
||||
v-if="bundlePending"
|
||||
class="circle mb-20"
|
||||
>
|
||||
<GraphCircle
|
||||
primary-stroke-color="green"
|
||||
secondary-stroke-color="white"
|
||||
secondary-stroke-color="lightgrey"
|
||||
:stroke-width="6"
|
||||
:percentage="percentage"
|
||||
:show-text="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<p
|
||||
v-if="customSupportBundleFeatureEnabled"
|
||||
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
|
||||
class="mb-20"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="url"
|
||||
:label="t('harvester.modal.bundle.url')"
|
||||
class="mb-10"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="description"
|
||||
required
|
||||
:label="t('harvester.modal.bundle.description')"
|
||||
type="multiline"
|
||||
:min-height="80"
|
||||
class="mb-10"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-for="(err, idx) in errors"
|
||||
:key="idx"
|
||||
>
|
||||
<Banner
|
||||
color="error"
|
||||
:label="stringify(err)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="footer mt-20">
|
||||
<button
|
||||
class="btn btn-sm role-secondary mr-10"
|
||||
@click="close"
|
||||
<template v-if="customSupportBundleFeatureEnabled">
|
||||
<LabeledSelect
|
||||
v-model:value="namespaces"
|
||||
:label="t('harvester.modal.bundle.namespaces.label')"
|
||||
:clearable="true"
|
||||
:multiple="true"
|
||||
:options="namespaceOptions"
|
||||
class="mb-10 label-select"
|
||||
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
||||
@update:value="updateNamespaces"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="timeout"
|
||||
:label="t('harvester.modal.bundle.timeout.label')"
|
||||
class="mb-10"
|
||||
type="number"
|
||||
:min="0"
|
||||
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
|
||||
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
|
||||
@keydown="onKeyDown"
|
||||
@update:value="val => updateNumberValue('timeout', val)"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="expiration"
|
||||
:label="t('harvester.modal.bundle.expiration.label')"
|
||||
class="mb-10"
|
||||
type="number"
|
||||
:min="0"
|
||||
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
|
||||
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
|
||||
@keydown="onKeyDown"
|
||||
@update:value="val => updateNumberValue('expiration', val)"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="nodeTimeout"
|
||||
:label="t('harvester.modal.bundle.nodeTimeout.label')"
|
||||
class="mb-10"
|
||||
type="number"
|
||||
:min="0"
|
||||
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
|
||||
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
|
||||
@keydown="onKeyDown"
|
||||
@update:value="val => updateNumberValue('nodeTimeout', val)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<div
|
||||
v-for="(err, idx) in errors"
|
||||
:key="idx"
|
||||
>
|
||||
{{ t('generic.close') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
type="submit"
|
||||
mode="generate"
|
||||
class="btn btn-sm bg-primary"
|
||||
:disabled="bundlePending"
|
||||
@click="save"
|
||||
/>
|
||||
<Banner
|
||||
color="error"
|
||||
:label="stringify(err)"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer mt-20">
|
||||
<button
|
||||
class="btn btn-sm role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.close') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
type="submit"
|
||||
mode="generate"
|
||||
class="btn btn-sm bg-primary"
|
||||
:disabled="bundlePending"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-modal>
|
||||
@@ -194,6 +354,10 @@ export default {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
|
||||
margin: 0 7px;
|
||||
}
|
||||
|
||||
.bundle {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
@@ -204,10 +368,8 @@ export default {
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 218px;
|
||||
|
||||
.circle {
|
||||
padding-top: 20px;
|
||||
padding: 10px 0;
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,10 @@ export default {
|
||||
|
||||
data.spec.clusterNetwork = this.clusterNetwork;
|
||||
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](`v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`);
|
||||
|
||||
await this.$store.dispatch('harvester/request', {
|
||||
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
|
||||
@@ -6,14 +6,13 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../types';
|
||||
import { NETWORK_TYPE } from '../config/types';
|
||||
|
||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
|
||||
|
||||
const AUTO = 'auto';
|
||||
const MANUAL = 'manual';
|
||||
@@ -44,13 +43,10 @@ export default {
|
||||
|
||||
data() {
|
||||
const config = JSON.parse(this.value.spec.config);
|
||||
|
||||
const annotations = this.value?.metadata?.annotations || {};
|
||||
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
||||
|
||||
if ((config.bridge || '').endsWith('-br')) {
|
||||
config.bridge = config.bridge.slice(0, -3);
|
||||
}
|
||||
|
||||
const type = this.value.vlanType || L2VLAN ;
|
||||
|
||||
return {
|
||||
@@ -78,6 +74,30 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
clusterBridge: {
|
||||
get() {
|
||||
if (!this.config.bridge) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// remove -br suffix if exists
|
||||
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
|
||||
},
|
||||
|
||||
set(neu) {
|
||||
if (neu === '') {
|
||||
this.config.bridge = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!neu.endsWith('-br')) {
|
||||
this.config.bridge = `${ neu }-br`;
|
||||
} else {
|
||||
this.config.bridge = neu;
|
||||
}
|
||||
}
|
||||
},
|
||||
modeOptions() {
|
||||
return [{
|
||||
label: this.t('harvester.network.layer3Network.mode.auto'),
|
||||
@@ -88,6 +108,14 @@ export default {
|
||||
}];
|
||||
},
|
||||
|
||||
kubeovnVpcSubnetSupport() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
|
||||
},
|
||||
|
||||
longhornV2LVMSupport() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
|
||||
},
|
||||
|
||||
clusterNetworkOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||
@@ -103,8 +131,30 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
networkType() {
|
||||
return [L2VLAN, UNTAGGED];
|
||||
networkTypes() {
|
||||
const types = [L2VLAN, UNTAGGED];
|
||||
|
||||
if (this.kubeovnVpcSubnetSupport) {
|
||||
types.push(OVERLAY);
|
||||
}
|
||||
|
||||
return types;
|
||||
},
|
||||
|
||||
isL2VlanNetwork() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === L2VLAN;
|
||||
}
|
||||
|
||||
return this.type === L2VLAN;
|
||||
},
|
||||
|
||||
isOverlayNetwork() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === OVERLAY;
|
||||
}
|
||||
|
||||
return this.type === OVERLAY;
|
||||
},
|
||||
|
||||
isUntaggedNetwork() {
|
||||
@@ -116,36 +166,54 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
type(newType) {
|
||||
if (newType === OVERLAY) {
|
||||
this.config.type = 'kube-ovn';
|
||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
|
||||
} else {
|
||||
this.config.type = 'bridge';
|
||||
this.config.promiscMode = true;
|
||||
this.config.ipam = {};
|
||||
this.config.bridge = '';
|
||||
delete this.config.provider;
|
||||
delete this.config.server_socket;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveNetwork(buttonCb) {
|
||||
const errors = [];
|
||||
|
||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||
}
|
||||
|
||||
if (!this.config.bridge) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||
}
|
||||
|
||||
if (this.layer3Network.mode === MANUAL) {
|
||||
if (!this.layer3Network.gateway) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||
}
|
||||
if (!this.layer3Network.cidr) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||
|
||||
if (!this.config.bridge) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||
}
|
||||
|
||||
if (this.layer3Network.mode === MANUAL) {
|
||||
if (!this.layer3Network.gateway) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||
}
|
||||
if (!this.layer3Network.cidr) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||
|
||||
await this.save(buttonCb);
|
||||
},
|
||||
|
||||
@@ -169,14 +237,19 @@ export default {
|
||||
updateBeforeSave() {
|
||||
this.config.name = this.value.metadata.name;
|
||||
|
||||
if (this.isOverlayNetwork) {
|
||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||
delete this.config.bridge;
|
||||
delete this.config.promiscMode;
|
||||
delete this.config.vlan;
|
||||
delete this.config.ipam;
|
||||
}
|
||||
|
||||
if (this.isUntaggedNetwork) {
|
||||
delete this.config.vlan;
|
||||
}
|
||||
|
||||
this.value.spec.config = JSON.stringify({
|
||||
...this.config,
|
||||
bridge: `${ this.config.bridge }-br`,
|
||||
});
|
||||
this.value.spec.config = JSON.stringify({ ...this.config });
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -212,14 +285,15 @@ export default {
|
||||
<LabeledSelect
|
||||
v-model:value="type"
|
||||
class="mb-20"
|
||||
:options="networkType"
|
||||
:options="networkTypes"
|
||||
:mode="mode"
|
||||
:disabled="isEdit"
|
||||
:label="t('harvester.fields.type')"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-if="!isUntaggedNetwork"
|
||||
v-if="isL2VlanNetwork"
|
||||
v-model:value.number="config.vlan"
|
||||
class="mb-20"
|
||||
required
|
||||
@@ -229,25 +303,20 @@ export default {
|
||||
:mode="mode"
|
||||
@update:value="input"
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
class="col span-12"
|
||||
>
|
||||
<LabeledSelect
|
||||
v-model:value="config.bridge"
|
||||
class="mb-20"
|
||||
:label="t('harvester.network.clusterNetwork.label')"
|
||||
required
|
||||
:options="clusterNetworkOptions"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LabeledSelect
|
||||
v-if="!isOverlayNetwork"
|
||||
v-model:value="clusterBridge"
|
||||
class="mb-20"
|
||||
:label="t('harvester.network.clusterNetwork.label')"
|
||||
required
|
||||
:disabled="isEdit"
|
||||
:options="clusterNetworkOptions"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="!isUntaggedNetwork"
|
||||
v-if="isL2VlanNetwork"
|
||||
name="layer3Network"
|
||||
:label="t('harvester.network.tabs.layer3Network')"
|
||||
:weight="98"
|
||||
|
||||
@@ -84,6 +84,10 @@ export default {
|
||||
this.value['_type'] = TYPES.OPAQUE;
|
||||
}
|
||||
|
||||
if ( this.realMode === _CLONE) {
|
||||
this.value['_type'] = this.liveValue._type;
|
||||
}
|
||||
|
||||
return {
|
||||
isCloud,
|
||||
nodeDrivers: null,
|
||||
|
||||
@@ -6,6 +6,8 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { DOC } from '../config/doc-links';
|
||||
import { docLink } from '../utils/feature-flags';
|
||||
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||
|
||||
@@ -58,7 +60,13 @@ export default {
|
||||
|
||||
return {
|
||||
setting,
|
||||
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
|
||||
description: isHarvester ? t(
|
||||
`advancedSettings.descriptions.harv-${ this.value.id }`,
|
||||
this.getDocLinkParams()
|
||||
) : t(
|
||||
`advancedSettings.descriptions.${ this.value.id }`,
|
||||
this.getDocLinkParams()
|
||||
),
|
||||
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
||||
enumOptions,
|
||||
canReset,
|
||||
@@ -171,6 +179,18 @@ export default {
|
||||
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
||||
this.$refs.settingComp.useDefault();
|
||||
}
|
||||
},
|
||||
getDocLinkParams() {
|
||||
const setting = HCI_ALLOWED_SETTINGS[this.value.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[this.value.id];
|
||||
|
||||
if (setting?.docPath) {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
const url = docLink(DOC[setting.docPath], version);
|
||||
|
||||
return { url };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
<script>
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
|
||||
import { HCI_SETTING } from '../../config/settings';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
|
||||
export default {
|
||||
name: 'CDISettings',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
provisioner: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
ArrayList,
|
||||
Checkbox,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
emits: ['update:cdiSettings'],
|
||||
|
||||
async fetch() {
|
||||
const hash = {
|
||||
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
|
||||
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
|
||||
};
|
||||
|
||||
await allHash(hash);
|
||||
|
||||
if (this.mode === _CREATE ) {
|
||||
this.setDefaultVolumeSnapshotClass();
|
||||
} else {
|
||||
this.initCDISettingsFromAnnotations();
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
cdiSettings: {
|
||||
volumeModeAccessModes: [],
|
||||
volumeSnapshotClass: null,
|
||||
cloneStrategy: null,
|
||||
filesystemOverhead: null,
|
||||
},
|
||||
defaultAddValue: { volumeMode: null, accessModes: [] },
|
||||
noneOption: { label: 'None', value: '' },
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
inStore() {
|
||||
return this.$store.getters['currentProduct'].inStore;
|
||||
},
|
||||
|
||||
allVolumeModeOptions() {
|
||||
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
|
||||
},
|
||||
|
||||
selectedVolumeModes() {
|
||||
return this.cdiSettings.volumeModeAccessModes
|
||||
.map((item) => item.volumeMode)
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
volumeModeOptions() {
|
||||
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
|
||||
},
|
||||
|
||||
allVolumeModesSelected() {
|
||||
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
|
||||
},
|
||||
|
||||
accessModeOptions() {
|
||||
return [
|
||||
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
||||
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
||||
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
||||
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
|
||||
];
|
||||
},
|
||||
|
||||
cloneStrategyOptions() {
|
||||
return [
|
||||
this.noneOption,
|
||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
|
||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
|
||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
|
||||
];
|
||||
},
|
||||
|
||||
volumeSnapshotClassOptions() {
|
||||
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
|
||||
|
||||
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
|
||||
},
|
||||
|
||||
isFilesystemOverheadValid() {
|
||||
const val = this.cdiSettings.filesystemOverhead;
|
||||
|
||||
if (val === null || val === '') return true;
|
||||
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
|
||||
|
||||
return regex.test(val);
|
||||
},
|
||||
|
||||
isCustomClass() {
|
||||
return this.mode === _CREATE || this.mode === _EDIT;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
provisioner() {
|
||||
this.resetCdiSettings();
|
||||
this.$nextTick(this.setDefaultVolumeSnapshotClass);
|
||||
},
|
||||
|
||||
cdiSettings: {
|
||||
handler(val) {
|
||||
this.$emit('update:cdiSettings', val);
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setDefaultVolumeSnapshotClass() {
|
||||
try {
|
||||
const setting = this.$store.getters[`${ this.inStore }/byId`](HCI.SETTING, HCI_SETTING.CSI_DRIVER_CONFIG);
|
||||
const config = JSON.parse(setting?.value || setting?.default || '{}');
|
||||
const defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
|
||||
|
||||
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
|
||||
|
||||
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
|
||||
this.cdiSettings.volumeSnapshotClass = null;
|
||||
}
|
||||
},
|
||||
|
||||
initCDISettingsFromAnnotations() {
|
||||
const annotations = this.value?.metadata?.annotations || {};
|
||||
|
||||
let volumeModeAccessModes = [];
|
||||
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||
|
||||
if (rawVolumeMode) {
|
||||
try {
|
||||
const parsed = JSON.parse(rawVolumeMode);
|
||||
|
||||
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
|
||||
volumeMode,
|
||||
accessModes: Array.isArray(accessModes) ? accessModes : [],
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
if (volumeModeAccessModes.length) {
|
||||
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
|
||||
}
|
||||
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
|
||||
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||
}
|
||||
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
|
||||
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||
}
|
||||
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
|
||||
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||
}
|
||||
},
|
||||
|
||||
resetCdiSettings() {
|
||||
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
|
||||
this.cdiSettings.volumeSnapshotClass = null;
|
||||
this.cdiSettings.cloneStrategy = null;
|
||||
this.cdiSettings.filesystemOverhead = null;
|
||||
},
|
||||
|
||||
validateFilesystemOverhead(e) {
|
||||
const val = e.target.value;
|
||||
|
||||
this.cdiSettings.filesystemOverhead = val;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ArrayList
|
||||
v-model:value="cdiSettings.volumeModeAccessModes"
|
||||
:initial-empty-row="true"
|
||||
:show-header="true"
|
||||
:mode="mode"
|
||||
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
|
||||
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
|
||||
:default-add-value="defaultAddValue"
|
||||
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
|
||||
:add-disabled="allVolumeModesSelected"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="column-headers">
|
||||
<div
|
||||
class="row"
|
||||
:class="{ custom: isCustomClass }"
|
||||
>
|
||||
<label
|
||||
class="col span-3 value text-label mb-10"
|
||||
for="volumeMode"
|
||||
>
|
||||
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
|
||||
</label>
|
||||
<label
|
||||
class="col span-9 value text-label mb-10"
|
||||
for="accessModes"
|
||||
>
|
||||
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #columns="{ row }">
|
||||
<div class="row">
|
||||
<div class="col span-3">
|
||||
<LabeledSelect
|
||||
id="volumeMode"
|
||||
v-model:value="row.value.volumeMode"
|
||||
:mode="mode"
|
||||
:options="volumeModeOptions"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="accessModes"
|
||||
class="col span-9"
|
||||
>
|
||||
<Checkbox
|
||||
v-for="opt in accessModeOptions"
|
||||
:key="opt.value"
|
||||
v-model:value="row.value.accessModes"
|
||||
:value-when-true="opt.value"
|
||||
:label="opt.label"
|
||||
type="checkbox"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="cdiSettings.volumeSnapshotClass"
|
||||
class="select mt-20 mb-20"
|
||||
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
|
||||
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
|
||||
:mode="mode"
|
||||
:options="volumeSnapshotClassOptions"
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="cdiSettings.cloneStrategy"
|
||||
class="select mb-20"
|
||||
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
|
||||
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
|
||||
:mode="mode"
|
||||
:options="cloneStrategyOptions"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model:value="cdiSettings.filesystemOverhead"
|
||||
class="select mb-20"
|
||||
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
|
||||
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
|
||||
:mode="mode"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
|
||||
:status="isFilesystemOverheadValid ? null : 'error'"
|
||||
@input="validateFilesystemOverhead"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.column-headers .row.custom {
|
||||
max-width: calc(100% - 75px);
|
||||
}
|
||||
|
||||
.row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
max-width: 480px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
@@ -6,11 +7,9 @@ import ArrayList from '@shell/components/form/ArrayList';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import Loading from '@shell/components/Loading';
|
||||
|
||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||
import {
|
||||
@@ -22,6 +21,7 @@ import { CSI_DRIVER } from '../../types';
|
||||
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';
|
||||
import CDISettings from './CDISettings';
|
||||
|
||||
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||
|
||||
@@ -45,6 +45,7 @@ export default {
|
||||
Tabbed,
|
||||
Loading,
|
||||
Tags,
|
||||
CDISettings,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
@@ -117,6 +118,7 @@ export default {
|
||||
key: '',
|
||||
values: [],
|
||||
},
|
||||
cdiSettings: {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -124,10 +126,10 @@ export default {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER })
|
||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
||||
};
|
||||
|
||||
if (this.value.longhornV2LVMSupport) {
|
||||
@@ -197,6 +199,14 @@ export default {
|
||||
|
||||
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
||||
},
|
||||
|
||||
isCDISettingsFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
|
||||
},
|
||||
|
||||
shouldShowCDISettingsTab() {
|
||||
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -249,6 +259,10 @@ export default {
|
||||
});
|
||||
|
||||
this.formatAllowedTopoloties();
|
||||
|
||||
if (this.shouldShowCDISettingsTab) {
|
||||
this.formatCDISettings();
|
||||
}
|
||||
},
|
||||
|
||||
formatAllowedTopoloties() {
|
||||
@@ -270,6 +284,42 @@ export default {
|
||||
if (matchLabelExpressions.length > 0) {
|
||||
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
||||
}
|
||||
},
|
||||
formatCDISettings() {
|
||||
const annotations = this.value.metadata.annotations || {};
|
||||
const volumeModeAccessModes = {};
|
||||
|
||||
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
|
||||
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
|
||||
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(volumeModeAccessModes).length > 0) {
|
||||
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||
}
|
||||
|
||||
if (this.cdiSettings.volumeSnapshotClass) {
|
||||
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||
}
|
||||
|
||||
if (this.cdiSettings.cloneStrategy) {
|
||||
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||
}
|
||||
|
||||
if (this.cdiSettings.filesystemOverhead) {
|
||||
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
|
||||
} else {
|
||||
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||
}
|
||||
|
||||
this.value.metadata.annotations = annotations;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -404,6 +454,19 @@ export default {
|
||||
</template>
|
||||
</ArrayList>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="shouldShowCDISettingsTab"
|
||||
name="cdiSettings"
|
||||
:label="t('harvester.storage.cdiSettings.label')"
|
||||
:weight="-2"
|
||||
>
|
||||
<CDISettings
|
||||
v-model:cdi-settings="cdiSettings"
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:provisioner="value.provisioner"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
|
||||
import { OS } from '../mixins/harvester-vm';
|
||||
import { HCI } from '../types';
|
||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
const ENCRYPT = 'encrypt';
|
||||
const DECRYPT = 'decrypt';
|
||||
@@ -168,11 +169,18 @@ export default {
|
||||
|
||||
return filteredStorages
|
||||
.map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let disabled = false;
|
||||
|
||||
if (isInternalStorageClass(s.name)) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled
|
||||
};
|
||||
}) || [];
|
||||
},
|
||||
|
||||
@@ -257,8 +257,12 @@ export default {
|
||||
:label="t('harvester.vmTemplate.tabs.basics')"
|
||||
>
|
||||
<CpuMemory
|
||||
class="mb-20"
|
||||
:cpu="cpu"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
:max-memory="maxMemory"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
:disabled="isConfig"
|
||||
@updateCpuMemory="updateCpuMemory"
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,7 @@ 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';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVolume',
|
||||
@@ -206,11 +207,18 @@ export default {
|
||||
|
||||
storageClassOptions() {
|
||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let disabled = false;
|
||||
|
||||
if (isInternalStorageClass(s.name)) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled
|
||||
};
|
||||
}) || [];
|
||||
},
|
||||
@@ -304,6 +312,10 @@ export default {
|
||||
return ['ReadWriteMany'];
|
||||
}
|
||||
|
||||
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
||||
return this.value.spec.accessModes;
|
||||
}
|
||||
|
||||
const storageClassName = this.value.spec.storageClassName;
|
||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
<script>
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
||||
import { set } from '@shell/utils/object';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../../types';
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||
|
||||
export default {
|
||||
name: 'EditSubnet',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
NameNsDescription,
|
||||
Tab,
|
||||
RadioGroup,
|
||||
ArrayList,
|
||||
ResourceTabs,
|
||||
Loading,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
created() {
|
||||
const vpc = this.$route.query.vpc || '';
|
||||
|
||||
set(this.value, 'spec', this.value.spec || {
|
||||
cidrBlock: '',
|
||||
protocol: NETWORK_PROTOCOL.IPv4,
|
||||
provider: '',
|
||||
vpc,
|
||||
gatewayIP: '',
|
||||
excludeIps: [],
|
||||
private: false
|
||||
});
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
||||
};
|
||||
|
||||
await allHash(hash);
|
||||
},
|
||||
|
||||
computed: {
|
||||
showAllowSubnets() {
|
||||
return this.value?.spec?.private === true;
|
||||
},
|
||||
|
||||
doneLocationOverride() {
|
||||
return this.value.doneOverride;
|
||||
},
|
||||
|
||||
allowSubnetTooltip() {
|
||||
return this.t('harvester.subnet.allowSubnet.tooltip', null, true);
|
||||
},
|
||||
|
||||
excludeIPsTooltip() {
|
||||
return this.t('harvester.subnet.excludeIPs.tooltip', null, true);
|
||||
},
|
||||
|
||||
protocolOptions() {
|
||||
return Object.values(NETWORK_PROTOCOL);
|
||||
},
|
||||
|
||||
provider: {
|
||||
get() {
|
||||
const raw = this.value.spec.provider;
|
||||
|
||||
if (!raw) {
|
||||
return '';
|
||||
}
|
||||
const vmNet = raw.split('.')[0] || '';
|
||||
const ns = raw.split('.')[1] || '';
|
||||
|
||||
return `${ ns }/${ vmNet }`;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
const ns = value.split('/')[0] || '';
|
||||
const vmNet = value.split('/')[1] || '';
|
||||
const provider = `${ vmNet }.${ ns }.ovn`;
|
||||
|
||||
set(this.value, 'spec.provider', provider);
|
||||
}
|
||||
},
|
||||
|
||||
providerOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vmNets = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT) || [];
|
||||
|
||||
return vmNets.filter((net) => net.vlanType === NETWORK_TYPE.OVERLAY).map((n) => ({
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
},
|
||||
|
||||
vpcOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
|
||||
|
||||
return vpcs.map((n) => ({
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveSubnet(buttonCb) {
|
||||
const errors = [];
|
||||
const name = this.value?.metadata?.name;
|
||||
|
||||
try {
|
||||
if (!name) {
|
||||
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||
} else if (!this.value?.spec?.cidrBlock) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.cidrBlock.label') }, true));
|
||||
} else if (!this.value?.spec?.provider) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
|
||||
} else if (this.value.spec.excludeIps.includes('')) {
|
||||
errors.push(this.t('harvester.validation.subnet.excludeIps'));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
await this.value.save();
|
||||
buttonCb(true);
|
||||
this.done();
|
||||
} catch (e) {
|
||||
this.errors = [e];
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<CruResource
|
||||
v-else
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
:errors="errors"
|
||||
@finish="saveSubnet"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:namespaced="false"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
<ResourceTabs
|
||||
class="mt-15"
|
||||
:need-events="false"
|
||||
:need-related="false"
|
||||
:mode="mode"
|
||||
:side-tabs="true"
|
||||
>
|
||||
<Tab
|
||||
name="Basic"
|
||||
:label="t('generic.basic')"
|
||||
:weight="-1"
|
||||
class="bordered-table"
|
||||
>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.cidrBlock"
|
||||
class="mb-20"
|
||||
required
|
||||
:placeholder="t('harvester.subnet.cidrBlock.placeholder')"
|
||||
:label="t('harvester.subnet.cidrBlock.label')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.spec.protocol"
|
||||
:label="t('harvester.subnet.protocol.label')"
|
||||
:options="protocolOptions"
|
||||
required
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="provider"
|
||||
:label="t('harvester.subnet.provider.label')"
|
||||
:options="providerOptions"
|
||||
:tooltip="t('harvester.subnet.provider.tooltip')"
|
||||
required
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.spec.vpc"
|
||||
:label="t('harvester.subnet.vpc.label')"
|
||||
:options="vpcOptions"
|
||||
required
|
||||
:disabled="true"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.gateway"
|
||||
class="mb-20"
|
||||
:placeholder="t('harvester.subnet.gateway.placeholder')"
|
||||
:label="t('harvester.subnet.gateway.label')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model:value="value.spec.private"
|
||||
name="enabled"
|
||||
:options="[true, false]"
|
||||
:label="t('harvester.subnet.private.label')"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
:mode="mode"
|
||||
tooltip-key="harvester.subnet.private.tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ArrayList
|
||||
v-if="showAllowSubnets"
|
||||
v-model:value="value.spec.allowSubnets"
|
||||
:show-header="true"
|
||||
class="mt-20"
|
||||
:mode="mode"
|
||||
:add-label="t('harvester.subnet.allowSubnet.addSubnet')"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="box">
|
||||
<h3 class="key">
|
||||
{{ t('harvester.subnet.allowSubnet.label') }}
|
||||
<i
|
||||
v-clean-tooltip="{content: allowSubnetTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="allowSubnetTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #columns="scope">
|
||||
<div class="key">
|
||||
<input
|
||||
v-model="scope.row.value"
|
||||
:placeholder="t('harvester.subnet.allowSubnet.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
<ArrayList
|
||||
v-model:value="value.spec.excludeIps"
|
||||
:show-header="true"
|
||||
class="mt-20"
|
||||
:mode="mode"
|
||||
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="box">
|
||||
<h3 class="key">
|
||||
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||
<i
|
||||
v-clean-tooltip="{content: excludeIPsTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="excludeIPsTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #columns="scope">
|
||||
<div class="key">
|
||||
<input
|
||||
v-model="scope.row.value"
|
||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</CruResource>
|
||||
</template>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
|
||||
export default {
|
||||
name: 'StaticRoutes',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const rows = (this.value || []).map((row) => {
|
||||
return {
|
||||
cidr: row.cidr || '',
|
||||
nextHopIP: row.nextHopIP || '',
|
||||
};
|
||||
});
|
||||
|
||||
return { rows };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
showAdd() {
|
||||
return !this.isView;
|
||||
},
|
||||
|
||||
showRemove() {
|
||||
return !this.isView;
|
||||
},
|
||||
nextHopIPTooltip() {
|
||||
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.queueUpdate = debounce(this.update, 100);
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.rows.push({
|
||||
cidr: '',
|
||||
nextHopIP: '',
|
||||
});
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
remove(idx) {
|
||||
removeAt(this.rows, idx);
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.isView) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('update:value', this.rows);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="rows.length"
|
||||
class="static-route-row"
|
||||
>
|
||||
<div
|
||||
v-for="(row, idx) in rows"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="pool-headers cidr">
|
||||
<span class="pool-cidr">
|
||||
<t k="harvester.vpc.staticRoutes.cidr.label" />
|
||||
</span>
|
||||
<span class="pool-nextHopIP">
|
||||
<t k="harvester.vpc.staticRoutes.nextHopIP.label" />
|
||||
<i
|
||||
v-clean-tooltip="{content: nextHopIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="nextHopIPTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pool-row cidr">
|
||||
<div class="pool-cidr">
|
||||
<span v-if="isView">
|
||||
{{ row.cidr }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="row.cidr"
|
||||
type="text"
|
||||
:placeholder="t('harvester.vpc.staticRoutes.cidr.placeholder')"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="pool-nextHopIP">
|
||||
<span v-if="isView">
|
||||
{{ row.nextHopIP }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="row.nextHopIP"
|
||||
type="text"
|
||||
:placeholder="t('harvester.vpc.staticRoutes.nextHopIP.placeholder')"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="showRemove"
|
||||
type="button"
|
||||
class="btn role-link pl-0"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<t k="generic.remove" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="showAdd"
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
@click="add()"
|
||||
>
|
||||
<t k="generic.add" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pool-headers, .pool-row {
|
||||
display: grid;
|
||||
grid-column-gap: $column-gutter;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
|
||||
&.cidr {
|
||||
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
import { HCI } from '../../types';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'VpcPeerings',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: { LabeledSelect },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
vpc: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await this.$store.dispatch('harvester/findAll', { type: HCI.VPC });
|
||||
},
|
||||
|
||||
data() {
|
||||
const rows = (this.value || []).map((row) => {
|
||||
return {
|
||||
localConnectIP: row.localConnectIP || '',
|
||||
remoteVpc: row.remoteVpc || '',
|
||||
};
|
||||
});
|
||||
|
||||
return { rows };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.mode === _EDIT;
|
||||
},
|
||||
|
||||
showAdd() {
|
||||
return !this.isView;
|
||||
},
|
||||
|
||||
showRemove() {
|
||||
return !this.isView;
|
||||
},
|
||||
|
||||
remoteVpcOptions() {
|
||||
const allVpcs = this.$store.getters['harvester/all'](HCI.VPC) || [];
|
||||
|
||||
// filter self vpc if editing
|
||||
const vpcs = this.isEdit ? allVpcs.filter((v) => v.id !== this.vpc.id) : allVpcs;
|
||||
|
||||
return vpcs.map((n) => ({
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
},
|
||||
localConnectIPTooltip() {
|
||||
return this.t('harvester.vpc.vpcPeerings.localConnectIP.tooltip');
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.queueUpdate = debounce(this.update, 100);
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.rows.push({
|
||||
localConnectIP: '',
|
||||
remoteVpc: '',
|
||||
});
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
remove(idx) {
|
||||
removeAt(this.rows, idx);
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.isView) {
|
||||
return;
|
||||
}
|
||||
this.$emit('update:value', this.rows);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="rows.length"
|
||||
class="static-route-row"
|
||||
>
|
||||
<div
|
||||
v-for="(row, idx) in rows"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="pool-headers localConnectIP">
|
||||
<span class="pool-localConnectIP">
|
||||
<t k="harvester.vpc.vpcPeerings.localConnectIP.label" />
|
||||
<i
|
||||
v-clean-tooltip="{content: localConnectIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="localConnectPTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</span>
|
||||
<span class="pool-remoteVpc">
|
||||
<t k="harvester.vpc.vpcPeerings.remoteVpc.label" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="pool-row localConnectIP">
|
||||
<div class="pool-localConnectIP">
|
||||
<span v-if="isView">
|
||||
{{ row.localConnectIP }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="row.localConnectIP"
|
||||
type="text"
|
||||
:placeholder="t('harvester.vpc.vpcPeerings.localConnectIP.placeholder')"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="pool-remoteVpc">
|
||||
<span v-if="isView">
|
||||
{{ row.remoteVpc }}
|
||||
</span>
|
||||
<LabeledSelect
|
||||
v-model:value="row.remoteVpc"
|
||||
:options="remoteVpcOptions"
|
||||
:mode="mode"
|
||||
@update:value="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="showRemove"
|
||||
type="button"
|
||||
class="btn role-link pl-0"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<t k="generic.remove" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="showAdd"
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
@click="add()"
|
||||
>
|
||||
<t k="generic.add" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pool-headers, .pool-row {
|
||||
display: grid;
|
||||
grid-column-gap: $column-gutter;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
|
||||
&.localConnectIP {
|
||||
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script>
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||
import StaticRoutes from './StaticRoutes';
|
||||
import VpcPeerings from './VpcPeerings';
|
||||
import { set } from '@shell/utils/object';
|
||||
|
||||
export default {
|
||||
name: 'EditVPC',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
NameNsDescription,
|
||||
Tab,
|
||||
StaticRoutes,
|
||||
ResourceTabs,
|
||||
Loading,
|
||||
VpcPeerings
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
data() {
|
||||
set(this.value, 'spec', this.value.spec || {
|
||||
staticRoutes: [],
|
||||
vpcPeerings: [],
|
||||
});
|
||||
|
||||
return { staticRoutes: [] };
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveVpc(buttonCb) {
|
||||
const errors = [];
|
||||
|
||||
try {
|
||||
const name = this.value?.metadata?.name;
|
||||
|
||||
if (!name) {
|
||||
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
await this.value.save();
|
||||
buttonCb(true);
|
||||
this.done();
|
||||
} catch (e) {
|
||||
this.errors = [e];
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<CruResource
|
||||
v-else
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
:errors="errors"
|
||||
@finish="saveVpc"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:namespaced="false"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
<ResourceTabs
|
||||
class="mt-15"
|
||||
:mode="mode"
|
||||
:side-tabs="true"
|
||||
>
|
||||
<Tab
|
||||
name="staticRoutes"
|
||||
:label="t('harvester.vpc.staticRoutes.label')"
|
||||
:weight="-1"
|
||||
class="bordered-table"
|
||||
>
|
||||
<StaticRoutes
|
||||
v-model:value="value.spec.staticRoutes"
|
||||
class="col span-12"
|
||||
:mode="mode"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="vpcPeerings"
|
||||
:label="t('harvester.vpc.vpcPeerings.label')"
|
||||
:weight="-2"
|
||||
class="bordered-table"
|
||||
>
|
||||
<VpcPeerings
|
||||
v-model:value="value.spec.vpcPeerings"
|
||||
class="col span-12"
|
||||
:mode="mode"
|
||||
:vpc="value"
|
||||
/>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</CruResource>
|
||||
</template>
|
||||
@@ -2,23 +2,44 @@
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { GIBIBYTE } from '../../utils/unit';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
import { HCI } from '../../types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI_SETTING } from '../../config/settings';
|
||||
|
||||
const DEFAULT_HOT_PLUG_TIMES = 4;
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditCpuMemory',
|
||||
|
||||
emits: ['updateCpuMemory'],
|
||||
|
||||
components: { UnitInput, InputOrDisplay },
|
||||
components: {
|
||||
UnitInput, InputOrDisplay, Checkbox
|
||||
},
|
||||
|
||||
props: {
|
||||
cpu: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
maxCpu: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
memory: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
maxMemory: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
enableHotPlug: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create',
|
||||
@@ -29,21 +50,59 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const res = await allHash({ settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) });
|
||||
|
||||
this.settings = res.settings || [];
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
localCpu: this.cpu,
|
||||
localMemory: this.memory
|
||||
localCpu: this.cpu,
|
||||
localMemory: this.memory,
|
||||
maxLocalCpu: this.maxCpu,
|
||||
maxLocalMemory: this.maxMemory,
|
||||
localEnableHotPlug: this.enableHotPlug,
|
||||
settings: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
cupDisplay() {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
cpuDisplay() {
|
||||
return `${ this.localCpu } C`;
|
||||
},
|
||||
|
||||
maxCpuDisplay() {
|
||||
return `${ this.maxLocalCpu } C`;
|
||||
},
|
||||
|
||||
memoryDisplay() {
|
||||
return `${ this.localMemory }`;
|
||||
},
|
||||
|
||||
maxMemoryDisplay() {
|
||||
return `${ this.maxLocalMemory }`;
|
||||
},
|
||||
|
||||
cpuMemoryHotplugTooltip() {
|
||||
return this.t('harvester.virtualMachine.hotplug.tooltip', { hotPlugTimes: this.maxHotplugRatio });
|
||||
},
|
||||
|
||||
isCPUMemoryHotPlugFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('cpuMemoryHotplug');
|
||||
},
|
||||
|
||||
maxHotplugRatio() {
|
||||
const maxHotPlugRatioSetting = this.settings.find((s) => s.id === HCI_SETTING.MAX_HOTPLUG_RATIO);
|
||||
const maxPlugRatio = maxHotPlugRatioSetting?.value ? parseInt(maxHotPlugRatioSetting.value, 10) : DEFAULT_HOT_PLUG_TIMES;
|
||||
|
||||
return maxPlugRatio;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -55,71 +114,161 @@ export default {
|
||||
if (neu && !neu.includes('null')) {
|
||||
this.localMemory = neu;
|
||||
}
|
||||
}
|
||||
},
|
||||
maxCpu(neu) {
|
||||
this.maxLocalCpu = neu;
|
||||
},
|
||||
maxMemory(neu) {
|
||||
if (neu && !neu.includes('null')) {
|
||||
this.maxLocalMemory = neu;
|
||||
}
|
||||
},
|
||||
enableHotPlug(neu) {
|
||||
this.localEnableHotPlug = neu;
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
change() {
|
||||
let memory = '';
|
||||
|
||||
if (String(this.localMemory).includes('Gi')) {
|
||||
memory = this.localMemory;
|
||||
hotPlugChanged(neu) {
|
||||
// If hot plug is enabled, we need to update the maxCpu and maxMemory values
|
||||
if (neu) {
|
||||
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
|
||||
} else {
|
||||
memory = `${ this.localMemory }${ GIBIBYTE }`;
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
|
||||
}
|
||||
if (memory.includes('null')) {
|
||||
memory = null;
|
||||
}
|
||||
this.$emit('updateCpuMemory', this.localCpu, memory);
|
||||
},
|
||||
|
||||
changeMemory() {
|
||||
if (this.localEnableHotPlug) {
|
||||
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||
}
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||
},
|
||||
changeCPU() {
|
||||
if (this.localEnableHotPlug) {
|
||||
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||
}
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||
},
|
||||
changeMaxCPUMemory() {
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
name="CPU"
|
||||
:value="cupDisplay"
|
||||
:mode="mode"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localCpu"
|
||||
label="CPU"
|
||||
suffix="C"
|
||||
:delay="0"
|
||||
required
|
||||
:disabled="disabled"
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
name="CPU"
|
||||
:value="cpuDisplay"
|
||||
:mode="mode"
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
class="mb-10"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localCpu"
|
||||
label="CPU"
|
||||
suffix="C"
|
||||
:delay="0"
|
||||
required
|
||||
:disabled="disabled"
|
||||
:mode="mode"
|
||||
class="mb-20"
|
||||
@update:value="changeCPU"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.memory')"
|
||||
:value="memoryDisplay"
|
||||
:mode="mode"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localMemory"
|
||||
:label="t('harvester.virtualMachine.input.memory')"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
required
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="changeMemory"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.memory')"
|
||||
:value="memoryDisplay"
|
||||
:mode="mode"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localMemory"
|
||||
:label="t('harvester.virtualMachine.input.memory')"
|
||||
<div
|
||||
v-if="isCPUMemoryHotPlugFeatureEnabled"
|
||||
class="row"
|
||||
>
|
||||
<Checkbox
|
||||
v-model:value="localEnableHotPlug"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.hotplug.title')"
|
||||
:disabled="isView"
|
||||
@update:value="hotPlugChanged"
|
||||
/>
|
||||
<i
|
||||
v-clean-tooltip="{content: cpuMemoryHotplugTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="cpuMemoryHotplugTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="localEnableHotPlug && isCPUMemoryHotPlugFeatureEnabled"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.maxCpu')"
|
||||
:value="maxCpuDisplay"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
required
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
class="mt-20"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="maxLocalCpu"
|
||||
:label="t('harvester.virtualMachine.input.maxCpu')"
|
||||
suffix="C"
|
||||
:delay="0"
|
||||
:disabled="disabled"
|
||||
:mode="mode"
|
||||
class="mt-20"
|
||||
@update:value="changeMaxCPUMemory"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.maxMemory')"
|
||||
:value="maxMemoryDisplay"
|
||||
:mode="mode"
|
||||
class="mt-20"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="maxLocalMemory"
|
||||
:label="t('harvester.virtualMachine.input.maxMemory')"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
:suffix="GIBIBYTE"
|
||||
class="mt-20"
|
||||
@update:value="changeMaxCPUMemory"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,7 @@ 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';
|
||||
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||
|
||||
export default {
|
||||
emits: ['update:value'],
|
||||
@@ -266,7 +267,17 @@ export default {
|
||||
|
||||
isLonghornV2(volume) {
|
||||
return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2;
|
||||
}
|
||||
},
|
||||
|
||||
isResizeDisabled(volume) {
|
||||
if (this.isCreate) return false;
|
||||
if (volume.newCreateId) return false;
|
||||
|
||||
const isStopped = this.vm.stateDisplay === OFF;
|
||||
const isLonghornV2 = this.isLonghornV2(volume);
|
||||
|
||||
return !isStopped || isLonghornV2;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -347,6 +358,7 @@ export default {
|
||||
:is-edit="isEdit"
|
||||
:is-view="isView"
|
||||
:is-virtual-type="isVirtualType"
|
||||
:is-resize-disabled="isResizeDisabled(rows[i])"
|
||||
:mode="mode"
|
||||
:idx="i"
|
||||
:validate-required="validateRequired"
|
||||
|
||||
+10
-8
@@ -64,7 +64,12 @@ export default {
|
||||
isVirtualType: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
isResizeDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -105,8 +110,9 @@ export default {
|
||||
})
|
||||
.sort((a, b) => (a.creationTimestamp > b.creationTimestamp ? -1 : 1))
|
||||
.map((image) => ({
|
||||
label: this.imageOptionLabel(image),
|
||||
value: image.id,
|
||||
label: this.imageOptionLabel(image),
|
||||
value: image.id,
|
||||
disabled: image.isImportedImage
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -134,10 +140,6 @@ export default {
|
||||
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);
|
||||
},
|
||||
@@ -349,7 +351,7 @@ export default {
|
||||
:label="t('harvester.fields.size')"
|
||||
:mode="mode"
|
||||
:required="validateRequired"
|
||||
:disable="isLonghornV2"
|
||||
:disabled="isResizeDisabled"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ 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 { isInternalStorageClass } from '../../../../utils/storage-class';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
|
||||
export default {
|
||||
@@ -55,7 +56,12 @@ export default {
|
||||
isVirtualType: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
isResizeDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -118,19 +124,22 @@ export default {
|
||||
|
||||
storageClassOptions() {
|
||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let disabled = false;
|
||||
|
||||
if (isInternalStorageClass(s.name)) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled
|
||||
};
|
||||
}) || [];
|
||||
},
|
||||
|
||||
isLonghornV2() {
|
||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
||||
},
|
||||
|
||||
isLonghornStorageClass() {
|
||||
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
||||
|
||||
@@ -302,7 +311,7 @@ export default {
|
||||
:mode="mode"
|
||||
:required="validateRequired"
|
||||
:label="t('harvester.fields.size')"
|
||||
:disabled="isLonghornV2"
|
||||
:disabled="isResizeDisabled"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
@@ -237,6 +237,9 @@ export default {
|
||||
}
|
||||
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION });
|
||||
const curVersion = versions.find( (V) => V.id === id);
|
||||
|
||||
if (!curVersion?.spec?.vm) return;
|
||||
|
||||
const cloneVersionVM = clone(curVersion.spec.vm);
|
||||
|
||||
delete cloneVersionVM.spec?.template?.spec?.accessCredentials;
|
||||
@@ -244,7 +247,6 @@ export default {
|
||||
delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS];
|
||||
|
||||
const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM);
|
||||
|
||||
const deleteDataSource = claimTemplate.map((volume) => {
|
||||
if (volume?.spec?.dataSource) {
|
||||
delete volume.spec.dataSource;
|
||||
@@ -255,10 +257,16 @@ export default {
|
||||
|
||||
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||
|
||||
// Update labels, instance labels and annotations
|
||||
this.value.metadata.labels = cloneVersionVM.metadata.labels;
|
||||
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels;
|
||||
this.value.metadata.annotations = cloneVersionVM.metadata.annotations;
|
||||
// Update instance labels, labels and annotations
|
||||
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels || {};
|
||||
this.value.metadata.labels = {
|
||||
...(curVersion.metadata?.labels || {}),
|
||||
...(cloneVersionVM.metadata?.labels || {})
|
||||
};
|
||||
this.value.metadata.annotations = {
|
||||
...(curVersion.metadata?.annotations || {}),
|
||||
...(cloneVersionVM.metadata?.annotations || {})
|
||||
};
|
||||
|
||||
this.getInitConfig({
|
||||
value: cloneVersionVM, existUserData: true, fromTemplate: true
|
||||
@@ -458,7 +466,9 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const errors = this.getAccessCredentialsValidation();
|
||||
const cpuMemoryErrors = this.getCPUMemoryValidation();
|
||||
const accessCredentialsErrors = this.getAccessCredentialsValidation();
|
||||
const errors = [...cpuMemoryErrors, ...accessCredentialsErrors];
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(errors);
|
||||
@@ -595,9 +605,13 @@ export default {
|
||||
:label="t('harvester.virtualMachine.detail.tabs.basics')"
|
||||
>
|
||||
<CpuMemory
|
||||
class="mb-20"
|
||||
:cpu="cpu"
|
||||
:max-cpu="maxCpu"
|
||||
:memory="memory"
|
||||
:max-memory="maxMemory"
|
||||
:mode="mode"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
@updateCpuMemory="updateCpuMemory"
|
||||
/>
|
||||
|
||||
|
||||
@@ -136,13 +136,31 @@ harvester:
|
||||
delete: Delete
|
||||
bundle:
|
||||
title: Generate a Support Bundle
|
||||
titleDescription: Collect system-related logs in Harvester to assist with troubleshooting and support.
|
||||
tip: Each field below specifies an aspect of the support bundle. For detailed explanations, please refer to the <a href="{doc}" target="_blank">documentation</a>.
|
||||
url: Issue URL
|
||||
description: Description
|
||||
requiredDesc: Description is required!
|
||||
titleDescription: Collect system-related logs in Harvester to help with troubleshooting and support.
|
||||
namespaces:
|
||||
label: Namespaces
|
||||
tooltip: Select additional namespaces to include in the support bundle.<br/>See docs support-bundle-namespaces for detail.
|
||||
selectAll: 'Select All'
|
||||
unselectAll: 'Unselect All'
|
||||
timeout:
|
||||
label: Timeout
|
||||
tooltip: Minutes allows for completion of the support bundle generation process.<br/>See docs support-bundle-timeout for detail.
|
||||
expiration:
|
||||
label: Expiration
|
||||
tooltip: Minutes before deleting packaged but not downloaded support bundle.<br/>See docs support-bundle-expiration for detail.
|
||||
nodeTimeout:
|
||||
label: Node Collection Timeout
|
||||
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
|
||||
hotplug:
|
||||
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
||||
title: Add Volume
|
||||
cpuMemoryHotplug:
|
||||
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
||||
title: Edit CPU and Memory
|
||||
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
|
||||
hotunplug:
|
||||
success: 'Volume { name } is detached successfully.'
|
||||
snapshot:
|
||||
@@ -199,6 +217,7 @@ harvester:
|
||||
suspendSchedule: Suspend
|
||||
restoreExistingVM: Replace Existing
|
||||
migrate: Migrate
|
||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||
abortMigration: Abort Migration
|
||||
createTemplate: Generate Template
|
||||
enableMaintenance: Enable Maintenance Mode
|
||||
@@ -382,6 +401,8 @@ harvester:
|
||||
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||
hash:
|
||||
sha512: 'Invalid SHA512 checksum.'
|
||||
subnet:
|
||||
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
|
||||
|
||||
dashboard:
|
||||
label: Dashboard
|
||||
@@ -555,6 +576,10 @@ harvester:
|
||||
virtualMachine:
|
||||
label: Virtual Machines
|
||||
osType: OS Type
|
||||
hotplug:
|
||||
title: Enable CPU and memory hotplug
|
||||
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
|
||||
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
|
||||
instance:
|
||||
singleInstance:
|
||||
multipleInstance:
|
||||
@@ -680,7 +705,7 @@ harvester:
|
||||
network:
|
||||
label: Network Data Template
|
||||
title: "Network Data:"
|
||||
tip: "The network-data configuration allows you to customize the instance’s networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
||||
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
||||
scheduling:
|
||||
affinity:
|
||||
anyNode: 'Run virtual machine on any available node'
|
||||
@@ -700,6 +725,9 @@ harvester:
|
||||
invalidUser: Invalid Username.
|
||||
input:
|
||||
name: Name
|
||||
cpu: CPU
|
||||
maxCpu: Maximum CPU
|
||||
maxMemory: Maximum Memory
|
||||
memory: Memory
|
||||
image: Image
|
||||
sshKey: SSHKey
|
||||
@@ -864,6 +892,8 @@ harvester:
|
||||
uploading: "{name} is uploading, please do not refresh or close the page."
|
||||
checksum: Checksum
|
||||
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
||||
tooltip:
|
||||
imported: Created automatically by the vm-import-controller
|
||||
|
||||
vmTemplate:
|
||||
label: Templates
|
||||
@@ -999,6 +1029,57 @@ harvester:
|
||||
progress: Restore in progress
|
||||
complete: Restore completed
|
||||
|
||||
subnet:
|
||||
cidrBlock:
|
||||
tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap.
|
||||
label: CIDR Block
|
||||
placeholder: e.g. 172.20.0.0/16
|
||||
protocol:
|
||||
label: Protocol
|
||||
provider:
|
||||
tooltip: Network provider for this Subnet. Must be one of the Virtual Machine Networks in OverlayNetwork type.
|
||||
label: Provider
|
||||
vpc:
|
||||
label: Virtual Private Cloud
|
||||
gateway:
|
||||
label: Gateway IP
|
||||
placeholder: e.g. 172.20.0.1
|
||||
private:
|
||||
label: Private Subnet
|
||||
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
|
||||
allowSubnet:
|
||||
label: Allow Subnets
|
||||
tooltip: You can specify certain subnets which can communicate with this subnet.
|
||||
addSubnet: Add Allow Subnet
|
||||
placeholder: e.g. 172.16.0.0/16
|
||||
excludeIPs:
|
||||
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
||||
|
||||
vpc:
|
||||
noAddonEnabled:
|
||||
prefix: The kubeovn-operator add-on is not enabled, click
|
||||
middle: here
|
||||
suffix: to enable the add-on to successfully create VPC and subnet.
|
||||
label: Virtual Private Cloud
|
||||
noChild: There is no subnet defined in this Virtual Private Cloud.
|
||||
createSubnet: Create Subnet
|
||||
staticRoutes:
|
||||
label: Static Routes
|
||||
cidr:
|
||||
label: CIDR
|
||||
placeholder: e.g. 172.16.0.0/16
|
||||
nextHopIP:
|
||||
tooltip: The localConnectIP on the other end of the peering VPC.
|
||||
label: Next Hop IP
|
||||
placeholder: e.g. 169.254.0.2
|
||||
vpcPeerings:
|
||||
label: VPC Peerings
|
||||
localConnectIP:
|
||||
tooltip: The designated IP address in CIDR notation of the VPC peering endpoint. Note that both IP addresses (for the VPC and the other VPC) should belong to the same subnet range and should not conflict with the cidrBlock in any existing subnets.
|
||||
label: Local Connect IP
|
||||
placeholder: e.g. 169.254.0.1/16
|
||||
remoteVpc:
|
||||
label: Remote VPC
|
||||
network:
|
||||
label: Virtual Machine Networks
|
||||
tabs:
|
||||
@@ -1055,6 +1136,9 @@ harvester:
|
||||
publicCertificate: Public Certificate
|
||||
privateKey: Private Key
|
||||
ca: CA
|
||||
rancherCluster:
|
||||
kubeConfig: Rancher KubeConfig
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
||||
storageNetwork:
|
||||
range:
|
||||
placeholder: e.g. 172.16.0.0/24
|
||||
@@ -1063,14 +1147,28 @@ harvester:
|
||||
clusterNetwork: Cluster Network
|
||||
vlan: VLAN ID
|
||||
exclude:
|
||||
label: Exclude
|
||||
placeholder: e.g. 172.16.0.1/32
|
||||
label: Exclude IPs
|
||||
placeholder: e.g. 172.16.0.1
|
||||
invalid: '"Exclude list" is invalid.'
|
||||
addIp: Add Exclude IP
|
||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||
vmForceDeletionPolicy:
|
||||
period: Period
|
||||
vmMigrationNetwork:
|
||||
parseError: "Failed to parse existing configuration."
|
||||
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
|
||||
clusterNetwork: Cluster Network
|
||||
vlan: VLAN ID
|
||||
range:
|
||||
placeholder: e.g. 172.16.0.0/24
|
||||
label: IP Range
|
||||
invalid: '"Range" is invalid.'
|
||||
exclude:
|
||||
label: Excluded IPs
|
||||
placeholder: e.g. 172.16.0.1/32
|
||||
invalid: '"Exclude list" is invalid.'
|
||||
addButton: Add Exclude IP
|
||||
ratio : Ratio
|
||||
autoRotateRKE2Certs:
|
||||
expiringInHours: Expiring in
|
||||
@@ -1084,6 +1182,11 @@ harvester:
|
||||
provisioner: Provisioner
|
||||
volumeSnapshotClassName: Volume Snapshot Class Name
|
||||
backupVolumeSnapshotClassName: Backup Volume Snapshot Class Name
|
||||
csiOnlineExpandValidation:
|
||||
provisioner: Provisioner
|
||||
value: Value
|
||||
invalidJsonFormat: "Configuration value is not a valid JSON format: {error}"
|
||||
failedToLoadDrivers: "Failed to load CSI drivers. Error: {error}"
|
||||
containerdRegistry:
|
||||
mirrors:
|
||||
mirrors: Mirrors
|
||||
@@ -1114,6 +1217,10 @@ harvester:
|
||||
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
|
||||
deleteImage: Please select an image to delete.
|
||||
deleteSuccess: "{name} deleted successfully."
|
||||
imagePreloadStrategy: Image Preload Strategy
|
||||
restoreVM: Restore VM
|
||||
strategyType: Strategy Type
|
||||
concurrency: Concurrency
|
||||
harvesterMonitoring:
|
||||
label: Harvester Monitoring
|
||||
section:
|
||||
@@ -1275,6 +1382,32 @@ harvester:
|
||||
allowedTopologies:
|
||||
title: Allowed Topologies
|
||||
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
|
||||
internal:
|
||||
label: Internal Storage Class
|
||||
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
|
||||
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
|
||||
cdiSettings:
|
||||
label: CDI Settings
|
||||
volumeModeAccessModes:
|
||||
label: Volume Mode / Access Modes
|
||||
tooltip: Specifies the default volume mode and access modes.
|
||||
add: Add
|
||||
volumeMode: Volume Mode
|
||||
accessModes:
|
||||
label: Access Modes
|
||||
volumeSnapshotClass:
|
||||
label: Volume Snapshot Class
|
||||
tooltip: Sets the Volume Snapshot Class name to be used when taking snapshots of PVCs under this StorageClass.
|
||||
cloneStrategy:
|
||||
label: Clone Strategy
|
||||
tooltip: Defines the clone strategy to use for volumes created with this StorageClass.
|
||||
copy: Copy
|
||||
snapshot: Snapshot
|
||||
csiClone: CSI Clone
|
||||
fileSystemOverhead:
|
||||
label: File System Overhead
|
||||
tooltip: Specifies the percentage of filesystem overhead to consider when calculating PVC size.
|
||||
placeholder: e.g. 0.05 (up to 3 decimal places, between 0 and 1)
|
||||
|
||||
vlanConfig:
|
||||
title: Network Configuration
|
||||
@@ -1579,6 +1712,7 @@ advancedSettings:
|
||||
'harv-backup-target': Custom backup target to store virtual machine backups.
|
||||
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme.
|
||||
'harv-csi-driver-config': Configure additional information for CSI drivers.
|
||||
'harv-csi-online-expand-validation': Allow online volume expansion for specific CSI drivers.
|
||||
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
|
||||
'harv-log-level': Configure Harvester server log level. Defaults to Info.
|
||||
'harv-server-version': Harvester server version.
|
||||
@@ -1597,7 +1731,7 @@ advancedSettings:
|
||||
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
||||
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
||||
'harv-storage-network': 'Longhorn storage-network setting.'
|
||||
'harv-support-bundle-namespaces': Specify resources in other namespaces to be collected by the support package.
|
||||
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
||||
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
||||
'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL.
|
||||
@@ -1607,6 +1741,10 @@ advancedSettings:
|
||||
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
|
||||
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
||||
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
|
||||
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
|
||||
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
|
||||
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
||||
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||
|
||||
typeLabel:
|
||||
kubevirt.io.virtualmachine: |-
|
||||
@@ -1664,6 +1802,11 @@ typeLabel:
|
||||
one { Virtual Machine Backup }
|
||||
other { Virtual Machine Backups }
|
||||
}
|
||||
kubeovn.io.vpc: |-
|
||||
{count, plural,
|
||||
one { Virtual Private Cloud }
|
||||
other { Virtual Private Clouds }
|
||||
}
|
||||
harvesterhci.io.cloudtemplate: |-
|
||||
{count, plural,
|
||||
one { Cloud Configuration Template }
|
||||
|
||||
@@ -116,7 +116,15 @@ export default {
|
||||
return {
|
||||
...s,
|
||||
description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`,
|
||||
customized: (!s.readOnly && s.data.value && s.data.value !== s.data.default) || s.data.hasCustomized
|
||||
customized: (!s.readOnly && s.data.value && (
|
||||
s.kind === 'json' ? (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(s.data.value)) !== JSON.stringify(JSON.parse(s.data.default));
|
||||
} catch {
|
||||
return s.data.value !== s.data.default;
|
||||
}
|
||||
})() : s.data.value !== s.data.default
|
||||
)) || s.data.hasCustomized
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,6 +83,34 @@ export default {
|
||||
:rows="rows"
|
||||
:schema="schema"
|
||||
:headers="headers"
|
||||
/>
|
||||
>
|
||||
<template #cell:name="{ row }">
|
||||
<td>
|
||||
<div>
|
||||
<router-link
|
||||
v-if="row?.detailLocation"
|
||||
:to="row.detailLocation"
|
||||
>
|
||||
{{ row.nameDisplay }}
|
||||
<i
|
||||
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||
class="icon icon-info text-info"
|
||||
style="margin-left: 0.4em;"
|
||||
/>
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ row.nameDisplay }}
|
||||
<i
|
||||
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||
class="icon icon-info text-info"
|
||||
style="margin-left: 0.4em;"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,8 @@ import ResourceTable from '@shell/components/ResourceTable';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
||||
import FilterLabel from '../components/FilterLabel';
|
||||
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
export default {
|
||||
name: 'ListHarvesterImage',
|
||||
@@ -53,6 +55,13 @@ export default {
|
||||
|
||||
return base;
|
||||
},
|
||||
|
||||
isInternalStorageClass(row) {
|
||||
const name = row?.spec?.targetStorageClassName ||
|
||||
row?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS];
|
||||
|
||||
return isInternalStorageClass(name);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -90,6 +99,11 @@ export default {
|
||||
v-if="row.isEncrypted"
|
||||
class="icon icon-lock"
|
||||
/>
|
||||
<i
|
||||
v-if="row.isImportedImage"
|
||||
v-clean-tooltip="t('harvester.image.tooltip.imported')"
|
||||
class="icon icon-info"
|
||||
/>
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ row.nameDisplay }}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
||||
import { allSettled } from '../utils/promise';
|
||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||
import { INTERNAL_STORAGE_CLASS } from '../config/types';
|
||||
|
||||
const schema = {
|
||||
id: HCI.VOLUME,
|
||||
@@ -130,6 +131,10 @@ export default {
|
||||
|
||||
getVMName(row) {
|
||||
return row.attachVM?.metadata?.name || '';
|
||||
},
|
||||
|
||||
isInternalStorageClass(storageClassName) {
|
||||
return this.$store.getters['type-map/labelFor'](INTERNAL_STORAGE_CLASS, storageClassName);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -170,7 +175,7 @@ export default {
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template #col:name="{row}">
|
||||
<template #col:name="{ row }">
|
||||
<td>
|
||||
<span>
|
||||
<router-link
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
<script>
|
||||
import Loading from '@shell/components/Loading';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
||||
import { PROVIDER, PROTOCOL, CIDR_BLOCK } from '@pkg/harvester/config/table-headers';
|
||||
import { HCI } from '../types';
|
||||
import { VPC } from '../config/query-params';
|
||||
import { ADD_ONS } from '../config/harvester-map';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import Banner from '@components/Banner/Banner.vue';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVPC',
|
||||
|
||||
components: {
|
||||
ResourceTable,
|
||||
Loading,
|
||||
MessageLink,
|
||||
Banner
|
||||
},
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
schema: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const hash = await allHash({ addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }) });
|
||||
|
||||
this.enabledKubeOvnAddon = hash.addons.find((addon) => addon.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
|
||||
|
||||
if (this.enabledKubeOvnAddon) {
|
||||
try {
|
||||
await allHash({
|
||||
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
|
||||
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||
});
|
||||
this.$store.dispatch('type-map/configureType', { match: HCI.SUBNET, isCreatable: this.enabledKubeOvnAddon });
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error fetching VPC and Subnet data:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
HCI,
|
||||
hasBothSchema: false,
|
||||
enabledKubeOvnAddon: false,
|
||||
to: `${ HCI.ADD_ONS }/kube-system/${ ADD_ONS.KUBEOVN_OPERATOR }?mode=edit`
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
headers() {
|
||||
return [
|
||||
STATE,
|
||||
NAME,
|
||||
NAMESPACE,
|
||||
CIDR_BLOCK,
|
||||
PROTOCOL,
|
||||
PROVIDER,
|
||||
AGE
|
||||
];
|
||||
},
|
||||
|
||||
rows() {
|
||||
return this.$store.getters[`harvester/all`](HCI.SUBNET) || [];
|
||||
},
|
||||
|
||||
vpcWithoutSubnets() {
|
||||
const vpcs = this.$store.getters[`harvester/all`](HCI.VPC) || [];
|
||||
|
||||
const out = vpcs.map((v) => {
|
||||
const hasChild = v.status?.subnets?.length > 0 || false;
|
||||
|
||||
return {
|
||||
...v,
|
||||
hasChild
|
||||
};
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
isSubnetCreatable() {
|
||||
return (this.subnetSchema?.collectionMethods || []).includes('POST');
|
||||
},
|
||||
|
||||
rowsWithFakeVpcs() {
|
||||
const fakeRows = this.vpcWithoutSubnets.map((vpc) => {
|
||||
return {
|
||||
groupByLabel: vpc.id,
|
||||
isFake: true,
|
||||
mainRowKey: vpc.id,
|
||||
nameDisplay: vpc.id,
|
||||
groupByVpc: vpc.id,
|
||||
availableActions: []
|
||||
};
|
||||
});
|
||||
|
||||
return [...this.rows, ...fakeRows];
|
||||
},
|
||||
|
||||
createVPCLocation() {
|
||||
const location = {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||
params: {
|
||||
product: HARVESTER_PRODUCT,
|
||||
resource: HCI.VPC,
|
||||
},
|
||||
};
|
||||
|
||||
return location;
|
||||
},
|
||||
|
||||
vpcSchema() {
|
||||
return this.$store.getters[`harvester/schemaFor`](HCI.VPC);
|
||||
},
|
||||
|
||||
subnetSchema() {
|
||||
return this.$store.getters[`harvester/schemaFor`](HCI.SUBNET);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
groupLabel(group) {
|
||||
return `${ this.t('harvester.vpc.label') }: ${ group.key }`;
|
||||
},
|
||||
|
||||
slotName(vpc) {
|
||||
return `main-row:${ vpc }`;
|
||||
},
|
||||
|
||||
createSubnetLocation(group) {
|
||||
const vpc = group.key;
|
||||
|
||||
const location = {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||
params: {
|
||||
product: HARVESTER_PRODUCT,
|
||||
resource: HCI.SUBNET,
|
||||
},
|
||||
};
|
||||
|
||||
location.query = { [VPC]: vpc };
|
||||
|
||||
return location;
|
||||
},
|
||||
|
||||
showVpcAction(event, group) {
|
||||
const vpc = group.key;
|
||||
|
||||
const resource = this.$store.getters[`harvester/byId`](HCI.VPC, vpc);
|
||||
|
||||
this.$store.commit(`action-menu/show`, {
|
||||
resources: [resource],
|
||||
elem: event.target
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
typeDisplay() {
|
||||
return this.t('harvester.vpc.label');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<div v-else-if="!enabledKubeOvnAddon">
|
||||
<Banner color="warning">
|
||||
<MessageLink
|
||||
:to="to"
|
||||
prefix-label="harvester.vpc.noAddonEnabled.prefix"
|
||||
middle-label="harvester.vpc.noAddonEnabled.middle"
|
||||
suffix-label="harvester.vpc.noAddonEnabled.suffix"
|
||||
/>
|
||||
</Banner>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Masthead
|
||||
:schema="vpcSchema"
|
||||
:type-display="t('harvester.vpc.label')"
|
||||
:resource="HCI.VPC"
|
||||
:create-location="createVPCLocation"
|
||||
:create-button-label="t('harvester.clusterNetwork.create.button.label')"
|
||||
/>
|
||||
<ResourceTable
|
||||
:rows="rowsWithFakeVpcs"
|
||||
:headers="headers"
|
||||
:schema="subnetSchema"
|
||||
:groupable="true"
|
||||
group-by="groupByVpc"
|
||||
>
|
||||
<template #header-middle>
|
||||
<div />
|
||||
</template>
|
||||
<template #group-by="{group}">
|
||||
<div class="group-bar">
|
||||
<div class="group-tab">
|
||||
<span>
|
||||
{{ groupLabel(group) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<router-link
|
||||
v-if="isSubnetCreatable"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
:to="createSubnetLocation(group)"
|
||||
>
|
||||
{{ t('harvester.vpc.createSubnet') }}
|
||||
</router-link>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm role-multi-action actions mr-10"
|
||||
@click="showVpcAction($event, group)"
|
||||
>
|
||||
<i class="icon icon-actions" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
v-for="(vpc) in vpcWithoutSubnets"
|
||||
:key="vpc.id"
|
||||
v-slot:[slotName(vpc.id)]
|
||||
>
|
||||
<tr
|
||||
v-show="!vpc.hasChild"
|
||||
class="main-row"
|
||||
>
|
||||
<td
|
||||
class="empty text-center"
|
||||
colspan="12"
|
||||
>
|
||||
{{ t('harvester.vpc.noChild') }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.state {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.icon-warning {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
.group-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.right {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.group-tab {
|
||||
&, &::after {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: -20px;
|
||||
}
|
||||
|
||||
SPAN {
|
||||
color: var(--body-text) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -22,8 +22,8 @@ export const VM_HEADERS = [
|
||||
{
|
||||
name: 'CPU',
|
||||
label: 'CPU',
|
||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||
value: 'spec.template.spec.domain.cpu.cores',
|
||||
sort: ['displayCPU'],
|
||||
value: 'displayCPU',
|
||||
align: 'center',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
import Loading from '@shell/components/Loading';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { STATE, AGE, NAME } from '@shell/config/table-headers';
|
||||
import { mapPref, GROUP_RESOURCES } from '@shell/store/prefs';
|
||||
import { NODE } from '@shell/config/types';
|
||||
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
import { CLUSTER_NETWORK } from '../config/query-params';
|
||||
import { HCI } from '../types';
|
||||
@@ -38,7 +36,6 @@ export default {
|
||||
|
||||
computed: {
|
||||
groupPreference: mapPref(GROUP_RESOURCES),
|
||||
|
||||
headers() {
|
||||
return [
|
||||
STATE,
|
||||
@@ -209,7 +206,6 @@ export default {
|
||||
{{ groupLabel(group) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<router-link
|
||||
v-if="isClusterNetworkCreatable && group.key !== 'mgmt'"
|
||||
|
||||
@@ -3,10 +3,9 @@ import jsyaml from 'js-yaml';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import difference from 'lodash/difference';
|
||||
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { set } from '@shell/utils/object';
|
||||
|
||||
import { getVmCPUMemoryValues } from '../../utils/cpuMemory';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { base64Decode } from '@shell/utils/crypto';
|
||||
@@ -164,6 +163,9 @@ export default {
|
||||
deleteAgent: true,
|
||||
memory: null,
|
||||
cpu: '',
|
||||
maxMemory: null,
|
||||
maxCpu: '',
|
||||
cpuMemoryHotplugEnabled: false,
|
||||
reservedMemory: null,
|
||||
accessCredentials: [],
|
||||
efiEnabled: false,
|
||||
@@ -348,8 +350,11 @@ export default {
|
||||
const runStrategy = spec.runStrategy || 'RerunOnFailure';
|
||||
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
|
||||
|
||||
const cpu = spec.template.spec.domain?.cpu?.cores;
|
||||
const memory = spec.template.spec.domain.resources.limits.memory;
|
||||
const {
|
||||
cpu, memory, maxCpu, maxMemory, isHotplugEnabled
|
||||
} = getVmCPUMemoryValues(vm);
|
||||
const cpuMemoryHotplugEnabled = isHotplugEnabled;
|
||||
|
||||
const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
||||
const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting;
|
||||
|
||||
@@ -409,6 +414,9 @@ export default {
|
||||
|
||||
this['cpu'] = cpu;
|
||||
this['memory'] = memory;
|
||||
this['maxCpu'] = maxCpu;
|
||||
this['maxMemory'] = maxMemory;
|
||||
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||
this['reservedMemory'] = reservedMemory;
|
||||
this['machineType'] = machineType;
|
||||
this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds;
|
||||
@@ -633,20 +641,26 @@ export default {
|
||||
this.spec.template.spec.domain.machine['type'] = this.machineType;
|
||||
}
|
||||
|
||||
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
||||
this.spec.template.spec.domain.resources.limits.cpu = this.cpu ? this.cpu.toString() : this.cpu;
|
||||
this.spec.template.spec.domain.resources.limits.memory = this.memory;
|
||||
this.setCPUAndMemory();
|
||||
// update terminationGracePeriodSeconds
|
||||
this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds;
|
||||
|
||||
// parse reserved memory
|
||||
const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm;
|
||||
|
||||
// parse reserved memory
|
||||
if (!this.reservedMemory) {
|
||||
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
||||
} else {
|
||||
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory;
|
||||
}
|
||||
|
||||
// add or remove cpu memory hotplug annotation
|
||||
if (this.cpuMemoryHotplugEnabled) {
|
||||
vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] = this.cpuMemoryHotplugEnabled.toString();
|
||||
} else {
|
||||
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG];
|
||||
}
|
||||
|
||||
if (this.maintenanceStrategy === 'Migrate') {
|
||||
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
|
||||
} else {
|
||||
@@ -654,6 +668,39 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
setCPUAndMemory() {
|
||||
if (this.cpuMemoryHotplugEnabled) {
|
||||
// set CPU
|
||||
this.spec.template.spec.domain.cpu.sockets = this.cpu;
|
||||
this.spec.template.spec.domain.cpu.cores = 1;
|
||||
|
||||
// set max CPU
|
||||
set(this.spec.template.spec, 'domain.cpu.maxSockets', this.maxCpu);
|
||||
// domain.resources.limits.cpu and memory are defined by k8s which requires string values
|
||||
// see https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/
|
||||
set(this.spec.template.spec, 'domain.resources.limits.cpu', this.maxCpu?.toString());
|
||||
|
||||
// set memory
|
||||
set(this.spec.template.spec, 'domain.memory.guest', this.memory);
|
||||
|
||||
// set max memory
|
||||
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
|
||||
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
|
||||
} else {
|
||||
this.spec.template.spec.domain.cpu.sockets = 1;
|
||||
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
||||
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
|
||||
this.spec.template.spec.domain.resources.limits.memory = this.memory;
|
||||
// clean below fields as we don't need them if not enable CPU and memory hotplug
|
||||
if (this.spec?.template?.spec?.domain?.cpu?.maxSockets) {
|
||||
delete this.spec.template.spec.domain.cpu.maxSockets;
|
||||
}
|
||||
if (this.spec?.template?.spec?.domain?.memory?.maxGuest) {
|
||||
delete this.spec.template.spec.domain.memory.maxGuest;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
parseDiskRows(disk) {
|
||||
const disks = [];
|
||||
const volumes = [];
|
||||
@@ -952,9 +999,12 @@ export default {
|
||||
this['sshKey'] = neu;
|
||||
},
|
||||
|
||||
updateCpuMemory(cpu, memory) {
|
||||
updateCpuMemory(cpu, memory, maxCpu = '', maxMemory = null, cpuMemoryHotplugEnabled = false) {
|
||||
this['cpu'] = cpu;
|
||||
this['memory'] = memory;
|
||||
this['maxCpu'] = maxCpu;
|
||||
this['maxMemory'] = maxMemory;
|
||||
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||
},
|
||||
|
||||
parseDisk(R, index) {
|
||||
@@ -1311,6 +1361,21 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
getCPUMemoryValidation() {
|
||||
const errors = [];
|
||||
const { cpu, memory } = this;
|
||||
|
||||
if ((!cpu)) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
|
||||
}
|
||||
|
||||
if ((!memory)) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
getAccessCredentialsValidation() {
|
||||
const errors = [];
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import SteveModel from '@shell/plugins/steve/steve-class';
|
||||
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
import { HARVESTER_CRD_MAP } from '../config/harvester-map';
|
||||
|
||||
export default class HarvesterResource extends SteveModel {
|
||||
get listLocation() {
|
||||
const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
const resource = HARVESTER_CRD_MAP[this.type] || this.type;
|
||||
|
||||
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || {
|
||||
name,
|
||||
params: {
|
||||
product: this.$rootGetters['productId'],
|
||||
cluster: this.$rootGetters['clusterId'],
|
||||
resource: this.type,
|
||||
resource
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file is Cloud Configuration Templates page model
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { HCI } from '../../types';
|
||||
import HarvesterResource from '../harvester';
|
||||
|
||||
@@ -12,7 +12,7 @@ export default class HarvesterNetworkAttachmentDef extends NetworkAttachmentDef
|
||||
params: {
|
||||
product: HARVESTER_PRODUCT,
|
||||
cluster: this.$rootGetters['clusterId'],
|
||||
resource: this.type,
|
||||
resource: HCI.NETWORK_ATTACHMENT,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,19 @@ export default class HciClusterflow extends HarvesterFlow {
|
||||
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.CLUSTER_FLOW;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get _detailLocation() {
|
||||
const schema = this.$getters['schemaFor'](this.type);
|
||||
|
||||
|
||||
@@ -21,6 +21,19 @@ export default class HciClusteroutput extends LogOutput {
|
||||
};
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.CLUSTER_OUTPUT;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get detailLocation() {
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
|
||||
@@ -14,6 +14,19 @@ export default class HarvesterLogFlow extends Flow {
|
||||
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.FLOW;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get _detailLocation() {
|
||||
const schema = this.$getters['schemaFor'](this.type);
|
||||
|
||||
|
||||
@@ -22,6 +22,19 @@ export default class HarvesterLogOutput extends LogOutput {
|
||||
};
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.OUTPUT;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get detailLocation() {
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
|
||||
@@ -19,6 +19,19 @@ export default class HciAlertmanagerConfig extends SteveModel {
|
||||
};
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.ALERTMANAGERCONFIG;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get doneOverride() {
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
|
||||
@@ -5,6 +5,25 @@ import Secret from '@shell/models/secret';
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
|
||||
export default class HciSecret extends Secret {
|
||||
// prevent harvester secret detail page be overridden.
|
||||
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
|
||||
get fullDetailPageOverride() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.SECRET;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get _detailLocation() {
|
||||
const schema = this.$getters['schemaFor'](this.type);
|
||||
|
||||
|
||||
@@ -4,10 +4,24 @@ import { HCI } from '../../types';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
||||
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||
import { isInternalStorageClass } from '../../utils/storage-class';
|
||||
|
||||
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
|
||||
|
||||
export default class HciStorageClass extends StorageClass {
|
||||
get listLocation() {
|
||||
const listLocation = clone(super.listLocation);
|
||||
|
||||
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
listLocation.params.resource = HCI.STORAGE;
|
||||
|
||||
return listLocation;
|
||||
}
|
||||
|
||||
get harvesterResourcesInExplorer() {
|
||||
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||
}
|
||||
|
||||
get detailLocation() {
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
@@ -85,4 +99,23 @@ export default class HciStorageClass extends StorageClass {
|
||||
get thirdPartyStorageFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
}
|
||||
|
||||
isInternalStorageClass() {
|
||||
return isInternalStorageClass(this.metadata?.name);
|
||||
}
|
||||
|
||||
get availableActions() {
|
||||
let out = super.availableActions || [];
|
||||
|
||||
if (this.isInternalStorageClass()) {
|
||||
out = out.filter((action) => {
|
||||
return !['setDefault', 'setAsDefault', 'promptRemove'].includes(action.action);
|
||||
});
|
||||
}
|
||||
if (out[0] && out[0].divider === true) {
|
||||
out = out.slice(1);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,4 +156,8 @@ export default class HciSetting extends HarvesterResource {
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
get disableResourceDetailDrawer() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ export default class HciVmBackup extends HarvesterResource {
|
||||
return detailLocation;
|
||||
}
|
||||
|
||||
get disableResourceDetailDrawerConfigTab() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get doneOverride() {
|
||||
const route = this.currentRoute();
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
@@ -57,40 +57,42 @@ export default class HciVmImage extends HarvesterResource {
|
||||
|
||||
const customActions = this.isReady ? [
|
||||
{
|
||||
action: 'createFromImage',
|
||||
enabled: canCreateVM,
|
||||
icon: 'icon icon-circle-plus',
|
||||
label: this.t('harvester.action.createVM'),
|
||||
action: 'createFromImage',
|
||||
enabled: canCreateVM,
|
||||
icon: 'icon icon-circle-plus',
|
||||
label: this.t('harvester.action.createVM'),
|
||||
},
|
||||
{
|
||||
action: 'encryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||
icon: 'icon icon-lock',
|
||||
label: this.t('harvester.action.encryptImage'),
|
||||
action: 'encryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||
icon: 'icon icon-lock',
|
||||
label: this.t('harvester.action.encryptImage'),
|
||||
},
|
||||
{
|
||||
action: 'decryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||
icon: 'icon icon-unlock',
|
||||
label: this.t('harvester.action.decryptImage'),
|
||||
action: 'decryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||
icon: 'icon icon-unlock',
|
||||
label: this.t('harvester.action.decryptImage'),
|
||||
},
|
||||
{
|
||||
action: 'imageDownload',
|
||||
enabled: this.links?.download,
|
||||
icon: 'icon icon-download',
|
||||
label: this.t('asyncButton.download.action'),
|
||||
action: 'imageDownload',
|
||||
enabled: this.links?.download,
|
||||
icon: 'icon icon-download',
|
||||
label: this.t('asyncButton.download.action'),
|
||||
}
|
||||
] : [];
|
||||
|
||||
let filteredOut;
|
||||
// imported image only allow download, download yaml and delete actions
|
||||
if (this.isImportedImage) {
|
||||
const custom = customActions.find((a) => a.action === 'imageDownload');
|
||||
const filtered = out.filter(({ action }) => ['download', 'promptRemove'].includes(action));
|
||||
|
||||
if (customActions.length > 0) {
|
||||
filteredOut = out;
|
||||
} else {
|
||||
// if the first item is a divider, remove it from the array
|
||||
filteredOut = out[0]?.divider ? out.slice(1) : out;
|
||||
return custom ? [custom, { divider: true }, ...filtered] : filtered;
|
||||
}
|
||||
|
||||
// if the first item is a divider, remove it from the array
|
||||
const filteredOut = customActions.length > 0 ? out : (out[0]?.divider ? out.slice(1) : out);
|
||||
|
||||
return [
|
||||
...customActions,
|
||||
...filteredOut
|
||||
@@ -211,6 +213,10 @@ export default class HciVmImage extends HarvesterResource {
|
||||
!!this.spec.securityParameters?.sourceImageNamespace;
|
||||
}
|
||||
|
||||
get isImportedImage() {
|
||||
return (this.metadata?.labels?.[HCI_ANNOTATIONS.IMPORTED_IMAGE]) === 'true';
|
||||
}
|
||||
|
||||
get displayNameWithNamespace() {
|
||||
return `${ this.metadata.namespace }/${ this.spec.displayName }`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { HCI } from '../types';
|
||||
import HarvesterResource from './harvester';
|
||||
|
||||
export default class HciSubnet extends HarvesterResource {
|
||||
get groupByVpc() {
|
||||
return this.spec?.vpc || '';
|
||||
}
|
||||
|
||||
get doneOverride() {
|
||||
const detailLocation = clone(this.listLocation);
|
||||
|
||||
detailLocation.params.resource = HCI.VPC;
|
||||
|
||||
return detailLocation;
|
||||
}
|
||||
|
||||
get parentLocationOverride() {
|
||||
return {
|
||||
...this.listLocation,
|
||||
params: {
|
||||
...this.listLocation.params,
|
||||
resource: HCI.VPC
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { HCI } from '../types';
|
||||
import HarvesterResource from './harvester';
|
||||
|
||||
export default class HciVPC extends HarvesterResource {
|
||||
get parentNameOverride() {
|
||||
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.VPC }"`, { count: 1 })?.trim();
|
||||
}
|
||||
|
||||
get doneOverride() {
|
||||
const detailLocation = clone(this.listLocation);
|
||||
|
||||
detailLocation.params.resource = HCI.VPC;
|
||||
|
||||
return detailLocation;
|
||||
}
|
||||
|
||||
get parentLocationOverride() {
|
||||
return {
|
||||
...this.listLocation,
|
||||
params: {
|
||||
...this.listLocation.params,
|
||||
resource: HCI.VPC
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
import { HCI } from '../types';
|
||||
import HarvesterResource from './harvester';
|
||||
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
|
||||
|
||||
export const OFF = 'Off';
|
||||
|
||||
@@ -31,8 +32,6 @@ const PAUSED_VM_MODAL_MESSAGE =
|
||||
|
||||
const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE';
|
||||
const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const POD_STATUS_NOT_READY = 'POD_NOT_READY';
|
||||
|
||||
const POD_STATUS_FAILED = 'POD_FAILED';
|
||||
const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF';
|
||||
@@ -169,6 +168,12 @@ export default class VirtVm extends HarvesterResource {
|
||||
icon: 'icon icon-storage',
|
||||
label: this.t('harvester.action.editVMQuota')
|
||||
},
|
||||
{
|
||||
action: 'cpuMemoryHotplug',
|
||||
enabled: !!this.actions?.cpuAndMemoryHotplug,
|
||||
icon: 'icon icon-os-management',
|
||||
label: this.t('harvester.action.cpuAndMemoryHotplug')
|
||||
},
|
||||
{
|
||||
action: 'createSchedule',
|
||||
enabled: this.schedulingVMBackupFeatureEnabled,
|
||||
@@ -322,6 +327,10 @@ export default class VirtVm extends HarvesterResource {
|
||||
});
|
||||
}
|
||||
|
||||
doSoftReboot() {
|
||||
this.doActionGrowl('softreboot', {});
|
||||
}
|
||||
|
||||
softrebootVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
@@ -476,6 +485,13 @@ export default class VirtVm extends HarvesterResource {
|
||||
});
|
||||
}
|
||||
|
||||
cpuMemoryHotplug(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
component: 'HarvesterCPUMemoryHotPlugDialog'
|
||||
});
|
||||
}
|
||||
|
||||
abortMigrationVM() {
|
||||
this.doActionGrowl('abortMigration', {});
|
||||
}
|
||||
@@ -728,6 +744,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
if (this &&
|
||||
!this.isVMExpectedRunning &&
|
||||
this.isVMCreated &&
|
||||
this.vmi?.status?.phase !== undefined &&
|
||||
this.vmi?.status?.phase !== VMIPhase.Succeeded &&
|
||||
this.vmi?.status?.phase !== VMIPhase.Pending
|
||||
) {
|
||||
@@ -1091,19 +1108,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
maxLength: 63,
|
||||
translationKey: 'harvester.fields.name'
|
||||
},
|
||||
{
|
||||
nullable: false,
|
||||
path: 'spec.template.spec.domain.cpu.cores',
|
||||
min: 1,
|
||||
required: true,
|
||||
translationKey: 'harvester.fields.cpu'
|
||||
},
|
||||
{
|
||||
nullable: false,
|
||||
path: 'spec.template.spec.domain.resources.limits.memory',
|
||||
required: true,
|
||||
translationKey: 'harvester.fields.memory'
|
||||
},
|
||||
{
|
||||
nullable: false,
|
||||
path: 'spec.template.spec',
|
||||
@@ -1127,12 +1131,11 @@ export default class VirtVm extends HarvesterResource {
|
||||
}
|
||||
|
||||
get memorySort() {
|
||||
const memory =
|
||||
this?.spec?.template?.spec?.domain?.resources?.requests?.memory || 0;
|
||||
const memory = getVmCPUMemoryValues(this).memory;
|
||||
|
||||
const formatSize = parseSi(memory);
|
||||
|
||||
return parseInt(formatSize);
|
||||
return parseInt(formatSize, 10);
|
||||
}
|
||||
|
||||
get ingoreVMMessage() {
|
||||
@@ -1158,14 +1161,29 @@ export default class VirtVm extends HarvesterResource {
|
||||
}
|
||||
|
||||
get stateDescription() {
|
||||
const conditions = get(this, 'status.conditions');
|
||||
const restartRequired = findBy(conditions, 'type', 'RestartRequired');
|
||||
|
||||
if (restartRequired && restartRequired.status === 'True') {
|
||||
return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
|
||||
}
|
||||
|
||||
const restoreConditions = this.restoreResource?.status?.conditions || [];
|
||||
const restoreError = restoreConditions.find((s) => s.error === true && s.reason === 'Error' && s.type === 'Ready');
|
||||
|
||||
if (restoreError?.message) {
|
||||
return restoreError.message;
|
||||
}
|
||||
|
||||
return this.ingoreVMMessage ? '' : super.stateDescription;
|
||||
}
|
||||
|
||||
get displayCPU() {
|
||||
return getVmCPUMemoryValues(this).cpu;
|
||||
}
|
||||
|
||||
get displayMemory() {
|
||||
return (
|
||||
this.spec.template.spec.domain.resources?.limits?.memory ||
|
||||
this.spec.template.spec.domain.resources?.requests?.memory
|
||||
);
|
||||
return getVmCPUMemoryValues(this).memory;
|
||||
}
|
||||
|
||||
get isQemuInstalled() {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "harvester",
|
||||
"description": "Rancher UI Extension for Harvester",
|
||||
"version": "1.6.0-dev",
|
||||
"version": "1.6.1-rc1",
|
||||
"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.11.0-0",
|
||||
"catalog.cattle.io/rancher-version": ">= 2.12.0-0",
|
||||
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<script>
|
||||
import { resourceNames } from '@shell/utils/string';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import { BACKUP_TYPE } from '@pkg/harvester/config/types';
|
||||
import { HCI } from '../types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
|
||||
export default {
|
||||
name: 'PromptRemoveVMBackupDialog',
|
||||
|
||||
emits: ['errors'],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
names: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
close: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
|
||||
doneLocation: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { errors: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState('action-menu', ['toRemove']),
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
resourceType() {
|
||||
const isSnapshot = this.value?.[0]?.spec?.type === BACKUP_TYPE.SNAPSHOT;
|
||||
const count = this.names?.length || 1;
|
||||
|
||||
if (isSnapshot) {
|
||||
return this.t(`typeLabel."${ HCI.VM_SNAPSHOT }"`, { count });
|
||||
}
|
||||
|
||||
return this.t(`typeLabel."${ HCI.BACKUP }"`, { count });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resourceNames,
|
||||
async remove(buttonDone) {
|
||||
try {
|
||||
await Promise.all(this.value.map((resource) => resource.remove()));
|
||||
this.close(buttonDone);
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
buttonDone(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-10">
|
||||
<div class="mb-5">
|
||||
{{ t('promptRemove.attemptingToRemove', { type: resourceType }) }} <span
|
||||
v-clean-html="resourceNames(names, null, t)"
|
||||
/>
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(error, i) in errors"
|
||||
:key="i"
|
||||
class=""
|
||||
color="error"
|
||||
:label="error"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,6 +16,8 @@ export const HCI = {
|
||||
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
||||
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
||||
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
||||
SUBNET: 'kubeovn.io.subnet',
|
||||
VPC: 'kubeovn.io.vpc',
|
||||
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
|
||||
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
|
||||
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
export function getVmCPUMemoryValues(vm) {
|
||||
if (!vm) {
|
||||
return {
|
||||
cpu: 0,
|
||||
memory: null,
|
||||
isHotplugEnabled: false
|
||||
};
|
||||
}
|
||||
|
||||
const isHotplugEnabled = isCPUMemoryHotPlugEnabled(vm);
|
||||
|
||||
if (isHotplugEnabled) {
|
||||
return {
|
||||
cpu: vm.spec.template.spec.domain.cpu.sockets,
|
||||
memory: vm.spec.template.spec.domain?.memory?.guest || null,
|
||||
maxCpu: vm.spec.template.spec.domain.cpu?.maxSockets || 0,
|
||||
maxMemory: vm.spec.template.spec.domain?.memory?.maxGuest || null,
|
||||
isHotplugEnabled
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
cpu: vm.spec.template.spec.domain.cpu.cores,
|
||||
memory: vm.spec.template.spec.domain.resources?.limits?.memory || null,
|
||||
isHotplugEnabled
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function isCPUMemoryHotPlugEnabled(vm) {
|
||||
return vm?.metadata?.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] === 'true' ||
|
||||
!!vm?.spec?.template?.spec?.domain.cpu?.maxSockets ||
|
||||
!!vm?.spec?.template?.spec?.domain?.memory?.maxGuest ||
|
||||
false;
|
||||
}
|
||||
@@ -8,7 +8,6 @@ export const docLink = (suffix, version) => {
|
||||
};
|
||||
|
||||
export function getVersion(v) {
|
||||
// e.g v1.4.0
|
||||
if (process.env.VUE_APP_SERVER_VERSION) {
|
||||
return process.env.VUE_APP_SERVER_VERSION;
|
||||
}
|
||||
@@ -18,20 +17,21 @@ export function getVersion(v) {
|
||||
return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`;
|
||||
} catch (error) {
|
||||
// fallback to the latest version
|
||||
return latestVersion(Object.keys(RELEASE_FEATURES));
|
||||
return getLatestVersion();
|
||||
}
|
||||
}
|
||||
|
||||
function latestVersion(versions) {
|
||||
return versions.sort((a, b) => semver.compare(a, b)).pop();
|
||||
function getLatestVersion() {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid).sort(semver.rcompare);
|
||||
|
||||
return allVersions[0] || '';
|
||||
}
|
||||
|
||||
// v1.3.3 => latest v1.3.x, v1.4.2 => latest v1.4.x
|
||||
function latestMinorVersion(v) {
|
||||
const minor = `v${ semver.major(v) }.${ semver.minor(v) }`;
|
||||
const minorVersions = Object.keys(RELEASE_FEATURES).filter((version) => version.startsWith(minor));
|
||||
function getLatestCompatibleVersion(version) {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid);
|
||||
const compatible = allVersions.filter((v) => semver.lte(v, version)).sort(semver.rcompare);
|
||||
|
||||
return latestVersion(minorVersions);
|
||||
return compatible[0] || getLatestVersion();
|
||||
}
|
||||
|
||||
export const featureEnabled = (featureKey, serverVersion) => {
|
||||
@@ -46,12 +46,16 @@ export const featureEnabled = (featureKey, serverVersion) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
let releasedFeatures = RELEASE_FEATURES[version];
|
||||
const compatibleVersion = getLatestCompatibleVersion(version);
|
||||
const releasedFeatures = RELEASE_FEATURES?.[compatibleVersion];
|
||||
|
||||
if (!releasedFeatures) {
|
||||
const fallback = latestMinorVersion(version);
|
||||
if (!Array.isArray(releasedFeatures)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Feature flags for version ${ version } are not defined. Please upgrade Harvester UI extension and check the support matrix.`
|
||||
);
|
||||
|
||||
releasedFeatures = RELEASE_FEATURES[fallback];
|
||||
return false;
|
||||
}
|
||||
|
||||
return releasedFeatures.includes(featureKey);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { INTERNAL_STORAGE_CLASS } from '../config/types';
|
||||
|
||||
export function isInternalStorageClass(name) {
|
||||
return name === INTERNAL_STORAGE_CLASS.VMSTATE_PERSISTENCE ||
|
||||
name === INTERNAL_STORAGE_CLASS.LONGHORN_STATIC;
|
||||
}
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
TAG_VERSION=$1
|
||||
|
||||
if [[ -z "$TAG_VERSION" ]]; then
|
||||
echo "No tag version provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(jq -r .version package.json)
|
||||
EXTENSION_VERSION=$(jq -r .version pkg/harvester/package.json)
|
||||
PKG_VERSION="v${EXTENSION_VERSION}"
|
||||
|
||||
if [[ "$VERSION" != "$EXTENSION_VERSION" ]]; then
|
||||
echo "Package version mismatch: $VERSION vs $EXTENSION_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${TAG_VERSION}" != "${PKG_VERSION}"* ]]; then
|
||||
echo "Package version $PKG_VERSION in pkg/harvester/package.json does not match tag version ${TAG_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Package version and tag version match: ${TAG_VERSION}"
|
||||
@@ -0,0 +1,16 @@
|
||||
/* eslint-disable no-console */
|
||||
import load from "@commitlint/load";
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.error('No PR title as first argument');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const prTitle = process.argv[2];
|
||||
const prLabel = prTitle.split(':')[0].trim();
|
||||
|
||||
const config = await load({ extends: ["./commitlint.config.js"] });
|
||||
const commitPrefix = config?.rules?.['type-enum']?.[2] || [];
|
||||
const label = commitPrefix.includes(prLabel) ? prLabel : 'other';
|
||||
|
||||
console.log(label);
|
||||
Reference in New Issue
Block a user