Compare commits

...
Author SHA1 Message Date
freeze 3c92a06268 feat: drop striped LVM volume group type from v1.9.1 (#1167)
The LVM add-on reaches GA in v1.9.1 and only supports dm-thin.
    Gate the striped option behind a v1.9.1 feature flag, default new
    LVM storage classes to dm-thin, and reset a cloned striped type so
    older clusters keep the current behaviour.

    Related issue: harvester/harvester#11731

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-09-23 12:14:48 +08:00
freeze 6256cdc97a ci: pin package version to the git tag for tag builds (#1165)
Sprint tags (vX.Y.Z-dev-YYYYMMDD) only share a prefix with the
    x.y.z-dev version in package.json, so every sprint plugin reported
    the same version and the catalog image push hit "Cannot overwrite
    production image". Bump the tree to the tag before building.

    Related issue: harvester/harvester#11047

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-09-22 19:33:24 +08:00
renovate[bot] 4323132565 deps: update patch dependencies (#1159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-21 11:00:14 +08:00
Andy Lee d05ec68e16 chore: add v1.10.0 and v1.9.1 feature flags (#1156)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-09-18 16:34:06 +08:00
Andy Lee cb2cef5e73 fix: avoid userpreferences PUT flood from SideNav refresh trigger (#1119)
kickSideNav toggled a dummy favorite via addFavorite/removeFavorite, which persist through prefs/set and issue a PUT /v1/userpreferences on every toggle, flooding the backend and surfacing 500 errors. Switch to the in-memory prefs/load mutation so the SideNav still re-renders without any network request.

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-09-16 09:31:28 +08:00
Andy Lee 45116b568a feat: change logo based on server-flavor setting (#1151)
* feat: use prime logo when server-flavor is prime

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

* feat: expose server-flavor setting

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

* refactor: copilot review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-09-16 09:30:37 +08:00
Victor Chang f42d798d10 fix: require names for cloud configuration templates (#1138)
Signed-off-by: Victor Chang <t7902195204@gmail.com>
2026-09-16 09:17:29 +08:00
Andy Lee 50b4cf5248 feat: add rancher k3k addon UI page (#1137)
* feat: add k3k addon UI

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

* feat: add rancher-k3k addon UI

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

* refactor: copilot review

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

* refactor: copilot review

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

* refactor: remove k3k edit page

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

* refactor: k3k addon description

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-09-14 15:46:14 +08:00
Siva Kanakala cba37194b6 fix: preserve per-volume storage class when editing image-sourced volumes and fallback to the image StorageClass for new volumes (#1144)
Signed-off-by: Siva Kanakala <siva.kanakala@suse.com>
2026-09-10 13:08:53 +08:00
renovate[bot] bbe0c4643f deps: update dependency qs to v6.16.0 [security] (#1134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-07 13:56:15 +08:00
marcelofukumoto 00584f1d19 chore(deps): bump @rancher/shell to 3.0.13 (#1130)
Clears 26 of the 54 open Dependabot alerts (axios, dompurify, serve-static, and
drops vulnerable send / @tootallnate/once copies).

3.0.13 ships the flat-config ESLint 10 toolchain, so lint now routes through the
@rancher/shell launcher (keeping the legacy .eslintrc on ESLint 7); a prelint step
installs an ESLint-7-compatible eslint-plugin-jest into the isolated toolchain.

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
2026-09-04 10:36:52 +02:00
Tim SerongandVolker Theile 4fa6470343 feat: LHv2: graduate from "Experimental" to "Preview" (#1129)
In Harvester v1.9.0, we're moving LHv2 from "Experimental" to "Preview",
so I've added a new preview label to the settings interface.  I figure
given Experimental had the "error" color, I should make "Preview" use
the warning color, to make it a bit less alarming :-)

Related-to: https://github.com/harvester/harvester/issues/9960

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-by: Volker Theile <vtheile@suse.com>
2026-09-01 11:30:38 +10:00
Andy Lee d550046189 fix: vGPU doesn't display when creating harvester downstream cluster (#1123)
* fix: vGPU doesn't display in edit VM page

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

* fix: align PCI passthrough action buttons height

Match the enable/disable passthrough and Actions dropdown buttons to 40px and move the selected-count label below them.

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-25 16:24:37 +08:00
Po Han Huang 5f32a9ade5 feat: improve readability for vm migration UI (#1112)
Signed-off-by: pohanhuang <pohan.huang@suse.com>
2026-08-18 12:32:02 +08:00
marcelofukumoto 3f39d561be security: fix 15 transitive Dependabot alerts via yarn.lock re-resolution (#1115)
Re-resolve 15 vulnerable transitive dependencies to their latest in-range
patched versions in yarn.lock only (no package.json changes): @babel/core,
@babel/plugin-transform-modules-systemjs, brace-expansion, fast-uri, form-data,
http-proxy-middleware, immutable, joi, js-cookie, launch-editor, nanoid,
shell-quote, svgo, websocket-driver, ws.

The remaining alerts are pinned by parent packages (chiefly @rancher/shell) or
need a cross-major bump, so they can't be resolved by the lockfile alone; they
are tracked separately.

Signed-off-by: Marcelo Fukumoto <marceloyfukumoto@gmail.com>
2026-08-17 10:30:05 +02:00
Andy Lee 7c84269ed6 fix: exclude forklift namespace from system classification (#1114)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-17 15:23:23 +10:00
Andy Lee f63be73b11 fix: ACCESS_MODE undefined error in forklift mapping page (#1111) 2026-08-14 10:01:27 +08:00
Andy Lee fabc1cec21 fix: prevent duplicated cloud-config for empty user data (#1109)
* fix: prevent duplicated cloud-config for empty user data

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

* refactor: AI review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-12 14:20:25 +08:00
Andy Lee ed2b6f2551 fix: remove map ownerref writeback and delete maps via plan refs (#1105)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-10 18:14:48 +08:00
renovate[bot] 72282ae50e deps: update dependency @types/lodash to v4.17.25 (#1099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 10:10:31 +08:00
Volker Theile 8fba4e7050 fix: Rename to Windows Unattended & Sysprep (#1096)
Rename title and other mentions of Sysprep to prevent misunderstandings regarding functionality. For the average Windows administrator who simply wants to automate the installation of a Windows ISO, it isn't immediately intuitive when the term “Sysprep” is used on its own.

Related to: https://github.com/harvester/harvester/issues/1836
Related to: https://github.com/harvester/harvester-ui-extension/pull/954

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-08-07 11:22:23 +08:00
32 changed files with 2250 additions and 1568 deletions
@@ -125,6 +125,13 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
# Tag builds (RC, formal and sprint releases) ship the tag as the package
# version. RC/formal tags already match package.json; sprint tags
# (vX.Y.Z-dev-YYYYMMDD) only share its prefix, so pin it here for the build.
- name: Pin package version to tag
if: ${{ inputs.CI_BUILD_TAG != '' }}
run: ./scripts/bump_version.sh "${{ inputs.CI_BUILD_TAG }}"
- id: ci-build-pkg
name: Build pkg
run: ./scripts/ci-build-pkg.sh harvester
@@ -58,6 +58,12 @@ jobs:
- name: Setup Nodejs with yarn install
uses: ./.github/actions/setup
# The catalog image is tagged with the package.json version. Sprint tags
# (vX.Y.Z-dev-YYYYMMDD) only share its prefix, so pin it to the tag here.
- name: Pin package version to tag
if: ${{ github.ref_type == 'tag' }}
run: ./scripts/bump_version.sh "${{ github.ref_name }}"
- name: Build and push UI image
run: |
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
+9 -7
View File
@@ -7,7 +7,7 @@
},
"dependencies": {
"@babel/plugin-transform-class-static-block": "7.29.7",
"@rancher/shell": "3.0.12-rc.7",
"@rancher/shell": "3.0.13",
"@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5",
@@ -27,7 +27,7 @@
"yaml": "^2.5.1"
},
"resolutions": {
"@types/node": "25.9.5",
"@types/node": "25.9.6",
"cronstrue": "2.59.0",
"d3-color": "3.1.0",
"ejs": "3.1.10",
@@ -35,11 +35,11 @@
"glob": "7.2.3",
"glob-parent": "6.0.2",
"json5": "2.2.3",
"@types/lodash": "4.17.24",
"@types/lodash": "4.17.25",
"merge": "2.1.1",
"node-forge": "1.4.0",
"nth-check": "2.1.1",
"qs": "6.15.3",
"qs": "6.16.0",
"roarr": "7.21.7",
"semver": "7.8.5",
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
@@ -48,15 +48,17 @@
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",
"build": "./node_modules/.bin/vue-cli-service build",
"clean": "./node_modules/@rancher/shell/scripts/clean",
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
"lint": "./node_modules/@rancher/shell/scripts/lint --max-warnings 0 --ext .js,.ts,.vue .",
"lint:fix": "./node_modules/@rancher/shell/scripts/lint --fix --max-warnings 0 --ext .js,.ts,.vue .",
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
"commitlint": "commitlint --edit",
"prepare": "husky",
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh",
"prelint": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error",
"prelint:fix": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error"
},
"devDependencies": {
"@commitlint/load": "^19.8.1",
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

+21 -14
View File
@@ -10,6 +10,7 @@ import { docLink } from '../utils/feature-flags';
const CATEGORY = {
ui: [
'branding',
HCI_SETTING.SERVER_FLAVOR,
'ui-source',
'ui-index',
]
@@ -53,7 +54,7 @@ export default {
};
},
computed: { ...mapGetters({ t: 'i18n/t' }) },
computed: { ...mapGetters({ t: 'i18n/t', isHarvesterPrime: 'harvester-common/isHarvesterPrime' }) },
watch: {
settings: {
@@ -231,14 +232,21 @@ export default {
v-if="setting.customized"
class="modified"
>
Modified
{{ t('advancedSettings.tags.modified.label') }}
</span>
<span
v-if="setting.experimental"
v-clean-tooltip="t('advancedSettings.experimental')"
v-clean-tooltip="t('advancedSettings.tags.experimental.description')"
class="experimental"
>
Experimental
{{ t('advancedSettings.tags.experimental.label') }}
</span>
<span
v-if="setting.preview"
v-clean-tooltip="t('advancedSettings.tags.preview.description')"
class="preview"
>
{{ t('advancedSettings.tags.preview.label') }}
</span>
</h1>
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
@@ -262,7 +270,8 @@ export default {
v-if="!setting.hide"
class="settings-value"
>
<pre v-if="setting.kind === 'json'">{{ setting.json }}</pre>
<pre v-if="setting.id === 'branding' && isHarvesterPrime">{{ $store.getters['harvester-common/privateLabel'] }}</pre>
<pre v-else-if="setting.kind === 'json'">{{ setting.json }}</pre>
<pre v-else-if="setting.kind === 'multiline'">{{ setting.data.value || setting.data.default }}</pre>
<pre v-else-if="setting.kind === 'enum'">{{ t(setting.enum) }}</pre>
<pre v-else-if="setting.kind === 'custom' && setting.custom">{{ setting.custom }}</pre>
@@ -352,21 +361,19 @@ export default {
flex: 1;
}
.modified {
.modified,
.experimental,
.preview {
margin-left: 10px;
border: 1px solid var(--primary);
border: 1px solid;
border-radius: 5px;
padding: 2px 10px;
font-size: 12px;
}
.experimental {
margin-left: 10px;
border: 1px solid var(--error);
border-radius: 5px;
padding: 2px 10px;
font-size: 12px;
}
.modified { border-color: var(--primary); }
.experimental { border-color: var(--error); }
.preview { border-color: var(--warning); }
.no-search-match {
text-align: center;
@@ -15,7 +15,7 @@ import MappingColumn from './MappingColumn.vue';
import StorageDefaultsModal from './StorageDefaultsModal.vue';
const DEFAULT_VOLUME_MODE = VOLUME_MODE.FILE_SYSTEM;
const DEFAULT_ACCESS_MODES = [ACCESS_MODE.READ_WRITE_MANY];
const DEFAULT_ACCESS_MODE = ACCESS_MODE?.READ_WRITE_MANY ?? 'ReadWriteMany';
const props = defineProps({
providerName: { type: String, default: '' },
@@ -268,9 +268,9 @@ const buildStorageEntries = () => {
capacity: 0,
target: '',
volumeMode: DEFAULT_VOLUME_MODE,
accessModes: [...DEFAULT_ACCESS_MODES],
accessModes: [DEFAULT_ACCESS_MODE],
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
inheritedFromProvider: false,
overridden: false,
usedBy: [],
@@ -310,9 +310,9 @@ const buildStorageEntriesFromProvider = (datastoresData) => {
capacity: ds.capacity || 0,
target: '',
volumeMode: DEFAULT_VOLUME_MODE,
accessModes: [...DEFAULT_ACCESS_MODES],
accessModes: [DEFAULT_ACCESS_MODE],
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
inheritedFromProvider: false,
overridden: false,
usedBy: [],
@@ -273,19 +273,6 @@ const startMigrationAction = async() => {
await plan.save();
const planOwnerRef = {
apiVersion: FORKLIFT_API_VERSION,
kind: 'Plan',
name: plan.metadata.name,
uid: plan.metadata.uid,
blockOwnerDeletion: true,
};
networkMap.metadata.ownerReferences = [planOwnerRef];
await networkMap.save();
storageMap.metadata.ownerReferences = [planOwnerRef];
await storageMap.save();
// Kick off the first migration through the model so the Migration payload
// lives in a single place (also reused by the dashboard start/restart action).
await plan.startMigration();
@@ -8,16 +8,22 @@ import { useI18n } from '@shell/composables/useI18n';
import { VOLUME_MODE, ACCESS_MODE } from '../../config/types';
const VOLUME_MODE_OPTIONS = [VOLUME_MODE.FILE_SYSTEM, VOLUME_MODE.BLOCK];
const ACCESS_MODE_OPTIONS = [ACCESS_MODE.READ_WRITE_ONCE, ACCESS_MODE.READ_WRITE_MANY, ACCESS_MODE.READ_ONLY_MANY];
const {
READ_WRITE_ONCE = 'ReadWriteOnce',
READ_ONLY_MANY = 'ReadOnlyMany',
READ_WRITE_MANY = 'ReadWriteMany',
READ_WRITE_ONCE_POD = 'ReadWriteOncePod'
} = ACCESS_MODE || {};
const ACCESS_MODE_OPTIONS = [READ_WRITE_ONCE, READ_WRITE_MANY, READ_ONLY_MANY, READ_WRITE_ONCE_POD];
const props = defineProps({
storageClassName: { type: String, default: '' },
providerName: { type: String, default: '' },
showInherited: { type: Boolean, default: false },
volumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
accessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
accessModes: { type: Array, default: () => ['ReadWriteMany'] },
inheritedVolumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
inheritedAccessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
inheritedAccessModes: { type: Array, default: () => ['ReadWriteMany'] },
});
const emit = defineEmits(['apply', 'close']);
@@ -26,7 +32,7 @@ const store = useStore();
const { t } = useI18n(store);
const localVolumeMode = ref(props.volumeMode || VOLUME_MODE.FILE_SYSTEM);
const localAccessMode = ref(props.accessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY);
const localAccessMode = ref(props.accessModes?.[0] || READ_WRITE_MANY);
const volumeModeOptions = VOLUME_MODE_OPTIONS.map((value) => ({ label: value, value }));
const accessModeOptions = ACCESS_MODE_OPTIONS.map((value) => ({ label: value, value }));
@@ -42,7 +48,7 @@ const apply = () => {
// Repopulate the dropdowns with the provider default values; the user then applies.
const reset = () => {
localVolumeMode.value = props.inheritedVolumeMode || VOLUME_MODE.FILE_SYSTEM;
localAccessMode.value = props.inheritedAccessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY;
localAccessMode.value = props.inheritedAccessModes?.[0] || READ_WRITE_MANY;
};
const cancel = () => {
+4
View File
@@ -77,6 +77,10 @@ const FEATURE_FLAGS = {
'staticIPForVM',
'fsFreezeDeadline',
],
'v1.9.1': [
'lvmDmThinOnly',
],
'v1.10.0': [],
};
const generateFeatureFlags = () => {
+47 -9
View File
@@ -57,6 +57,8 @@ import {
} from './table-headers';
import { ADD_ONS } from './harvester-map';
import { registerAddonSideNav } from '../utils/dynamic-nav';
import { setVendor } from '@shell/config/private-label';
import { createCssVars } from '@shell/utils/color';
const TEMPLATE = HCI.VM_VERSION;
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
@@ -109,15 +111,51 @@ export function init($plugin, store) {
}
};
store.dispatch('setIsSingleProduct', {
productName: PRODUCT_NAME,
logo: require(`@shell/assets/images/providers/harvester.svg`),
productNameKey: 'harvester.productLabel',
getVersionInfo: (store) => store.getters[`${ PRODUCT_NAME }/byId`]?.(HCI.SETTING, 'server-version')?.value || 'unknown',
afterLoginRoute: home,
logoRoute: home,
supportCustomLogo: true
});
store.watch(
(_state, getters) => JSON.stringify([
!!getters['harvester-common/isHarvesterPrime'],
getters['prefs/theme'] === 'dark',
getters['harvester-common/privateLabel']
]),
(value) => {
const [isHarvesterPrime, isDark, privateLabel] = JSON.parse(value);
const primeLogo = isDark ? require('../assets/images/dark/suse-virtualization.svg') : require('../assets/images/suse-virtualization.svg');
setVendor(privateLabel || '');
let primaryColorStyle = document.getElementById('harvester-prime-primary-color');
// set harvester prime primary color style to #4dd192 (Cyan Green)
if (isHarvesterPrime) {
if (!primaryColorStyle) {
primaryColorStyle = document.createElement('style');
primaryColorStyle.id = 'harvester-prime-primary-color';
document.head.appendChild(primaryColorStyle);
primaryColorStyle.sheet.insertRule('body.theme-light, body.theme-dark {}', 0);
}
const colors = createCssVars('#4dd192', isDark ? 'dark' : 'light');
const rule = primaryColorStyle.sheet.cssRules[0];
Object.entries(colors).forEach(([property, color]) => rule.style.setProperty(property.trim(), color));
} else {
primaryColorStyle?.remove();
}
const primeLabelKey = privateLabel ? 'harvester.branding.primeLabel' : 'harvester.branding.primeEmptyLabel';
store.dispatch('setIsSingleProduct', {
productName: PRODUCT_NAME,
logo: isHarvesterPrime ? primeLogo : require(`@shell/assets/images/providers/harvester.svg`),
productNameKey: isHarvesterPrime ? primeLabelKey : 'harvester.productLabel',
getVersionInfo: (store) => store.getters[`${ PRODUCT_NAME }/byId`]?.(HCI.SETTING, 'server-version')?.value || 'unknown',
afterLoginRoute: home,
logoRoute: home,
supportCustomLogo: !isHarvesterPrime
});
},
{ immediate: true }
);
}
product({
+5 -1
View File
@@ -3,6 +3,7 @@ export const HCI_SETTING = {
CONTAINERD_REGISTRY: 'containerd-registry',
LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version',
SERVER_FLAVOR: 'server-flavor',
UI_INDEX: 'ui-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
@@ -68,6 +69,7 @@ export const HCI_ALLOWED_SETTINGS = {
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
},
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.SERVER_FLAVOR]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
@@ -111,15 +113,17 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
kind: 'boolean',
experimental: true,
preview: true,
featureFlag: 'longhornV2LVMSupport'
},
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
kind: 'boolean',
preview: true,
featureFlag: 'longhornV2HugepageSettings'
},
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
kind: 'number',
preview: true,
featureFlag: 'longhornV2HugepageSettings'
},
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
@@ -390,14 +390,17 @@ export default {
:vmi-resource="vmi"
:vmim-resource="vmim"
/>
<DashboardMetrics
<div
v-if="showVmMetrics && liveMigrationProgressEnabled"
:detail-url="VM_MIGRATION_DETAIL_URL"
graph-height="640px"
:has-summary-and-detail="false"
:vars="graphVars"
class="mb-30"
/>
class="migration-metrics-scroll mb-30"
>
<DashboardMetrics
:detail-url="VM_MIGRATION_DETAIL_URL"
graph-height="960px"
:has-summary-and-detail="false"
:vars="graphVars"
/>
</div>
<Events
v-if="liveMigrationProgressEnabled"
:resource="vmi"
@@ -436,3 +439,10 @@ export default {
</Tabbed>
</div>
</template>
<style lang="scss" scoped>
.migration-metrics-scroll {
max-height: 640px;
overflow-y: auto;
}
</style>
+164
View File
@@ -0,0 +1,164 @@
<script>
import merge from 'lodash/merge';
import jsyaml from 'js-yaml';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { LabeledInput } from '@components/Form/LabeledInput';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';
const DEFAULT_VALUE = {
rancher: {
hostname: '',
version: '',
bootstrapPassword: '',
},
};
export default {
name: 'HarvesterEnableK3k',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
const addon = this.resources[0];
let valuesContentJson;
try {
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(addon.spec.valuesContent));
} catch (e) {
valuesContentJson = merge({}, DEFAULT_VALUE);
}
return { valuesContentJson };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
buttonDisabled() {
const { rancher } = this.valuesContentJson;
return !(rancher?.hostname || '').trim() || !(rancher?.version || '').trim() || !(rancher?.bootstrapPassword || '').trim();
}
},
methods: {
close() {
this.$emit('close');
},
async enable(buttonCb) {
const addon = this.resources[0];
try {
addon.spec.valuesContent = jsyaml.dump(this.valuesContentJson);
addon.spec.enabled = true;
await addon.save();
buttonCb(true);
this.close();
} catch (err) {
addon.spec.enabled = false;
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(addon.metadata.name) }),
err,
}, { root: true });
buttonCb(false);
}
}
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
<h4
v-clean-html="t('harvester.addons.rancherK3k.enable.title')"
class="text-default-text"
/>
</template>
<template #body>
<div class="body">
<div class="row mb-15">
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.rancher.hostname"
:required="true"
:label="t('harvester.addons.rancherK3k.hostname')"
placeholder="rancher.$vip.nip.io"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.rancher.version"
:required="true"
:label="t('harvester.addons.rancherK3k.version')"
placeholder="v1.14.0"
/>
</div>
</div>
<div class="row mb-15">
<div class="col span-12">
<LabeledInput
v-model:value="valuesContentJson.rancher.bootstrapPassword"
:required="true"
type="password"
:label="t('harvester.addons.rancherK3k.password')"
:tooltip="t('harvester.addons.rancherK3k.passwordTooltip')"
/>
</div>
</div>
</div>
</template>
<template #actions>
<div class="buttons actions">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="enable"
:disabled="buttonDisabled"
@click="enable"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.body {
display: flex;
flex-direction: column;
min-width: 400px;
}
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -0,0 +1,167 @@
<script>
import merge from 'lodash/merge';
import jsyaml from 'js-yaml';
import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
const DEFAULT_VALUE = {
certManagerVersion: 'v1.20.2',
rancher: {
hostname: '',
version: '',
bootstrapPassword: '',
},
k3kCluster: {
servers: 1,
version: 'v1.35.4-k3s1',
storageClassName: 'harvester-longhorn',
},
};
export default {
name: 'EditAddonRancherK3k',
components: { LabeledInput, RadioGroup },
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
},
mode: {
type: String,
required: true
},
registerBeforeHook: {
type: Function,
required: true,
},
},
data() {
let valuesContentJson = {};
try {
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(this.value.spec.valuesContent));
} catch (err) {
valuesContentJson = merge({}, DEFAULT_VALUE);
this.$store.dispatch('growl/fromError', {
title: this.$store.getters['i18n/t']('generic.notification.title.error'),
err: err.data || err,
}, { root: true });
}
return { valuesContentJson };
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
methods: {
willSave() {
const errors = [];
if (!this.value.spec.enabled) {
return Promise.resolve();
}
if (!this.valuesContentJson.rancher.hostname) {
errors.push(this.t('validation.required', { key: this.t('harvester.addons.rancherK3k.hostname') }, true));
}
if (!this.valuesContentJson.rancher.version) {
errors.push(this.t('validation.required', { key: this.t('harvester.addons.rancherK3k.version') }, true));
}
if (!this.valuesContentJson.rancher.bootstrapPassword) {
errors.push(this.t('validation.required', { key: this.t('harvester.addons.rancherK3k.password') }, true));
}
if (errors.length > 0) {
return Promise.reject(errors);
} else {
return Promise.resolve();
}
},
},
watch: {
valuesContentJson: {
handler(neu) {
this.value.spec['valuesContent'] = jsyaml.dump(neu);
},
deep: true,
immediate: true
},
},
};
</script>
<template>
<div>
<div class="row">
<div class="col span-12">
<RadioGroup
v-model:value="value.spec.enabled"
class="mb-20"
name="model"
:mode="mode"
:options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
/>
</div>
</div>
<template v-if="value.spec.enabled">
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.rancher.hostname"
label-key="harvester.addons.rancherK3k.hostname"
:required="true"
:mode="mode"
placeholder="rancher.$vip.nip.io"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.rancher.version"
label-key="harvester.addons.rancherK3k.version"
:required="true"
:mode="mode"
placeholder="v1.14.0"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.rancher.bootstrapPassword"
label-key="harvester.addons.rancherK3k.password"
:mode="mode"
:required="true"
type="password"
:tooltip="t('harvester.addons.rancherK3k.passwordTooltip')"
/>
</div>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
:deep() .radio-group {
display: flex;
.radio-container {
margin-right: 30px;
}
}
</style>
@@ -7,6 +7,7 @@ import NameNsDescription from '@shell/components/form/NameNsDescription';
import YamlEditor from '@shell/components/YamlEditor';
import CreateEditView from '@shell/mixins/create-edit-view';
import FormValidation from '@shell/mixins/form-validation';
import { HCI } from '@pkg/harvester/config/labels-annotations';
export default {
@@ -23,14 +24,19 @@ export default {
NameNsDescription,
},
mixins: [CreateEditView],
mixins: [CreateEditView, FormValidation],
inheritAttrs: false,
data() {
return {
config: this.value.data?.cloudInit || '',
type: this.value?.metadata?.labels?.[HCI.CLOUD_INIT] || 'user',
config: this.value.data?.cloudInit || '',
type: this.value?.metadata?.labels?.[HCI.CLOUD_INIT] || 'user',
fvFormRuleSets: [{
path: 'metadata.name',
rules: ['required'],
translationKey: 'nameNsDescription.name.label',
}],
};
},
@@ -78,6 +84,7 @@ export default {
:resource="value"
:errors="errors"
:apply-hooks="applyHooks"
:validation-passed="fvFormIsValid"
@finish="save"
@cancel="done"
@error="e=>errors=e"
@@ -86,6 +93,7 @@ export default {
:value="value"
:mode="mode"
:namespaced="true"
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
@update:value="$emit('update:value', $event)"
/>
@@ -7,8 +7,12 @@ import { allHash } from '@shell/utils/promise';
import { clone } from '@shell/utils/object';
import { HCI } from '../../../types';
import { NODE } from '@shell/config/types';
import { _CREATE, _CLONE } from '@shell/config/query-params';
import { LVM_TOPOLOGY_LABEL } from '../index.vue';
const STRIPED_TYPE = 'striped';
const DM_THIN_TYPE = 'dm-thin';
const DEFAULT_PARAMETERS = [
'type',
'vgName'
@@ -54,8 +58,17 @@ export default {
data() {
const node = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).find((t) => t.key === LVM_TOPOLOGY_LABEL)?.values[0];
// striped is dropped once the LVM add-on reaches GA (v1.9.1); dm-thin is the only supported type.
const dmThinOnly = this.$store.getters['harvester-common/getFeatureEnabled']('lvmDmThinOnly');
const volumeGroupTypes = dmThinOnly ? [DM_THIN_TYPE] : [STRIPED_TYPE, DM_THIN_TYPE];
// Default new storage classes to dm-thin, and reset a cloned striped type that is no longer selectable.
if (dmThinOnly && [_CREATE, _CLONE].includes(this.realMode) && !volumeGroupTypes.includes(this.value.parameters.type)) {
this.value.parameters.type = DM_THIN_TYPE;
}
return {
volumeGroupTypes: ['striped', 'dm-thin'],
volumeGroupTypes,
node,
};
},
@@ -246,4 +246,27 @@ export default {
.group-actions {
display: inline;
}
// Match the enable/disable passthrough bulk-action buttons height to the .btn (40px) on the right.
:deep(.bulk .rc-button.btn-medium.bulk-action:not(.btn-sm)) {
min-height: 40px;
}
// Make the collapsed "Actions" dropdown button 40px too.
:deep(.bulk .rc-button.btn-medium.bulk-actions-dropdown:not(.btn-sm)) {
min-height: 40px;
}
// Lay the bulk row out with flex so the "N selected" label can wrap below the buttons.
:deep(.bulk) {
display: flex;
flex-wrap: wrap;
align-items: center;
}
:deep(.bulk .action-availability) {
flex-basis: 100%;
margin-left: 0;
margin-top: 6px;
}
</style>
@@ -5,6 +5,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import Banner from '@components/Banner/Banner.vue';
import remove from 'lodash/remove';
import { set } from '@shell/utils/object';
import { uniq } from '@shell/utils/array';
import { HCI } from '../../../types';
import DeviceList from './DeviceList';
import CompatibilityMatrix from '../CompatibilityMatrix';
@@ -60,7 +61,15 @@ export default {
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
});
vmDevices.forEach(({ name }) => {
// When off, read from spec; otherwise the spec name is a placeholder ('provisioned'),
// so read the real allocated device names from the deviceAllocationDetails annotation.
const hostDeviceNames = this.vm.isOff ? [
...vmDevices.map(({ name }) => name),
] : [
...Object.values(this.vm?.provisionedHostDevices || {}).reduce((acc, devices) => [...acc, ...devices], []),
];
uniq(hostDeviceNames).forEach((name) => {
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
selectedDevices.push(name);
}
+25 -4
View File
@@ -84,6 +84,8 @@ dialog:
harvester:
branding:
primeLabel: '{vendor}'
primeEmptyLabel: ''
logos:
tip: 'Upload a logo to replace the Harvester logo in the top-level navigation header. Image height should be 21 pixels with a max width of 200 pixels. Max file size is 20KB. Accepted formats: JPEG, PNG, SVG.'
favicon:
@@ -835,13 +837,13 @@ harvester:
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>"
sysprep:
title: Windows Sysprep Configuration
title: Windows Unattended & Sysprep Configuration
description: "Configure Windows automated installation using autounattend.xml. The configuration will be stored in a Kubernetes Secret and mounted as a CD-ROM during installation. <a href='https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup' target='_blank'>Learn more</a>"
createNew: Create new...
createNewTitle: Create Windows Sysprep Template
createNewTitle: Create Windows Unattended & Sysprep Template
xmlContent: autounattend.xml content
secret:
label: Windows Sysprep Template
label: Windows Unattended & Sysprep Template
validation:
invalidXml: Invalid XML syntax
invalidNamespace: Invalid unattend.xml namespace. Expected 'urn:schemas-microsoft-com:unattend'
@@ -1832,6 +1834,8 @@ harvester:
'rancher-logging': rancher-logging is an add-on to collect versatile logs, events, and audits from the Harvester cluster and route them to many kinds of servers based on flows.
'rancher-monitoring': rancher-monitoring is an add-on to collect Harvester cluster and virtual machine metrics, view them on the embedded dashboard, and send alert(s) to remote servers.
'rancher-vcluster': rancher-vcluster deploys a virtual cluster (vcluster) with Rancher installed.
'k3k': k3k is an add-on to deploy the k3k controller.
'rancher-k3k': rancher-k3k deploys a k3k virtual cluster with Rancher installed.
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices.
@@ -2136,6 +2140,14 @@ harvester:
hostname: Hostname
rancherVersion: Rancher Version
password: Bootstrap Password
rancherK3k:
accessRancher: Access the Rancher Dashboard
hostname: Hostname
version: Rancher Version
password: Bootstrap Password
passwordTooltip: The initial password in Rancher for the first time login.
enable:
title: Enable K3k
switchFailed: "{action} {name} failed!"
nvidiaDriverToolkit:
titles:
@@ -2417,7 +2429,15 @@ typeDescription:
harvester: "Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos and color scheme."
advancedSettings:
experimental: 'Experimental features allow users to test and evaluate early-access functionality prior to official supported releases'
tags:
modified:
label: Modified
experimental:
label: Experimental
description: 'Experimental: The feature is incomplete but its essential functionality is operational. The feature is not recommended for use in production environments.'
preview:
label: Technical Preview
description: 'Technical Preview: The feature is almost complete and its functionality is not expected to change significantly. Explore the feature extensively before using in production environments.'
descriptions:
'harv-vlan': Default Network Interface name of the VLAN network.
'harv-backup-target': Custom backup target to store virtual machine backups.
@@ -2427,6 +2447,7 @@ advancedSettings:
'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.
'harv-server-flavor': "Indicates the Harvester edition: Prime or Community."
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server.
+1 -1
View File
@@ -72,7 +72,7 @@ export default {
/>
<a
v-if="scope.row.metadata.name === 'rancher-vcluster' && scope.row.spec.enabled"
v-if="['rancher-vcluster', 'rancher-k3k'].includes(scope.row.metadata.name) && scope.row.spec.enabled"
v-tooltip="t('harvester.addons.rancherVcluster.accessRancher')"
class="ml-5"
rel="nofollow noopener noreferrer"
+12
View File
@@ -38,6 +38,10 @@ export const SSH_EXISTING_TYPE = {
export default {
methods: {
hasCloudConfigComment(userScript) {
if (typeof userScript === 'string' && /^\s*#cloud-config(\s|$)/.test(userScript)) {
return true;
}
// Check that userData contains: #cloud-config
const userDataDoc = userScript ? YAML.parseDocument(userScript) : YAML.parseDocument({});
const items = userDataDoc?.contents?.items || [];
@@ -52,6 +56,14 @@ export default {
exist = true;
}
if (userDataDoc?.contents?.commentBefore === 'cloud-config' || userDataDoc?.contents?.commentBefore?.includes('cloud-config\n')) {
exist = true;
}
if (userDataDoc?.contents?.comment === 'cloud-config' || userDataDoc?.contents?.comment?.includes('cloud-config\n')) {
exist = true;
}
items.map((item) => {
const key = item.key;
+5 -3
View File
@@ -1211,9 +1211,10 @@ export default {
userDataDoc = config.installAgent ? this.mergeQGA({ userDataDoc, ...config }) : this.deleteQGA({ userDataDoc, ...config });
const userDataYaml = userDataDoc.toString();
const userDataValue = userDataDoc.toJSON();
if (userDataYaml === '{}\n') {
// When the YAML parsed value is '{}\n', it means that the userData is empty, then undefined is returned.
if (userDataValue === null || (typeof userDataValue === 'object' && !Array.isArray(userDataValue) && isEmpty(userDataValue))) {
// Empty userData should not create cloud-init content like `null`.
return undefined;
}
@@ -1316,8 +1317,9 @@ export default {
case SOURCE_TYPE.IMAGE: {
const image = this.images.find( (I) => R.image === I.id);
out.spec.storageClassName = R.realName ? R.storageClassName : (R.storageClassName || image?.storageClassName);
if (image) {
out.spec.storageClassName = image.storageClassName;
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: image.id };
} else {
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: '' };
@@ -5,6 +5,7 @@ import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
import { get } from '@shell/utils/object';
import { NAMESPACE } from '@shell/config/types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
import { FORKLIFT_NAMESPACE } from '@pkg/harvester/config/harvester-map';
import { HCI } from '../../types';
const OBSCURE_NAMESPACE_PREFIX = [
@@ -93,6 +94,10 @@ export default class HciNamespace extends namespace {
}
get isSystem() {
if (this.metadata?.name === FORKLIFT_NAMESPACE) {
return false;
}
const systemNamespaces = ['fleet-default'];
if (systemNamespaces.includes(this.metadata.name)) {
+18 -6
View File
@@ -5,12 +5,14 @@ import HarvesterResource from './harvester';
import { HCI } from '../types';
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
const RANCHER_VCLUSTER = 'harvester-system/rancher-vcluster';
const RANCHER_K3K_ADDON = 'rancher-k3k/rancher-k3k';
export default class HciAddonConfig extends HarvesterResource {
get availableActions() {
const out = super._availableActions;
if (this.id === 'harvester-system/rancher-vcluster') {
if (this.id === RANCHER_VCLUSTER || this.id === RANCHER_K3K_ADDON) {
const rancherDashboard = {
action: 'goToRancher',
enabled: this.spec.enabled,
@@ -57,6 +59,15 @@ export default class HciAddonConfig extends HarvesterResource {
return;
}
if (!this.spec.enabled && this.id === RANCHER_K3K_ADDON) {
this.$dispatch('promptModal', {
resources: [this],
component: 'HarvesterEnableK3k',
});
return;
}
this.spec.enabled = !this.spec.enabled;
await this.save();
} catch (err) {
@@ -69,18 +80,19 @@ export default class HciAddonConfig extends HarvesterResource {
}
goToRancher() {
const valuesContent = jsyaml.load(this.spec.valuesContent);
window.open(
`https://${ valuesContent.hostname }`,
this.rancherHostname,
'_blank',
);
}
get rancherHostname() {
const valuesContent = jsyaml.load(this.spec.valuesContent);
const valuesContent = jsyaml.load(this.spec.valuesContent) || {};
return `https://${ valuesContent.hostname }`;
// rancher-k3k nests the hostname under `rancher`, rancher-vcluster keeps it at the top level
const hostname = this.id === RANCHER_K3K_ADDON ? valuesContent.rancher?.hostname : valuesContent.hostname;
return `https://${ hostname }`;
}
get stateColor() {
@@ -1315,6 +1315,18 @@ export default class VirtVm extends HarvesterResource {
}
}
get provisionedHostDevices() {
// Rancher-provisioned VMs store hostDevices.name as a placeholder ('provisioned'); the
// real allocated PCI device names live in this annotation, keyed by resourceName.
try {
const deviceAllocationDetails = JSON.parse(this.metadata?.annotations[HCI_ANNOTATIONS.VM_DEVICE_ALLOCATION_DETAILS] || '{}');
return deviceAllocationDetails?.hostdevices || {};
} catch (error) {
return {};
}
}
get schedulingVMBackupFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
}
@@ -0,0 +1,14 @@
import ManagementSetting from '@shell/models/management.cattle.io.setting';
import { HCI_SETTING } from '../../config/settings';
export default class HarvesterManagementSetting extends ManagementSetting {
get _availableActions() {
const actions = super._availableActions;
if (this.$rootGetters['isStandaloneHarvester'] && this.id === HCI_SETTING.UI_PL) {
return actions.filter((action) => !['goToClone', 'promptRemove'].includes(action.action));
}
return actions;
}
}
+14 -2
View File
@@ -63,6 +63,7 @@ export default {
return {
vendor: getVendor(),
uiPLSetting: {},
privateLabelDraft: null,
uiLogoDarkSetting: {},
uiLogoDark: '',
uiLogoLightSetting: {},
@@ -81,6 +82,17 @@ export default {
};
},
computed: {
isHarvesterPrime() {
return !!this.$store.getters['harvester-common/isHarvesterPrime'];
},
privateLabel: {
get() {
return this.privateLabelDraft ?? (this.isHarvesterPrime ? this.$store.getters['harvester-common/privateLabel'] : this.uiPLSetting.value);
},
set(value) {
this.privateLabelDraft = value;
}
},
mode() {
const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
@@ -112,7 +124,7 @@ export default {
this.errors.push(e);
},
async save(btnCB) {
this.uiPLSetting.value = this.uiPLSetting.value.replaceAll(/[\<>&=#()"]/gm, '');
this.uiPLSetting.value = (this.privateLabel || '').replaceAll(/[\<>&=#()"]/gm, '');
if (this.customizeLogo) {
this.uiLogoLightSetting.value = this.uiLogoLight;
this.uiLogoDarkSetting.value = this.uiLogoDark;
@@ -169,7 +181,7 @@ export default {
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="uiPLSetting.value"
v-model:value="privateLabel"
:label="t('branding.uiPL.label')"
:mode="mode"
:maxlength="100"
@@ -68,29 +68,35 @@ export default defineComponent({
return (this.value || []).map((plan) => {
const planName = plan?.metadata?.name || '-';
const namespace = plan?.metadata?.namespace || '';
const networkMapRef = this.getPlanMapRef(plan, 'network');
const storageMapRef = this.getPlanMapRef(plan, 'storage');
const migrations = existing[HCI.FORKLIFT_MIGRATION]
.filter((resource) => this.ownedByPlan(resource, plan))
.map((resource) => ({
name: resource?.metadata?.name,
namespace: resource?.metadata?.namespace || namespace,
}));
.map((resource) => resource?.metadata?.name)
.filter(Boolean);
const matchedNetworkMap = existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => this.ownedByPlan(resource, plan));
const matchedStorageMap = existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => this.ownedByPlan(resource, plan));
const matchedNetworkMap = networkMapRef ? existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => resource?.metadata?.name === networkMapRef.name && resource?.metadata?.namespace === networkMapRef.namespace) : null;
const matchedStorageMap = storageMapRef ? existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => resource?.metadata?.name === storageMapRef.name && resource?.metadata?.namespace === storageMapRef.namespace) : null;
return {
planName,
namespace,
migrations,
networkMap: matchedNetworkMap ? {
namespace: matchedNetworkMap?.metadata?.namespace || namespace,
namespace: matchedNetworkMap?.metadata?.namespace || networkMapRef?.namespace || namespace,
name: matchedNetworkMap?.metadata?.name,
} : null,
} : (networkMapRef ? {
namespace: networkMapRef.namespace,
name: networkMapRef.name,
} : null),
storageMap: matchedStorageMap ? {
namespace: matchedStorageMap?.metadata?.namespace || namespace,
namespace: matchedStorageMap?.metadata?.namespace || storageMapRef?.namespace || namespace,
name: matchedStorageMap?.metadata?.name,
} : null,
} : (storageMapRef ? {
namespace: storageMapRef.namespace,
name: storageMapRef.name,
} : null),
};
});
},
@@ -113,6 +119,20 @@ export default defineComponent({
methods: {
resourceNames,
getPlanMapRef(plan, mapType) {
const ref = plan?.spec?.map?.[mapType];
const name = ref?.name;
if (!name) {
return null;
}
return {
name,
namespace: ref?.namespace || plan?.metadata?.namespace || '',
};
},
/**
* Determine whether a related resource (migration / network map / storage map)
* is owned by the given plan by inspecting its `metadata.ownerReferences`.
@@ -137,9 +157,8 @@ export default defineComponent({
const existing = this.existingRelatedResources;
const targets = new Map();
const addTarget = (type, resource) => {
const name = resource?.metadata?.name;
const ns = resource?.metadata?.namespace;
const addTarget = (type, name, namespace) => {
const ns = namespace || '';
if (!name) {
return;
@@ -153,17 +172,20 @@ export default defineComponent({
};
for (const plan of this.value || []) {
existing[HCI.FORKLIFT_NETWORK_MAP]
.filter((resource) => this.ownedByPlan(resource, plan))
.forEach((resource) => addTarget(HCI.FORKLIFT_NETWORK_MAP, resource));
const networkMapRef = this.getPlanMapRef(plan, 'network');
const storageMapRef = this.getPlanMapRef(plan, 'storage');
existing[HCI.FORKLIFT_STORAGE_MAP]
.filter((resource) => this.ownedByPlan(resource, plan))
.forEach((resource) => addTarget(HCI.FORKLIFT_STORAGE_MAP, resource));
if (networkMapRef) {
addTarget(HCI.FORKLIFT_NETWORK_MAP, networkMapRef.name, networkMapRef.namespace);
}
if (storageMapRef) {
addTarget(HCI.FORKLIFT_STORAGE_MAP, storageMapRef.name, storageMapRef.namespace);
}
existing[HCI.FORKLIFT_MIGRATION]
.filter((resource) => this.ownedByPlan(resource, plan))
.forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource));
.forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource?.metadata?.name, resource?.metadata?.namespace));
}
return [...targets.values()];
@@ -256,11 +278,11 @@ export default defineComponent({
<div>• {{ t('harvester.addons.vmMigration.labels.migration') }}:</div>
<template v-if="summary.migrations.length">
<div
v-for="migration in summary.migrations"
:key="`${ migration.namespace }/${ migration.name }`"
v-for="migrationName in summary.migrations"
:key="migrationName"
>
<div class="ml-20">
- {{ migration.name }}
- {{ migrationName }}
</div>
</div>
</template>
+19
View File
@@ -1,4 +1,6 @@
import Parse from 'url-parse';
import { MANAGEMENT } from '@shell/config/types';
import { SETTING } from '@shell/config/settings';
import { HCI } from '../types';
import { PRODUCT_NAME } from '../config/harvester';
import { featureEnabled, getVersion } from '../utils/feature-flags';
@@ -64,6 +66,23 @@ const mutations = {
};
const getters = {
isHarvesterPrime: (_state, _getters, _rootState, rootGetters) => {
const setting = rootGetters['harvester/byId']?.(HCI.SETTING, 'server-flavor');
return (setting?.value || setting?.default) === 'prime';
},
privateLabel: (_state, getters, _rootState, rootGetters) => {
const setting = rootGetters['management/byId']?.(MANAGEMENT.SETTING, SETTING.PL);
const value = setting?.value || '';
if (getters.isHarvesterPrime && value === (setting?.default || 'Harvester')) {
return '';
}
return value;
},
getBundleId(state) {
return state.latestBundleId;
},
+7 -2
View File
@@ -1,3 +1,5 @@
import { FAVORITE_TYPES } from '@shell/store/prefs';
/**
* Dynamically toggles SideNav entries based on the enabled status of a specific Addon.
*
@@ -26,10 +28,13 @@ export function registerAddonSideNav(store, productName, {
// Toggle the trigger a few times so an early kick (fired before the SideNav
// has mounted on first login) is retried once the component is listening.
// Use the prefs/load mutation (in-memory only)
[0, 600, 1500].forEach((delay) => {
setTimeout(() => {
store.dispatch('type-map/addFavorite', TRIGGER);
setTimeout(() => store.dispatch('type-map/removeFavorite', TRIGGER), 300);
const base = (store.getters['prefs/get'](FAVORITE_TYPES) || []).filter((t) => t !== TRIGGER);
store.commit('prefs/load', { key: FAVORITE_TYPES, value: [...base, TRIGGER] });
setTimeout(() => store.commit('prefs/load', { key: FAVORITE_TYPES, value: [...base] }), 300);
}, delay);
});
};
+1550 -1461
View File
File diff suppressed because it is too large Load Diff