Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot]anda110605 535a6067c9 fix: validate Windows CloudInit YAML on save
Co-authored-by: a110605 <5744158+a110605@users.noreply.github.com>
2026-08-05 15:56:22 +08:00
copilot-swe-agent[bot] ace28d1c0e fix(vm): add isWindows guard to validateCloudInit in template version 2026-08-05 15:56:22 +08:00
copilot-swe-agent[bot] 1df5af32b8 fix(vm): block VM creation with invalid CloudInit YAML content 2026-08-05 15:56:22 +08:00
Andy Lee fbd4a1108f ci: add test steps in PR template (#1093)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 15:53:40 +08:00
Andy Lee 823a88c85b ci: not use actions/github-script@v7 (#1092)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 14:37:24 +08:00
Andy Lee 3a2c6e3a3d ci: support manual standalone build trigger and PR comment (#1091)
* ci: add manually trigger build

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

* refactor: PR comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 14:21:41 +08:00
Andy Lee be6e9b5f06 fix(subnet): support None option for VLAN dropdown (#1085)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 12:23:57 +08:00
Andy Lee a88bed9eb4 fix: clear keyword should clear highlight (#1088)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 00:24:34 +08:00
Andy Lee f97f14c811 deps: bump @rancher/shell to 3.0.12-rc.7 (#1084)
* deps: update rancher/shell to 3.0.12-rc.7

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

* fix: move filter labels/schedule buttons position

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

* feat: improve serial console

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

* feat(serial-console): add terminal search toolbar and highlight tuning

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

* refactor: clear searach result after close

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

* refactor: fine tune serial console

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-04 19:53:24 +08:00
Andy Lee f8a29a1c54 fix: project resource quota not set in POST API payload (#1082)
* fix: project resource quota not set in POST API payload

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

* fix: guard onQuotasInput against undefined payload

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-04 13:27:30 +08:00
8 changed files with 123 additions and 20 deletions
+7 -3
View File
@@ -8,12 +8,16 @@
- [ ] Yes, the backend owner is @
- [ ] No, frontend-only.
### Related Issue #
### Related Issue
<!-- Link the issue as harvester/harvester#<issue_number> -->
harvester/harvester#<issue_number>
### Test screenshot or video (Required)
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
### Test Steps
<!-- Provide the test steps for reviewer to verify the issue -->
1. Go to ... page
### Test screenshot or video
<!-- Attach screenshot or video of the changes and eventual comparison -->
@@ -1,12 +1,12 @@
name: Build Standalone on PR Merge
on:
workflow_dispatch: # manually trigger on Github Actions only support PR branch pushed to harvester/harvester-ui-extension
push:
branches:
- main
- 'release-harvester-v*'
- '*-dev'
- bump_shell_rc7_demo
pull_request:
branches:
- main
@@ -30,3 +30,35 @@ jobs:
id-token: write
with:
CI_BRANCH: ${{github.ref_name}}
comment-on-pr:
name: Comment build result on PR
# Only for manual runs so the existing push / pull_request flows are untouched.
if: github.event_name == 'workflow_dispatch'
needs:
- build
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Add build artifact comment to the PR opened from this branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
# Match scripts/version: sanitize the branch and map main -> latest.
if [ "$BRANCH" = "main" ]; then
DIR="latest"
else
DIR=$(printf '%s' "$BRANCH" | sed -E 's/[^a-zA-Z0-9.-]+/-/g')
fi
URL="https://releases.rancher.com/harvester-ui/dashboard/${DIR}/index.html"
PR=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --base main --head "$BRANCH" --json number --jq '.[0].number // empty')
if [ -z "$PR" ]; then
echo "No open PR found for branch $BRANCH, skipping comment."
exit 0
fi
BODY=$(printf '🚀 **UI Artifact Build Status:** `SUCCESS` ✅\n> ui-index: %s\n> ui-source: external' "$URL")
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "$BODY"
@@ -213,9 +213,15 @@ export default {
},
clearSearchHighlights() {
this.terminal?.clearSelection();
this.searchAddon?.clearActiveDecoration();
this.searchAddon?.clearDecorations();
this.terminal?.clearSelection();
// Disposing the search decorations doesn't repaint the webgl/canvas
// layer, so force a refresh to remove the lingering highlight.
if (this.terminal) {
this.terminal.refresh(0, this.terminal.rows - 1);
}
},
findNext() {
@@ -1,4 +1,5 @@
<script>
import jsyaml from 'js-yaml';
import { mapGetters } from 'vuex';
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
@@ -177,8 +178,17 @@ export default {
},
async saveVMT(buttonCb) {
this.errors = [];
this.parseVM();
this.validateCloudInit();
if (this.errors.length) {
buttonCb(false);
return;
}
const templates = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_TEMPLATE });
const template = templates.find( (O) => O.metadata.name === this.templateValue.metadata.name);
@@ -237,6 +247,24 @@ export default {
this.$refs.yamlEditor?.refresh();
}
},
validateCloudInit() {
if (this.userScript) {
try {
jsyaml.load(this.userScript);
} catch (e) {
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.user.invalidYaml'));
}
}
if (this.networkScript) {
try {
jsyaml.load(this.networkScript);
} catch (e) {
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.network.invalidYaml'));
}
}
},
},
};
</script>
+14 -3
View File
@@ -9,7 +9,7 @@ import Loading from '@shell/components/Loading';
import CreateEditView from '@shell/mixins/create-edit-view';
import { RadioGroup } from '@components/Form/Radio';
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
import { set } from '@shell/utils/object';
import { set, remove } from '@shell/utils/object';
import ArrayList from '@shell/components/form/ArrayList';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types';
@@ -142,10 +142,12 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore;
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
return vlans.map((vlan) => ({
const options = vlans.map((vlan) => ({
label: vlan.id,
value: vlan.id,
}));
return [{ label: this.t('generic.none'), value: '' }, ...options];
}
},
@@ -160,6 +162,14 @@ export default {
}
},
methods: {
onVlanChange(value) {
if (value === '') {
remove(this.value.spec, 'vlan');
} else {
set(this.value, 'spec.vlan', value);
}
},
async saveSubnet(buttonCb) {
const errors = [];
const name = this.value?.metadata?.name;
@@ -283,12 +293,13 @@ export default {
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.vlan"
:value="value.spec.vlan ?? ''"
class="mb-20"
:options="vlanOptions"
:placeholder="t('harvester.subnet.vlan.placeholder')"
:label="t('harvester.subnet.vlan.label')"
:mode="mode"
@update:value="onVlanChange"
/>
</div>
</div>
@@ -1,4 +1,5 @@
<script>
import jsyaml from 'js-yaml';
import { isEqual } from 'lodash';
import { mapGetters } from 'vuex';
import Tabbed from '@shell/components/Tabbed';
@@ -379,6 +380,7 @@ export default {
this.validateCPUMemory();
this.validateWindowsSysprep();
this.validateCloudInit();
// block create VM flow if has validation errors
if (this.errors.length) {
@@ -424,6 +426,24 @@ export default {
}
},
validateCloudInit() {
if (this.userScript) {
try {
jsyaml.load(this.userScript);
} catch (e) {
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.user.invalidYaml'));
}
}
if (this.networkScript) {
try {
jsyaml.load(this.networkScript);
} catch (e) {
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.network.invalidYaml'));
}
}
},
async saveSingle(buttonCb) {
this.parseVM();
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
@@ -63,6 +63,7 @@ export default {
RANCHER_TYPES,
fvFormRuleSets: [{ path: 'spec.displayName', rules: ['required'] }],
canEditPSPBindings: true,
isQuotasValid: true,
};
},
computed: {
@@ -190,15 +191,13 @@ export default {
this['membershipUpdate'] = update;
},
removeQuota(key) {
['resourceQuota', 'namespaceDefaultResourceQuota'].forEach((specProp) => {
if (this.value?.spec[specProp]?.limit && this.value?.spec[specProp]?.limit[key]) {
delete this.value?.spec[specProp]?.limit[key];
}
if (this.value?.spec[specProp]?.usedLimit && this.value?.spec[specProp]?.usedLimit[key]) {
delete this.value?.spec[specProp]?.usedLimit[key];
}
});
validateQuotas(isValid) {
this.isQuotasValid = isValid;
},
onQuotasInput({ projectLimit, nsLimit } = {}) {
this.value.spec.resourceQuota = { ...this.value.spec.resourceQuota, limit: projectLimit };
this.value.spec.namespaceDefaultResourceQuota = { ...this.value.spec.namespaceDefaultResourceQuota, limit: nsLimit };
}
},
};
@@ -212,7 +211,7 @@ export default {
:resource="value"
:subtypes="[]"
:can-yaml="false"
:validation-passed="fvFormIsValid"
:validation-passed="fvFormIsValid && isQuotasValid"
@error="e=>errors=e"
@finish="save"
@cancel="done"
@@ -267,10 +266,11 @@ export default {
:weight="9"
>
<ResourceQuota
v-model:value="value"
:value="value"
:mode="canEditTabElements"
:types="isStandaloneHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
@remove="removeQuota"
@input="onQuotasInput"
@validationChanged="validateQuotas"
/>
</Tab>
<Tab
+2
View File
@@ -830,10 +830,12 @@ harvester:
label: User Data Template
title: "User Data:"
tip: "You can specify user data to configure an instance or run a configuration script during launch. If you launch more than one instance at a time, the user data is available to all the instances in that reservation. <a href='https://cloudinit.readthedocs.io/en/latest/topics/examples.html' target='_blank'>Learn more</a>"
invalidYaml: 'Invalid User Data YAML syntax'
network:
label: Network Data Template
title: "Network Data:"
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
invalidYaml: 'Invalid Network Data YAML syntax'
sysprep:
title: Windows 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>"