mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Compare commits
55
Commits
v1.5.0-rc2
...
v1.5.2-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4caecb48a7 | ||
|
|
8d1d370197 | ||
|
|
df9992146c | ||
|
|
acbc120dc1 | ||
|
|
59c63c8d9e | ||
|
|
4a4f2c96bb | ||
|
|
d7bc112a64 | ||
|
|
08e81b0628 | ||
|
|
4599159bff | ||
|
|
ef40cf5d8d | ||
|
|
d57dbd4c8e | ||
|
|
4c68d057c1 | ||
|
|
b4b2017c61 | ||
|
|
9f0d927c9c | ||
|
|
18a53f5baf | ||
|
|
f872bb5abf | ||
|
|
cd015d57e7 | ||
|
|
635254afdc | ||
|
|
3c464e9d10 | ||
|
|
d412ee2aa6 | ||
|
|
580b2892cb | ||
|
|
965c7d9b72 | ||
|
|
01e3867da1 | ||
|
|
fd52218350 | ||
|
|
1839bf387e | ||
|
|
3e9a0a4377 | ||
|
|
a2898a2554 | ||
|
|
d11a045aed | ||
|
|
ae2a2f6d28 | ||
|
|
cf68c3976a | ||
|
|
33f8300c0e | ||
|
|
b26e34a2bd | ||
|
|
01fd3e5e61 | ||
|
|
d5df7ba602 | ||
|
|
607aa106e1 | ||
|
|
49e2be6ff4 | ||
|
|
9ee81f4054 | ||
|
|
f8fa82f614 | ||
|
|
a6c60de2cb | ||
|
|
843b388ede | ||
|
|
f2e8cce29f | ||
|
|
31682dd165 | ||
|
|
9fac43d5e5 | ||
|
|
cd6ea6e402 | ||
|
|
3bfe57dc54 | ||
|
|
089573f193 | ||
|
|
5d0c4f0c5a | ||
|
|
57bb1e8562 | ||
|
|
30ff129691 | ||
|
|
9175f16bfc | ||
|
|
7c5e0f93d1 | ||
|
|
8a7d3dffbe | ||
|
|
b7810c5970 | ||
|
|
72dedd46d1 | ||
|
|
24f716ab20 |
@@ -2,6 +2,7 @@ pull_request_rules:
|
||||
- name: Automatically open backport PR to release-harvester-v1.5
|
||||
conditions:
|
||||
- base=main
|
||||
- label="require backport 1.5"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
|
||||
@@ -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
|
||||
@@ -32,5 +33,4 @@ jobs:
|
||||
with:
|
||||
registry_target: docker.io
|
||||
registry_user: rancher
|
||||
repository_name: harvester-ui-catalog
|
||||
secrets: inherit
|
||||
|
||||
@@ -6,9 +6,6 @@ on:
|
||||
registry_target:
|
||||
required: true
|
||||
type: string
|
||||
repository_name:
|
||||
required: true
|
||||
type: string
|
||||
registry_user:
|
||||
required: true
|
||||
type: string
|
||||
@@ -69,13 +66,5 @@ jobs:
|
||||
|
||||
- name: Build and push UI image
|
||||
run: |
|
||||
publish="yarn publish-pkgs -c -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
||||
$publish
|
||||
|
||||
REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2)
|
||||
IMAGE_VERSION=$(cat package.json | jq .version | sed 's/"//g')
|
||||
FULL_IMAGE_NAME=$(echo ${{ inputs.registry_target }}/${{ inputs.registry_user }}/ui-extension-$REPO_NAME:$IMAGE_VERSION)
|
||||
IMAGE_ID=$(docker inspect --format='{{.Id}}' $FULL_IMAGE_NAME)
|
||||
|
||||
docker tag $IMAGE_ID ${{ inputs.registry_target }}/${{ inputs.registry_user }}/${{ inputs.repository_name }}:$IMAGE_VERSION
|
||||
docker push ${{ inputs.registry_target }}/${{ inputs.registry_user }}/${{ inputs.repository_name }}:$IMAGE_VERSION
|
||||
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
||||
$publish
|
||||
@@ -128,7 +128,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
|
||||
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "harvester-ui-extension",
|
||||
"version": "1.5.0-rc2",
|
||||
"version": "1.5.2-rc1",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -13,24 +13,24 @@
|
||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||
"vue-draggable-next": "^2.2.1",
|
||||
"yaml": "^2.5.1",
|
||||
"@babel/plugin-transform-class-static-block": "7.26.0"
|
||||
"@babel/plugin-transform-class-static-block": "7.28.3"
|
||||
},
|
||||
"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.9",
|
||||
"follow-redirects": "1.15.2",
|
||||
"ejs": "3.1.10",
|
||||
"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": {
|
||||
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
|
||||
</p>
|
||||
|
||||
<p class="mb-5">
|
||||
<p>
|
||||
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
|
||||
:href="releaseVersion"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
|
||||
import { Banner } from '@components/Banner';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
@@ -62,7 +61,7 @@ export default {
|
||||
|
||||
<div
|
||||
class="row"
|
||||
@update:value="update"
|
||||
@input="update"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<LabeledInput
|
||||
|
||||
@@ -59,7 +59,7 @@ export default {
|
||||
name="model"
|
||||
:options="[true,false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@input="update"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
|
||||
@@ -1,69 +1,55 @@
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.0
|
||||
const featuresV130 = [
|
||||
'supportHarvesterClusterVersion'
|
||||
];
|
||||
import semver from 'semver';
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.1
|
||||
const featuresV131 = [
|
||||
...featuresV130,
|
||||
'autoRotateRke2CertsSetting',
|
||||
'supportBundleNodeCollectionTimeoutSetting'
|
||||
];
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
|
||||
const featuresV132 = [
|
||||
...featuresV131,
|
||||
'kubeconfigDefaultTokenTTLMinutesSetting',
|
||||
'improveMaintenanceMode',
|
||||
];
|
||||
|
||||
// TODO: add v1.3.3 official release note
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
|
||||
const featuresV133 = [
|
||||
...featuresV132,
|
||||
];
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.4.0
|
||||
const featuresV140 = [
|
||||
...featuresV133,
|
||||
'cpuPinning',
|
||||
'usbPassthrough',
|
||||
'volumeEncryption',
|
||||
'schedulingVMBackup',
|
||||
'vmSnapshotQuota',
|
||||
'longhornV2LVMSupport',
|
||||
'improveMaintenanceMode',
|
||||
];
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.4.1
|
||||
const featuresV141 = [
|
||||
...featuresV140
|
||||
];
|
||||
|
||||
// TODO: add v1.4.2 official release note
|
||||
const featuresV142 = [
|
||||
...featuresV141,
|
||||
'refreshIntervalInSecond',
|
||||
'allowEmptySnapshotClassName'
|
||||
];
|
||||
|
||||
// TODO: add v1.5.0 official release note
|
||||
const featuresV150 = [
|
||||
...featuresV142,
|
||||
'tpmPersistentState',
|
||||
'efiPersistentState',
|
||||
'untaggedNetworkSetting',
|
||||
'skipSingleReplicaDetachedVol',
|
||||
'thirdPartyStorage'
|
||||
];
|
||||
|
||||
export const RELEASE_FEATURES = {
|
||||
'v1.3.0': featuresV130,
|
||||
'v1.3.1': featuresV131,
|
||||
'v1.3.2': featuresV132,
|
||||
'v1.3.3': featuresV133,
|
||||
'v1.4.0': featuresV140,
|
||||
'v1.4.1': featuresV141,
|
||||
'v1.4.2': featuresV142,
|
||||
'v1.5.0': featuresV150
|
||||
const FEATURE_FLAGS = {
|
||||
'v1.3.0': [
|
||||
'supportHarvesterClusterVersion'
|
||||
],
|
||||
'v1.3.1': [
|
||||
'autoRotateRke2CertsSetting',
|
||||
'supportBundleNodeCollectionTimeoutSetting'
|
||||
],
|
||||
'v1.3.2': [
|
||||
'kubeconfigDefaultTokenTTLMinutesSetting',
|
||||
'improveMaintenanceMode',
|
||||
],
|
||||
'v1.3.3': [],
|
||||
'v1.4.0': [
|
||||
'cpuPinning',
|
||||
'usbPassthrough',
|
||||
'volumeEncryption',
|
||||
'schedulingVMBackup',
|
||||
'vmSnapshotQuota',
|
||||
'longhornV2LVMSupport',
|
||||
'improveMaintenanceMode',
|
||||
],
|
||||
'v1.4.1': [],
|
||||
'v1.4.2': [
|
||||
'refreshIntervalInSecond',
|
||||
'allowEmptySnapshotClassName'
|
||||
],
|
||||
'v1.4.3': [],
|
||||
'v1.5.0': [
|
||||
'tpmPersistentState',
|
||||
'efiPersistentState',
|
||||
'untaggedNetworkSetting',
|
||||
'skipSingleReplicaDetachedVol',
|
||||
'thirdPartyStorage'
|
||||
],
|
||||
'v1.5.1': [],
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
const versions = [...Object.keys(FEATURE_FLAGS)].filter((version) => semver.valid(version)).sort(semver.compare);
|
||||
|
||||
const generatedFlags = {};
|
||||
|
||||
versions.forEach((version, index) => {
|
||||
const previousVersion = versions[index - 1];
|
||||
|
||||
generatedFlags[version] = previousVersion ? [...generatedFlags[previousVersion], ...FEATURE_FLAGS[version]] : [...FEATURE_FLAGS[version]];
|
||||
});
|
||||
|
||||
return generatedFlags;
|
||||
};
|
||||
|
||||
export const RELEASE_FEATURES = generateFeatureFlags();
|
||||
|
||||
@@ -66,4 +66,5 @@ export const HCI = {
|
||||
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
||||
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
||||
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||
K8S_ARCH: 'kubernetes.io/arch'
|
||||
};
|
||||
|
||||
@@ -106,6 +106,12 @@ export default {
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template
|
||||
@@ -127,11 +133,6 @@ export default {
|
||||
@click="create"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
@@ -67,7 +67,7 @@ export default {
|
||||
plusMore() {
|
||||
const remaining = this.resources.length - this.names.length;
|
||||
|
||||
return this.t('dialog.confirmExecution.andOthers', { count: remaining });
|
||||
return this.t('dialog.confirmExecution.andOthers', { count: remaining }, true);
|
||||
},
|
||||
|
||||
type() {
|
||||
@@ -138,6 +138,8 @@ export default {
|
||||
<Banner
|
||||
v-for="(error, i) in errors"
|
||||
:key="i"
|
||||
:label="error"
|
||||
color="error"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -42,8 +42,25 @@ export default {
|
||||
computed: {
|
||||
...mapState('action-menu', ['modalData']),
|
||||
|
||||
warningMessageKey() {
|
||||
return this.modalData.warningMessageKey;
|
||||
title() {
|
||||
return this.modalData.title || 'dialog.promptRemove.title';
|
||||
},
|
||||
|
||||
formattedType() {
|
||||
return this.type.toLowerCase();
|
||||
},
|
||||
|
||||
warningMessage() {
|
||||
if (this.modalData.warningMessage) return this.modalData.warningMessage;
|
||||
|
||||
const isPlural = this.type.endsWith('s');
|
||||
const thisOrThese = isPlural ? 'these' : 'this';
|
||||
const defaultMessage = this.t('dialog.promptRemove.warningMessage', {
|
||||
type: this.formattedType,
|
||||
thisOrThese,
|
||||
});
|
||||
|
||||
return defaultMessage;
|
||||
},
|
||||
|
||||
names() {
|
||||
@@ -70,6 +87,12 @@ export default {
|
||||
return this.resources[0].nameDisplay;
|
||||
},
|
||||
|
||||
needConfirmation() {
|
||||
const { needConfirmation = true } = this.modalData ;
|
||||
|
||||
return needConfirmation === true;
|
||||
},
|
||||
|
||||
plusMore() {
|
||||
const remaining = this.resources.length - this.names.length;
|
||||
|
||||
@@ -97,11 +120,15 @@ export default {
|
||||
},
|
||||
|
||||
deleteDisabled() {
|
||||
if (!this.needConfirmation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.confirmName !== this.nameToMatch;
|
||||
},
|
||||
|
||||
protip() {
|
||||
return this.t('promptRemove.protip', { alternateLabel });
|
||||
return this.t('dialog.promptRemove.protip', { alternateLabel });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -137,35 +164,45 @@ export default {
|
||||
>
|
||||
<template #title>
|
||||
<h4 class="text-default-text">
|
||||
{{ t('promptRemove.title') }}
|
||||
{{ t(title, { type }, true) }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="pl-10 pr-10">
|
||||
<span
|
||||
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
||||
v-clean-html="warningMessage"
|
||||
></span>
|
||||
|
||||
<div class="mt-10 mb-10">
|
||||
<span
|
||||
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
|
||||
></span>
|
||||
</div>
|
||||
<div class="mb-10">
|
||||
<CopyToClipboardText :text="nameToMatch" />
|
||||
</div>
|
||||
<input
|
||||
id="confirm"
|
||||
v-model="confirmName"
|
||||
type="text"
|
||||
/>
|
||||
<div class="text-info mt-20">
|
||||
{{ protip }}
|
||||
<div
|
||||
v-if="needConfirmation"
|
||||
class="mt-20"
|
||||
>
|
||||
<div class="mt-10 mb-10">
|
||||
<span
|
||||
v-clean-html="t('dialog.promptRemove.confirmName', {
|
||||
type: formattedType,
|
||||
nameToMatch: escapeHtml(nameToMatch)
|
||||
}, true)"
|
||||
></span>
|
||||
</div>
|
||||
<div class="mb-10">
|
||||
<CopyToClipboardText :text="nameToMatch" />
|
||||
</div>
|
||||
<input
|
||||
id="confirm"
|
||||
v-model="confirmName"
|
||||
type="text"
|
||||
/>
|
||||
<div class="text-info mt-20">
|
||||
{{ protip }}
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(error, i) in errors"
|
||||
:key="i"
|
||||
:label="error"
|
||||
color="error"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -106,6 +106,12 @@ export default {
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -125,13 +131,6 @@ export default {
|
||||
@click="create"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -57,6 +57,10 @@ export default {
|
||||
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||
return false;
|
||||
}
|
||||
// we won't show golden image volume in the hot plug volume modal
|
||||
if (pvc.isGoldenImageVolume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !pvc.attachVM;
|
||||
})
|
||||
@@ -137,6 +141,12 @@ export default {
|
||||
class="mt-20"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -156,11 +166,6 @@ export default {
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -103,6 +103,12 @@ export default {
|
||||
:label="t('generic.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -121,13 +127,6 @@ export default {
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -120,6 +120,12 @@ export default {
|
||||
v-model:value="description"
|
||||
:label="t('harvester.modal.createTemplate.description')"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -138,11 +144,6 @@ export default {
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -128,6 +128,12 @@ export default {
|
||||
<Banner color="warning">
|
||||
<span>{{ t('harvester.modal.ejectCDROM.warnTip') }}</span>
|
||||
</Banner>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -147,13 +153,6 @@ export default {
|
||||
@click="remove"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -187,6 +187,12 @@ export default {
|
||||
class="mt-20"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -205,11 +211,6 @@ export default {
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -122,16 +122,15 @@ export default {
|
||||
</Banner>
|
||||
{{ t('harvester.modal.downloadImage.startMessage') }}
|
||||
<br /><br />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
>
|
||||
{{ err }}
|
||||
</Banner>
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
|
||||
@@ -161,6 +161,12 @@ export default {
|
||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||
:options="nodeNameList"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template
|
||||
@@ -181,12 +187,6 @@ export default {
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
@@ -96,6 +96,12 @@ export default {
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -109,12 +115,6 @@ export default {
|
||||
</button>
|
||||
<AsyncButton @click="save" />
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -136,6 +136,12 @@ export default {
|
||||
:options="backupOption"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -154,11 +160,6 @@ export default {
|
||||
@click="saveRestore"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -90,6 +90,12 @@ export default {
|
||||
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
||||
class="text-default-text"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -111,13 +117,6 @@ export default {
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -132,6 +132,12 @@ export default {
|
||||
:label="t('generic.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -150,11 +156,6 @@ export default {
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -107,6 +107,12 @@ export default {
|
||||
:placeholder="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.placeholder')"
|
||||
:options="clusterNetworks"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -125,11 +131,6 @@ export default {
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -70,16 +70,16 @@ export default {
|
||||
v-clean-html="t('harvester.modal.restart.tip')"
|
||||
class="pl-10 pr-10"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="bottom">
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="btn role-secondary mr-10"
|
||||
|
||||
@@ -145,6 +145,12 @@ export default {
|
||||
class="mt-20"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -162,10 +168,6 @@ export default {
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -79,6 +79,12 @@ export default {
|
||||
:label="t('harvester.modal.snapshot.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -96,12 +102,6 @@ export default {
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -98,6 +98,12 @@ export default {
|
||||
:label="t('harvester.modal.volumeClone.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
@@ -115,10 +121,6 @@ export default {
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -233,7 +233,7 @@ export default {
|
||||
:disabled="isEdit || isView"
|
||||
:mode="mode"
|
||||
row
|
||||
@input="onTypeChange"
|
||||
@update:value="onTypeChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="row mb-30">
|
||||
|
||||
@@ -307,10 +307,10 @@ export default {
|
||||
const storageClassName = this.value.spec.storageClassName;
|
||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||
|
||||
let readWriteOnce = this.value.isLvm || this.value.isLonghornV2;
|
||||
let readWriteOnce = this.value.isLvm || (!this.value.thirdPartyStorageFeatureEnabled && this.value.isLonghornV2);
|
||||
|
||||
if (storageClass) {
|
||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || storageClass.parameters?.dataEngine === DATA_ENGINE_V2;
|
||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||
}
|
||||
|
||||
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
||||
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
});
|
||||
|
||||
const devices = [
|
||||
...this.otherDevices(this.value.domain.devices.hostDevices || []),
|
||||
...this.otherDevices(this.value?.domain?.devices?.hostDevices || []),
|
||||
...formatted,
|
||||
];
|
||||
|
||||
@@ -121,7 +121,9 @@ export default {
|
||||
return inUse;
|
||||
}
|
||||
|
||||
vm.hostDevices.forEach((device) => {
|
||||
const hostDevices = vm?.hostDevices || [];
|
||||
|
||||
hostDevices.forEach((device) => {
|
||||
inUse[device.name] = { usedBy: [vm.metadata.name] };
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||
|
||||
import { _VIEW, _EDIT } from '@shell/config/query-params';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import { HCI } from '../../types';
|
||||
@@ -90,9 +90,6 @@ export default {
|
||||
},
|
||||
|
||||
sshOption() {
|
||||
if (this.mode === _VIEW || this.mode === _EDIT) {
|
||||
return [];
|
||||
}
|
||||
const out = this.$store.getters['harvester/all'](HCI.SSH).map( (O) => {
|
||||
return {
|
||||
label: O.id,
|
||||
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
});
|
||||
|
||||
const devices = [
|
||||
...this.otherDevices(this.value.domain.devices.hostDevices || []),
|
||||
...this.otherDevices(this.value?.domain?.devices?.hostDevices || []),
|
||||
...formatted,
|
||||
];
|
||||
|
||||
@@ -121,7 +121,9 @@ export default {
|
||||
return inUse;
|
||||
}
|
||||
|
||||
vm.hostDevices.forEach((device) => {
|
||||
const hostDevices = vm?.hostDevices || [];
|
||||
|
||||
hostDevices.forEach((device) => {
|
||||
inUse[device.name] = { usedBy: [vm.metadata.name] };
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -111,7 +111,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pvc.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true') {
|
||||
if (pvc.isGoldenImageVolume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -142,10 +142,12 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// update this.value with existing volume spec, then update to upstream component
|
||||
this.value.accessModes = pvcResource.spec.accessModes[0];
|
||||
this.value.size = pvcResource.spec.resources.requests.storage;
|
||||
this.value.storageClassName = pvcResource.spec.storageClassName;
|
||||
this.value.volumeMode = pvcResource.spec.volumeMode;
|
||||
this.update();
|
||||
},
|
||||
|
||||
'value.type'(neu) {
|
||||
|
||||
@@ -201,10 +201,10 @@ export default {
|
||||
|
||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||
|
||||
let readWriteOnce = this.value.pvc?.isLvm || this.value.pvc?.isLonghornV2;
|
||||
let readWriteOnce = this.value.pvc?.isLvm || (!this.thirdPartyStorageClassEnabled && this.value.pvc?.isLonghornV2);
|
||||
|
||||
if (storageClass) {
|
||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || storageClass.parameters?.dataEngine === DATA_ENGINE_V2;
|
||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||
}
|
||||
|
||||
return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import { clone, set } from '@shell/utils/object';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
@@ -15,9 +16,7 @@ import PodAffinity from '@shell/components/form/PodAffinity';
|
||||
import VGpuDevices from './VirtualMachineVGpuDevices/index';
|
||||
import UsbDevices from './VirtualMachineUSBDevices/index';
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
|
||||
import { clear } from '@shell/utils/array';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { saferDump } from '@shell/utils/create-yaml';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
@@ -212,7 +211,10 @@ export default {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
usbPassthroughEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -409,13 +411,26 @@ export default {
|
||||
}
|
||||
const cloneDeepNewVM = clone(this.value);
|
||||
|
||||
// new VM
|
||||
delete cloneDeepNewVM?.metadata;
|
||||
delete cloneDeepNewVM?.__clone;
|
||||
|
||||
// old VM
|
||||
delete this.cloneVM?.metadata;
|
||||
delete this.cloneVM?.__clone;
|
||||
|
||||
// add empty hostDevices to old VM as CRD does not have it.
|
||||
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
|
||||
|
||||
if (devicesObj && devicesObj.hostDevices === undefined) {
|
||||
set(devicesObj, 'hostDevices', []);
|
||||
}
|
||||
|
||||
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
|
||||
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
|
||||
|
||||
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
|
||||
// only check VM is changed in form page.
|
||||
if (isEqual(oldVM, newVM)) {
|
||||
return;
|
||||
}
|
||||
@@ -424,7 +439,11 @@ export default {
|
||||
this.isOpen = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.restartDialog.resolve = resolve;
|
||||
if (this?.$refs?.restartDialog) {
|
||||
this.$refs.restartDialog.resolve = resolve;
|
||||
} else {
|
||||
return resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -474,6 +493,7 @@ export default {
|
||||
},
|
||||
|
||||
generateYaml() {
|
||||
this.showYaml = this?.$refs?.vmCruResource?.showYaml || false;
|
||||
this.parseVM();
|
||||
const out = saferDump(this.value);
|
||||
|
||||
@@ -487,6 +507,7 @@ export default {
|
||||
<CruResource
|
||||
v-if="spec"
|
||||
id="vm"
|
||||
ref="vmCruResource"
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:cancel-event="true"
|
||||
@@ -674,7 +695,7 @@ export default {
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="enabledPCI"
|
||||
v-if="enabledPCI && usbPassthroughEnabled"
|
||||
:label="t('harvester.tab.usbDevices')"
|
||||
name="usbDevices"
|
||||
:weight="-7"
|
||||
|
||||
@@ -29,7 +29,7 @@ export default {
|
||||
const rows = (this.value || []).map((row) => {
|
||||
let type = 'cidr';
|
||||
|
||||
if (row.rangeStart && row.rangeEnd) {
|
||||
if (row.rangeStart || row.rangeEnd) {
|
||||
type = 'range';
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
:labels="[t('generic.disabled'),t('generic.enabled')]"
|
||||
:options="[false, true]"
|
||||
:disabled="disabled"
|
||||
@input="onToggle"
|
||||
@update:value="onToggle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,11 @@ dialog:
|
||||
message: "Are you sure you want to continue stop the {type} {names}?"
|
||||
pause:
|
||||
message: "Are you sure you want to continue pause the {type} {names}?"
|
||||
promptRemove:
|
||||
title: "Delete {type}"
|
||||
warningMessage: "Deleting the selected {type} permanently removes all resources on {thisOrThese} {type}. This action is irreversible. Do you want to continue?"
|
||||
confirmName: "Type <b>{nameToMatch}</b> to delete the {type}:"
|
||||
protip: "Tip: Hold the {alternateLabel} key while clicking Delete to bypass the confirmation step."
|
||||
|
||||
harvester:
|
||||
branding:
|
||||
@@ -61,6 +66,7 @@ harvester:
|
||||
tip: 'Upload an icon to replace the Harvester favicon in the browser tab. Max file size is 20KB'
|
||||
productLabel: 'Harvester'
|
||||
modal:
|
||||
|
||||
backup:
|
||||
success: 'Backup { backUpName } has been initiated.'
|
||||
addBackup: Add Backup
|
||||
@@ -99,6 +105,9 @@ harvester:
|
||||
tip: Please enter a virtual machine name!
|
||||
success: 'Virtual machine { name } cloned successfully.'
|
||||
failed: 'Failed clone virtual machine!'
|
||||
osImage:
|
||||
title: Delete Image
|
||||
message: Are you sure you want to delete the image { name } ?
|
||||
downloadImage:
|
||||
title: Download Image
|
||||
banner: 'This action takes a while depending on the image size ({ size }). Please be patient.'
|
||||
@@ -781,7 +790,7 @@ harvester:
|
||||
instanceLabels:
|
||||
banner: These labels are automatically synchronized to the virtual machine instance.
|
||||
labels:
|
||||
banner: These key values are added as labels to the virtual machine.
|
||||
banner: These key values are added as labels to the virtual machine.
|
||||
|
||||
volume:
|
||||
label: Volumes
|
||||
@@ -842,6 +851,8 @@ harvester:
|
||||
=1 {1 image is uploading, please do not refresh or close the page.}
|
||||
other {{count} images are uploading, please do not refresh or close the page.}
|
||||
}
|
||||
osUpgrade:
|
||||
uploading: "{name} is uploading, please do not refresh or close the page."
|
||||
checksum: Checksum
|
||||
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
||||
|
||||
@@ -870,14 +881,18 @@ harvester:
|
||||
upgradeNode: Upgrading Node
|
||||
upgradeSysService: Upgrading System Service
|
||||
upgradeImage: Download Upgrade Image
|
||||
osUpgrade: OS Upgrade
|
||||
osUpgrade: Cluster Upgrade
|
||||
uploadNew: Upload New Image
|
||||
deleteHeader: Please select an image to delete.
|
||||
deleteExisting: Delete Existing Image
|
||||
selectExisting: Select Existing Image
|
||||
createRepository: Creating Upgrade Repository
|
||||
succeeded: Succeeded
|
||||
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>.
|
||||
checkReady: I have read and understood the upgrade instructions related to this Harvester version.
|
||||
pending: Pending
|
||||
upload:
|
||||
duplicatedFile: The file you are trying to upload already exists.
|
||||
repoInfo:
|
||||
upgradeStatus: Upgrade Status
|
||||
os: OS
|
||||
@@ -1080,10 +1095,15 @@ harvester:
|
||||
addConfig: Add Configuration
|
||||
|
||||
upgrade:
|
||||
selectExitImage: Please select the OS image to upgrade.
|
||||
unknownImageName: Image name is not found.
|
||||
imageUrl: Please input a valid image URL.
|
||||
chooseFile: Please select to upload an image.
|
||||
checksum: Checksum
|
||||
networkError: Unable to upload the image. Resolve network issues that may have occurred and try again.
|
||||
cancelUpload: Cancelled the image upload.
|
||||
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
|
||||
deleteImage: Please select an image to delete.
|
||||
deleteSuccess: "{name} deleted successfully."
|
||||
harvesterMonitoring:
|
||||
label: Harvester Monitoring
|
||||
section:
|
||||
|
||||
@@ -132,6 +132,7 @@ export default {
|
||||
|
||||
const hash = {
|
||||
vms: this.fetchClusterResources(HCI.VM),
|
||||
pvcs: this.fetchClusterResources(PVC),
|
||||
nodes: this.fetchClusterResources(NODE),
|
||||
events: this.fetchClusterResources(EVENT),
|
||||
metricNodes: this.fetchClusterResources(METRIC.NODE),
|
||||
@@ -221,6 +222,7 @@ export default {
|
||||
nodes: [],
|
||||
metricNodes: [],
|
||||
vms: [],
|
||||
pvcs: [],
|
||||
monitoring: {},
|
||||
VM_DASHBOARD_METRICS_URL,
|
||||
CLUSTER_METRICS_SUMMARY_URL,
|
||||
@@ -285,6 +287,14 @@ export default {
|
||||
out[resource.type].name = this.t(`typeLabel."${ resource.spoofed.name }"`, { count: out[resource.type].total });
|
||||
}
|
||||
|
||||
if (resource.type === PVC) {
|
||||
// filter out the golden image volumes
|
||||
const goldenImageVolumeCount = (this.pvcs || []).filter((pvc) => pvc.isGoldenImageVolume).length;
|
||||
|
||||
out[resource.type].useful = out[resource.type].useful - goldenImageVolumeCount;
|
||||
out[resource.type].total = out[resource.type].total - goldenImageVolumeCount;
|
||||
}
|
||||
|
||||
if (resource.type === HCI.BLOCK_DEVICE) {
|
||||
let total = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
@@ -101,14 +101,14 @@ export default {
|
||||
const metricCol = [
|
||||
{
|
||||
name: 'cpu',
|
||||
labelKey: 'node.detail.glance.consumptionGauge.cpu',
|
||||
labelKey: 'harvester.dashboard.hardwareResourceGauge.cpu',
|
||||
value: 'id',
|
||||
formatter: 'HarvesterCPUUsed',
|
||||
formatterOpts: { showUsed: true },
|
||||
},
|
||||
{
|
||||
name: 'memory',
|
||||
labelKey: 'node.detail.glance.consumptionGauge.memory',
|
||||
labelKey: 'harvester.dashboard.hardwareResourceGauge.memory',
|
||||
value: 'id',
|
||||
formatter: 'HarvesterMemoryUsed',
|
||||
formatterOpts: { showUsed: true },
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Banner } from '@components/Banner';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
|
||||
import { MANAGEMENT } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import Tabbed from '@shell/components/Tabbed/index.vue';
|
||||
import Tab from '@shell/components/Tabbed/Tab.vue';
|
||||
import Settings from '@pkg/harvester/components/SettingList.vue';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING } from '../config/settings';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
@@ -21,14 +20,6 @@ export default {
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
let isDev;
|
||||
|
||||
try {
|
||||
isDev = this.$store.getters['prefs/get'](VIEW_IN_API);
|
||||
} catch {
|
||||
isDev = this.$store.getters['prefs/get'](DEV);
|
||||
}
|
||||
|
||||
const isSingleProduct = !!this.$store.getters['isSingleProduct'];
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
@@ -77,7 +68,7 @@ export default {
|
||||
};
|
||||
|
||||
s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline' || s.customFormatter === 'json' || s.data.customFormatter === 'json');
|
||||
s.hasActions = !s.readOnly || isDev;
|
||||
s.hasActions = s.id === HCI_SETTING.SERVER_VERSION ? true : !s.readOnly;
|
||||
initSettings.push(s);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,10 +6,8 @@ import {
|
||||
} from '@shell/config/types';
|
||||
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 { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
const schema = {
|
||||
id: HCI.VOLUME,
|
||||
@@ -72,7 +70,7 @@ export default {
|
||||
},
|
||||
filterRows() {
|
||||
// we only show the non golden image PVCs in the list
|
||||
return this.rows.filter((pvc) => pvc?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] !== 'true');
|
||||
return this.rows.filter((pvc) => !pvc?.isGoldenImageVolume);
|
||||
},
|
||||
headers() {
|
||||
return [
|
||||
|
||||
@@ -140,6 +140,7 @@ export default {
|
||||
return {
|
||||
OS,
|
||||
isClone,
|
||||
showYaml: false,
|
||||
spec: null,
|
||||
osType: 'linux',
|
||||
sshKey: [],
|
||||
@@ -269,7 +270,7 @@ export default {
|
||||
|
||||
needNewSecret() {
|
||||
// When creating a template it is always necessary to create a new secret.
|
||||
return this.resourceType === HCI.VM_VERSION || this.isCreate;
|
||||
return this.showYaml ? false : this.resourceType === HCI.VM_VERSION || this.isCreate;
|
||||
},
|
||||
|
||||
defaultTerminationSetting() {
|
||||
@@ -669,7 +670,7 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.secretName || this.needNewSecret) {
|
||||
if (this.needNewSecret || !this.secretName) {
|
||||
this.secretName = this.generateSecretName(this.secretNamePrefix);
|
||||
}
|
||||
|
||||
@@ -871,7 +872,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (out.length === 0 && !!this.spec.template.spec.accessCredentials) {
|
||||
if (out.length === 0 && !!this.spec.template.spec.accessCredentials === false) {
|
||||
delete this.spec.template.spec.accessCredentials;
|
||||
} else {
|
||||
this.spec.template.spec.accessCredentials = out;
|
||||
@@ -921,7 +922,9 @@ export default {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return userDataYaml;
|
||||
const hasCloudComment = this.hasCloudConfigComment(userDataYaml);
|
||||
|
||||
return hasCloudComment ? userDataYaml : `#cloud-config\n${ userDataYaml }`;
|
||||
} catch (e) {
|
||||
console.error('Error: Unable to parse yaml document', e); // eslint-disable-line no-console
|
||||
|
||||
@@ -1442,7 +1445,6 @@ export default {
|
||||
sshAuthorizedKeys.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
const userDataJson = this.convertToJson(this.userScript);
|
||||
|
||||
userDataJson.ssh_authorized_keys = sshAuthorizedKeys;
|
||||
@@ -1578,19 +1580,9 @@ export default {
|
||||
*/
|
||||
handler(neu) {
|
||||
if (this.deleteAgent) {
|
||||
let out = this.getUserData({
|
||||
this['userScript'] = this.getUserData({
|
||||
installAgent: neu, osType: this.osType, deletePackage: this.deletePackage
|
||||
});
|
||||
|
||||
if (neu) {
|
||||
const hasCloudComment = this.hasCloudConfigComment(out);
|
||||
|
||||
if (!hasCloudComment) {
|
||||
out = `#cloud-config\n${ out }`;
|
||||
}
|
||||
}
|
||||
|
||||
this['userScript'] = out;
|
||||
this.refreshYamlEditor();
|
||||
}
|
||||
this.deleteAgent = true;
|
||||
@@ -1615,15 +1607,16 @@ export default {
|
||||
},
|
||||
|
||||
sshKey(neu, old) {
|
||||
// refresh yaml editor to get the latest userScript
|
||||
this.userScript = this.getUserData({ installAgent: this.installAgent, osType: this.osType });
|
||||
this.refreshYamlEditor();
|
||||
|
||||
const _diff = difference(old, neu);
|
||||
|
||||
// delete removed ssh key in userdata if needed
|
||||
if (_diff.length > 0 && this.isCreate) {
|
||||
this.deleteSSHFromUserData(_diff);
|
||||
}
|
||||
|
||||
// refresh yaml editor to get the latest userScript
|
||||
this.userScript = this.getUserData({ installAgent: this.installAgent, osType: this.osType });
|
||||
this.refreshYamlEditor();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,8 +67,7 @@ export default class HciNamespace extends namespace {
|
||||
promptRemove(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
warningMessageKey: 'promptRemove.confirmRelatedResource',
|
||||
component: 'ConfirmRelatedToRemoveDialog'
|
||||
component: 'ConfirmRelatedToRemoveDialog'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -115,8 +115,7 @@ export default class HciNode extends HarvesterResource {
|
||||
promptRemove(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
warningMessageKey: 'promptRemove.confirmRelatedResource',
|
||||
component: 'ConfirmRelatedToRemoveDialog'
|
||||
component: 'ConfirmRelatedToRemoveDialog'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,9 +185,16 @@ export default class HciNode extends HarvesterResource {
|
||||
}
|
||||
|
||||
get isKVMDisable() {
|
||||
// Arm based
|
||||
const isARMMachine = this.metadata.labels?.[HCI_ANNOTATIONS.K8S_ARCH]?.includes('arm');
|
||||
|
||||
if (isARMMachine) {
|
||||
return this.status.capacity['devices.kubevirt.io/kvm'] && this.status.capacity['devices.kubevirt.io/kvm'] === '0';
|
||||
}
|
||||
|
||||
const allNotExist = !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] && !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU];
|
||||
|
||||
return this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] === 'false' || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU] === 'false' || allNotExist;
|
||||
return allNotExist || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] === 'false' || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU] === 'false';
|
||||
}
|
||||
|
||||
get stateDisplay() {
|
||||
|
||||
@@ -344,6 +344,10 @@ export default class HciPv extends HarvesterResource {
|
||||
return this.dataEngine === DATA_ENGINE_V2;
|
||||
}
|
||||
|
||||
get isGoldenImageVolume() {
|
||||
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true';
|
||||
}
|
||||
|
||||
get thirdPartyStorageFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
}
|
||||
|
||||
@@ -28,17 +28,15 @@ export default class HciSetting extends HarvesterResource {
|
||||
}
|
||||
|
||||
const schema = this.$getters['schemaFor'](HCI.UPGRADE);
|
||||
|
||||
const hasUpgradeAccess = !!schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase()));
|
||||
|
||||
if (this.id === HCI_SETTING.SERVER_VERSION && hasUpgradeAccess) {
|
||||
const latestUpgrade = this.$getters['all'](HCI.UPGRADE).find((upgrade) => upgrade.isLatestUpgrade);
|
||||
|
||||
out.unshift({
|
||||
action: 'goToAirgapUpgrade',
|
||||
enabled: true,
|
||||
icon: 'icon icon-refresh',
|
||||
label: this.t('harvester.upgradePage.upgrade'),
|
||||
disabled: !!latestUpgrade && !latestUpgrade?.isUpgradeSucceeded
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -50,35 +50,45 @@ export default class HciVmImage extends HarvesterResource {
|
||||
canCreateVM = false;
|
||||
}
|
||||
|
||||
return [
|
||||
const customActions = this.isReady ? [
|
||||
{
|
||||
action: 'createFromImage',
|
||||
enabled: canCreateVM,
|
||||
icon: 'icon icon-circle-plus',
|
||||
label: this.t('harvester.action.createVM'),
|
||||
disabled: !this.isReady,
|
||||
},
|
||||
{
|
||||
action: 'encryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||
icon: 'icon icon-lock',
|
||||
label: this.t('harvester.action.encryptImage'),
|
||||
disabled: !this.isReady,
|
||||
},
|
||||
{
|
||||
action: 'decryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||
icon: 'icon icon-unlock',
|
||||
label: this.t('harvester.action.decryptImage'),
|
||||
disabled: !this.isReady,
|
||||
},
|
||||
{
|
||||
action: 'imageDownload',
|
||||
enabled: this.links?.download,
|
||||
icon: 'icon icon-download',
|
||||
label: this.t('asyncButton.download.action'),
|
||||
},
|
||||
...out
|
||||
action: 'imageDownload',
|
||||
enabled: this.links?.download,
|
||||
icon: 'icon icon-download',
|
||||
label: this.t('asyncButton.download.action'),
|
||||
}
|
||||
] : [];
|
||||
|
||||
let filteredOut;
|
||||
|
||||
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 [
|
||||
...customActions,
|
||||
...filteredOut
|
||||
];
|
||||
}
|
||||
|
||||
@@ -295,7 +305,7 @@ export default class HciVmImage extends HarvesterResource {
|
||||
}
|
||||
|
||||
get uploadImage() {
|
||||
return async(file) => {
|
||||
return async(file, opt = {}) => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('chunk', file);
|
||||
@@ -303,22 +313,23 @@ export default class HciVmImage extends HarvesterResource {
|
||||
try {
|
||||
this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true });
|
||||
|
||||
await this.doAction('upload', formData, {
|
||||
const result = await this.doAction('upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'File-Size': file.size,
|
||||
},
|
||||
params: { size: file.size },
|
||||
signal: opt.signal,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: err.message }, { root: true });
|
||||
|
||||
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
|
||||
|
||||
return Promise.reject(err);
|
||||
throw err;
|
||||
} finally {
|
||||
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
|
||||
}
|
||||
|
||||
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
},
|
||||
{
|
||||
action: 'takeVMSnapshot',
|
||||
enabled: !!this.actions?.backup && !this.longhornV2Volumes.length,
|
||||
enabled: (!!this.actions?.snapshot || !!this.action?.backup) && !this.longhornV2Volumes.length,
|
||||
icon: 'icon icon-snapshot',
|
||||
label: this.t('harvester.action.vmSnapshot')
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "harvester",
|
||||
"description": "Rancher UI Extension for Harvester",
|
||||
"version": "1.5.0-rc2",
|
||||
"version": "1.5.2-rc1",
|
||||
"private": false,
|
||||
"rancher": {
|
||||
"annotations": {
|
||||
|
||||
@@ -7,13 +7,16 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import UpgradeInfo from '../../../../components/UpgradeInfo';
|
||||
|
||||
import { HCI } from '../../../../types';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester';
|
||||
import ImagePercentageBar from '@shell/components/formatter/ImagePercentageBar';
|
||||
import { Banner } from '@components/Banner';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
const IMAGE_METHOD = {
|
||||
NEW: 'new',
|
||||
EXIST: 'exist'
|
||||
NEW: 'new',
|
||||
EXIST: 'exist',
|
||||
DELETE: 'delete'
|
||||
};
|
||||
|
||||
const DOWNLOAD = 'download';
|
||||
@@ -22,7 +25,7 @@ const UPLOAD = 'upload';
|
||||
export default {
|
||||
name: 'HarvesterAirgapUpgrade',
|
||||
components: {
|
||||
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo
|
||||
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo, ImagePercentageBar, Banner
|
||||
},
|
||||
|
||||
inheritAttrs: false,
|
||||
@@ -39,36 +42,32 @@ export default {
|
||||
spec: { image: '' }
|
||||
});
|
||||
|
||||
const imageValue = await this.$store.dispatch('harvester/create', {
|
||||
type: HCI.IMAGE,
|
||||
metadata: {
|
||||
name: '',
|
||||
namespace: 'harvester-system',
|
||||
generateName: 'image-',
|
||||
annotations: {}
|
||||
},
|
||||
spec: {
|
||||
sourceType: UPLOAD,
|
||||
displayName: '',
|
||||
checksum: ''
|
||||
},
|
||||
});
|
||||
|
||||
await this.initImageValue();
|
||||
this.value = value;
|
||||
this.imageValue = imageValue;
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
if (this.uploadController) {
|
||||
this.uploadController.abort();
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
value: null,
|
||||
file: {},
|
||||
imageId: '',
|
||||
imageSource: IMAGE_METHOD.NEW,
|
||||
sourceType: UPLOAD,
|
||||
imageValue: null,
|
||||
errors: [],
|
||||
enableLogging: true,
|
||||
IMAGE_METHOD
|
||||
value: null,
|
||||
file: {},
|
||||
uploadImageId: '',
|
||||
imageId: '',
|
||||
deleteImageId: '',
|
||||
imageSource: IMAGE_METHOD.NEW,
|
||||
sourceType: UPLOAD,
|
||||
uploadController: null,
|
||||
uploadResult: null,
|
||||
imageValue: null,
|
||||
enableLogging: true,
|
||||
IMAGE_METHOD,
|
||||
skipSingleReplicaDetachedVol: false,
|
||||
errors: [],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -77,22 +76,45 @@ export default {
|
||||
return `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
},
|
||||
|
||||
osImageOptions() {
|
||||
return this.$store.getters['harvester/all'](HCI.IMAGE)
|
||||
.filter((I) => I.isOSImage)
|
||||
.map((I) => {
|
||||
return {
|
||||
label: I.spec.displayName,
|
||||
value: I.id,
|
||||
disabled: !I.isReady
|
||||
};
|
||||
});
|
||||
skipSingleReplicaDetachedVolFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
||||
},
|
||||
|
||||
uploadImage() {
|
||||
allOSImages() {
|
||||
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
|
||||
},
|
||||
|
||||
deleteOSImageOptions() {
|
||||
return this.allOSImages.map((I) => {
|
||||
return {
|
||||
label: I.spec.displayName,
|
||||
value: I.id,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
osImageOptions() {
|
||||
return this.allOSImages.map((I) => {
|
||||
return {
|
||||
label: I.spec.displayName,
|
||||
value: I.id,
|
||||
disabled: !I.isReady
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
createNewImage() {
|
||||
return this.imageSource === IMAGE_METHOD.NEW;
|
||||
},
|
||||
|
||||
selectExistImage() {
|
||||
return this.imageSource === IMAGE_METHOD.EXIST;
|
||||
},
|
||||
|
||||
deleteExistImage() {
|
||||
return this.imageSource === IMAGE_METHOD.DELETE;
|
||||
},
|
||||
|
||||
fileName() {
|
||||
return this.file?.name || '';
|
||||
},
|
||||
@@ -100,21 +122,89 @@ export default {
|
||||
canEnableLogging() {
|
||||
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
|
||||
},
|
||||
|
||||
uploadProgress() {
|
||||
const image = this.$store.getters['harvester/byId'](HCI.IMAGE, this.imageValue.id);
|
||||
|
||||
return image?.status?.progress;
|
||||
},
|
||||
|
||||
enableUpgrade() {
|
||||
if (this.deleteExistImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.sourceType === DOWNLOAD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.sourceType === UPLOAD) {
|
||||
return this.fileName === '' ? true : this.uploadProgress === 100;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
isUploading() {
|
||||
return this.fileName !== '' && this.uploadProgress !== 100;
|
||||
},
|
||||
|
||||
showProgressBar() {
|
||||
return this.createNewImage && this.sourceType === UPLOAD && this.isUploading;
|
||||
},
|
||||
|
||||
showUploadSuccessBanner() {
|
||||
return this.createNewImage && this.fileName !== '' && isEmpty(this.errors) && !this.showUploadingWarningBanner && this.uploadResult?._status === 200;
|
||||
},
|
||||
|
||||
showUploadingWarningBanner() {
|
||||
return this.createNewImage && this.isUploading;
|
||||
},
|
||||
|
||||
showUpgradeOptions() {
|
||||
return this.createNewImage || this.selectExistImage;
|
||||
},
|
||||
|
||||
disableUploadButton() {
|
||||
return this.sourceType === UPLOAD && this.isUploading;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
done() {
|
||||
if (this.uploadController) {
|
||||
this.uploadController.abort();
|
||||
}
|
||||
this.$router.push({
|
||||
name: this.doneRoute,
|
||||
params: { resource: HCI.SETTING, product: 'harvester' }
|
||||
});
|
||||
},
|
||||
|
||||
async initImageValue() {
|
||||
this.imageValue = await this.$store.dispatch('harvester/create', {
|
||||
type: HCI.IMAGE,
|
||||
metadata: {
|
||||
name: '',
|
||||
namespace: 'harvester-system',
|
||||
generateName: 'image-',
|
||||
annotations: {}
|
||||
},
|
||||
spec: {
|
||||
sourceType: UPLOAD,
|
||||
displayName: '',
|
||||
checksum: this.imageValue?.spec?.checksum || '',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
let res = null;
|
||||
|
||||
this.file = {};
|
||||
this.errors = [];
|
||||
if (!this.imageValue.spec.displayName && this.uploadImage) {
|
||||
|
||||
if (!this.imageValue.spec.displayName && this.createNewImage) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
|
||||
buttonCb(false);
|
||||
|
||||
@@ -125,26 +215,11 @@ export default {
|
||||
if (this.imageSource === IMAGE_METHOD.NEW) {
|
||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
||||
|
||||
if (this.sourceType === UPLOAD) {
|
||||
this.imageValue.spec.sourceType = UPLOAD;
|
||||
const file = this.file;
|
||||
|
||||
if (!file.name) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.selectExitImage'));
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.imageValue.spec.url = '';
|
||||
|
||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = file.name;
|
||||
|
||||
res = await this.imageValue.save();
|
||||
|
||||
res.uploadImage(file);
|
||||
if (this.sourceType === UPLOAD && this.uploadImageId !== '') {
|
||||
this.value.spec.image = this.uploadImageId;
|
||||
} else if (this.sourceType === DOWNLOAD) {
|
||||
this.imageValue.spec.sourceType = DOWNLOAD;
|
||||
|
||||
if (!this.imageValue.spec.url) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
|
||||
buttonCb(false);
|
||||
@@ -153,12 +228,12 @@ export default {
|
||||
}
|
||||
|
||||
res = await this.imageValue.save();
|
||||
this.value.spec.image = res.id;
|
||||
}
|
||||
|
||||
this.value.spec.image = res.id;
|
||||
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
|
||||
if (!this.imageId) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -169,18 +244,87 @@ export default {
|
||||
if (this.canEnableLogging) {
|
||||
this.value.spec.logEnabled = this.enableLogging;
|
||||
}
|
||||
if (this.skipSingleReplicaDetachedVolFeatureEnabled) {
|
||||
this.value.metadata.annotations = { [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
|
||||
}
|
||||
|
||||
await this.value.save();
|
||||
this.done();
|
||||
buttonCb(true);
|
||||
} catch (e) {
|
||||
this.errors = exceptionToErrorsArray(e);
|
||||
this.errors = [e?.message] || exceptionToErrorsArray(e);
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
|
||||
handleFileUpload() {
|
||||
this.file = this.$refs.file.files[0];
|
||||
async uploadFile(file) {
|
||||
const fileName = file.name;
|
||||
|
||||
if (!fileName) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
|
||||
|
||||
return;
|
||||
}
|
||||
const isDuplicatedFile = this.allOSImages.some((I) => I.spec.displayName === fileName);
|
||||
|
||||
if (isDuplicatedFile) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.upgradePage.upload.duplicatedFile'));
|
||||
this.file = {};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.errors = [];
|
||||
this.imageValue.spec.sourceType = UPLOAD;
|
||||
this.imageValue.spec.displayName = fileName;
|
||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = fileName;
|
||||
this.imageValue.spec.url = '';
|
||||
|
||||
try {
|
||||
const res = await this.imageValue.save();
|
||||
|
||||
this.uploadImageId = res.id;
|
||||
this.uploadController = new AbortController();
|
||||
|
||||
const signal = this.uploadController.signal;
|
||||
|
||||
this.uploadResult = await res.uploadImage(file, { signal });
|
||||
} catch (e) {
|
||||
if (e?.code === 'ERR_NETWORK') {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.networkError'));
|
||||
} else if (e?.code === 'ERR_CANCELED') {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.cancelUpload'));
|
||||
} else {
|
||||
this.errors = exceptionToErrorsArray(e);
|
||||
}
|
||||
this.file = {};
|
||||
this.uploadImageId = '';
|
||||
}
|
||||
},
|
||||
|
||||
handleImageDelete(imageId) {
|
||||
const image = this.allOSImages.find((I) => I.id === imageId);
|
||||
|
||||
if (image) {
|
||||
this.$store.dispatch('harvester/promptModal', {
|
||||
resources: [image],
|
||||
component: 'ConfirmRelatedToRemoveDialog',
|
||||
needConfirmation: false,
|
||||
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
|
||||
});
|
||||
this.deleteImageId = '';
|
||||
}
|
||||
},
|
||||
|
||||
async handleFileUpload() {
|
||||
this.uploadImageId = '';
|
||||
this.errors = [];
|
||||
this.file = this.$refs.file?.files[0];
|
||||
if (this.file) {
|
||||
await this.initImageValue();
|
||||
await this.uploadFile(this.file);
|
||||
}
|
||||
},
|
||||
|
||||
selectFile() {
|
||||
@@ -190,21 +334,27 @@ export default {
|
||||
},
|
||||
|
||||
watch: {
|
||||
imageSource(neu) {
|
||||
if (neu !== IMAGE_METHOD.DELETE) {
|
||||
this.deleteImageId = '';
|
||||
}
|
||||
},
|
||||
|
||||
'imageValue.spec.url': {
|
||||
handler(neu) {
|
||||
const suffixName = neu?.split('/')?.pop();
|
||||
const splitName = suffixName?.split('.') || [];
|
||||
const fileSuffix = splitName?.pop()?.toLowerCase();
|
||||
|
||||
if (splitName.length > 1 && fileSuffix === 'iso' && !this.imageValue.spec.displayName) {
|
||||
if (splitName.length > 1 && fileSuffix === 'iso' && suffixName !== this.imageValue.spec.displayName) {
|
||||
this.imageValue.spec.displayName = suffixName;
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
|
||||
file(neu) {
|
||||
if (!this.imageValue.spec.displayName && neu.name) {
|
||||
// update name input if select new image
|
||||
if (neu.name && neu.name !== this.imageValue.spec.displayName) {
|
||||
this.imageValue.spec.displayName = neu.name;
|
||||
}
|
||||
}
|
||||
@@ -227,9 +377,11 @@ export default {
|
||||
:errors="errors"
|
||||
:can-yaml="false"
|
||||
finish-button-mode="upgrade"
|
||||
:validation-passed="enableUpgrade"
|
||||
:cancel-event="true"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<RadioGroup
|
||||
v-model:value="imageSource"
|
||||
@@ -238,18 +390,57 @@ export default {
|
||||
:options="[
|
||||
IMAGE_METHOD.NEW,
|
||||
IMAGE_METHOD.EXIST,
|
||||
IMAGE_METHOD.DELETE,
|
||||
]"
|
||||
:labels="[
|
||||
t('harvester.upgradePage.uploadNew'),
|
||||
t('harvester.upgradePage.selectExisting'),
|
||||
t('harvester.upgradePage.deleteExisting'),
|
||||
]"
|
||||
/>
|
||||
|
||||
<UpgradeInfo />
|
||||
<UpgradeInfo v-if="createNewImage || selectExistImage" />
|
||||
|
||||
<div v-if="uploadImage">
|
||||
<Banner
|
||||
v-if="showUploadSuccessBanner"
|
||||
color="success"
|
||||
class="mt-0 mb-30"
|
||||
:label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })"
|
||||
/>
|
||||
<Banner
|
||||
v-if="showUploadingWarningBanner"
|
||||
color="warning"
|
||||
class="mt-0 mb-30"
|
||||
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="showUpgradeOptions"
|
||||
class="mt-10 mb-10"
|
||||
>
|
||||
<Checkbox
|
||||
v-if="canEnableLogging"
|
||||
v-model:value="enableLogging"
|
||||
class="check mb-20"
|
||||
type="checkbox"
|
||||
:label="t('harvester.upgradePage.enableLogging')"
|
||||
/>
|
||||
<div
|
||||
v-if="skipSingleReplicaDetachedVolFeatureEnabled"
|
||||
class="mb-20"
|
||||
>
|
||||
<Checkbox
|
||||
v-model:value="skipSingleReplicaDetachedVol"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.upgradePage.skipSingleReplicaDetachedVol')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="createNewImage">
|
||||
<LabeledInput
|
||||
v-model.trim="imageValue.spec.displayName"
|
||||
v-model:value.trim="imageValue.spec.displayName"
|
||||
class="mb-20"
|
||||
label-key="harvester.fields.name"
|
||||
required
|
||||
@@ -261,14 +452,6 @@ export default {
|
||||
label-key="harvester.setting.upgrade.checksum"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-if="canEnableLogging"
|
||||
v-model:value="enableLogging"
|
||||
class="check mb-20"
|
||||
type="checkbox"
|
||||
:label="t('harvester.upgradePage.enableLogging')"
|
||||
/>
|
||||
|
||||
<RadioGroup
|
||||
v-model:value="sourceType"
|
||||
class="mb-20 image-group"
|
||||
@@ -285,7 +468,7 @@ export default {
|
||||
|
||||
<LabeledInput
|
||||
v-if="sourceType === 'download'"
|
||||
v-model.trim="imageValue.spec.url"
|
||||
v-model:value.trim="imageValue.spec.url"
|
||||
class="labeled-input--tooltip"
|
||||
required
|
||||
label-key="harvester.image.url"
|
||||
@@ -298,6 +481,7 @@ export default {
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-primary"
|
||||
:disabled="disableUploadButton"
|
||||
@click="selectFile"
|
||||
>
|
||||
{{ t('harvester.image.uploadFile') }}
|
||||
@@ -318,28 +502,56 @@ export default {
|
||||
{{ fileName ? fileName : t('harvester.generic.noFileChosen') }}
|
||||
</span>
|
||||
</div>
|
||||
<ImagePercentageBar
|
||||
v-if="showProgressBar"
|
||||
class="mt-20"
|
||||
:value="uploadProgress"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LabeledSelect
|
||||
v-else
|
||||
v-if="selectExistImage"
|
||||
v-model:value="imageId"
|
||||
:options="osImageOptions"
|
||||
required
|
||||
class="mb-20"
|
||||
label-key="harvester.fields.image"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="deleteExistImage"
|
||||
class="mt-20"
|
||||
>
|
||||
<Banner
|
||||
color="info"
|
||||
class="mt-10 mb-30"
|
||||
:label="t('harvester.upgradePage.deleteHeader')"
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-model:value="deleteImageId"
|
||||
:options="deleteOSImageOptions"
|
||||
required
|
||||
class="mb-20"
|
||||
label-key="harvester.fields.image"
|
||||
@update:value="handleImageDelete"
|
||||
/>
|
||||
</div>
|
||||
</CruResource>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#air-gap {
|
||||
padding: 20px;
|
||||
|
||||
:deep() .image-group .radio-group {
|
||||
display: flex;
|
||||
.radio-container {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
.parent {
|
||||
grid-template-columns:auto 40px;
|
||||
}
|
||||
.chooseFile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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,30 +17,45 @@ 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) => {
|
||||
const minSupportedVersion = '1.3.0';
|
||||
|
||||
const version = getVersion(serverVersion);
|
||||
let releasedFeatures = RELEASE_FEATURES[version];
|
||||
|
||||
if (!releasedFeatures) {
|
||||
const fallback = latestMinorVersion(version);
|
||||
if (semver.lt(version.replace('v', ''), minSupportedVersion)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Harvester UI extension only supports Harvester cluster version >= ${ minSupportedVersion }. Current version: ${ version }`);
|
||||
|
||||
releasedFeatures = RELEASE_FEATURES[fallback];
|
||||
return false;
|
||||
}
|
||||
|
||||
const compatibleVersion = getLatestCompatibleVersion(version);
|
||||
const releasedFeatures = RELEASE_FEATURES?.[compatibleVersion];
|
||||
|
||||
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.`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return releasedFeatures.includes(featureKey);
|
||||
|
||||
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}"
|
||||
Reference in New Issue
Block a user