harvester-ui-extension PR 1040 (Copilot, driven by a110605) at head 535a606, reviewed 2026-11-05. It blocks saving a VM or a template version when User Data or Network Data doesn't parse as YAML. Fixes harvester 10980. Nothing is posted.
Preview: harvesterpr1040r.mf-created-dashboard.ui.rancher.space. Built from adc1ea4, which is head 535a606 merged onto main 12c28c0 in a local clone, backend Harvester .118 (v1.9.0-rc4). PR + fix: a0ed496, local only, not pushed. Baseline: the shared harvester-main, rebuilt today at 12c28c0.
#cloud-config, and require a mapping. I built it and every row below comes out right.admin with HARVESTER_PASSWORD from .env, and open PR 1040: Create VM.#!/bin/bash apt-get update # start it systemctl enable --now nginx
No VM on .118 holds a script, so I reproduced the edit lock by rewriting the secret's GET response in the browser. ttt itself wasn't changed.
Both of main's outcomes matter here. In all 8 script and format cases I ran, what main sends intact is what fails to parse as YAML, because getUserData falls into its catch and returns the text as typed. So this PR blocks exactly the non-YAML content that works today, and lets through the scripts main already changes (FYI 1).
ttt holding a script, Save clicked with nothing changed. Blocked, no request sent. (Form cropped to the banner and the editor.)#ps1_sysnative user data. Blocked. (Form cropped.)User data isn't always YAML. Cloud-init picks the format from the first line, and #! scripts, Content-Type: multipart/mixed, ## template: jinja and Cloudbase-Init's #ps1_sysnative are all valid. The tip above this editor also says it can run a script. main sends these as typed, and here they're blocked with "Invalid User Data YAML syntax".
It reaches existing VMs too. A VM with a script like this can't be saved anymore, even when nothing in its user data was touched.
Can we parse only when the first line is #cloud-config and let everything else through as it does today? hasCloudConfigComment looks close, but it also matches a #cloud-config inside a MIME part, so checking the first line is safer.
Attach the two screenshots above to this comment. Docs: cloud-init user-data formats, Cloudbase-Init userdata. KubeVirt's startup scripts page also shows a #!/bin/bash script as cloudInitNoCloud user data.
The issue says "input arbitrary words". The check only catches words that break the surrounding structure, like the issue video, where they follow the default template's mapping. Network Data starts empty, so words there are a valid YAML string and go through as typed.
jsyaml.load doesn't throw on plain words, because they're a valid YAML string. So words in Network Data, which starts empty, still save, and that's the issue's case. The same goes for user data with #cloud-config and only words. Checking that the result is a mapping (empty is fine) covers both.
This is the same method as in the VM page. Both pages already use VM_MIXIN, which imports js-yaml too. Could it live there? The changes above then go in one place, and it's easy to add a unit test. This path has none yet.
validateCloudInit() {
const isMapping = (text) => {
try {
const doc = jsyaml.load(text);
return doc === undefined || doc === null || (typeof doc === 'object' && !Array.isArray(doc));
} catch (e) {
return false;
}
};
if (/^\s*#cloud-config(\s|$)/.test(this.userScript || '') && !isMapping(this.userScript)) {
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.user.invalidYaml'));
}
if (this.networkScript && !isMapping(this.networkScript)) {
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.network.invalidYaml'));
}
},This goes in the main comment because it has no line to anchor to. It matters because QA works from these steps.
535a606.{"password":"test - invalid indentation"}, so it saves. The author's screenshot used a different input, the default template plus words, and that does block.| What was typed | main | PR | PR + fix |
|---|---|---|---|
| Should block | |||
| Default template plus a word (issue video) | Sent as typed | Blocked | Blocked |
#cloud-config plus only words | Sent as typed | Sent as typed | Blocked |
| Network Data: words (the box starts empty) | Sent as typed | Sent as typed | Blocked |
| Network Data: bad indentation | Sent as typed | Blocked | Blocked |
| Windows: bad cloud-config (votdev's ask) | Sent as typed | Blocked | Blocked |
| Should save | |||
| Valid cloud-config | Sent as typed | Sent as typed | Sent as typed |
| Network Data: valid v1 | Sent as typed | Sent as typed | Sent as typed |
| Windows: valid cloud-config | Sent as typed | Sent as typed | Sent as typed |
#!/bin/bash, comment between commands | Sent as typed | Blocked | Sent as typed |
#!/bin/bash with a heredoc | Sent as typed | Blocked | Sent as typed |
| MIME multipart | Sent as typed | Blocked | Sent as typed |
## template: jinja with an if block | Sent as typed | Blocked | Sent as typed |
Windows: #ps1_sysnative | Sent as typed | Blocked | Sent as typed |
| Not the ask here (see FYI) | |||
| PR step 3 snippet. It is valid YAML, so no check can block it | Sent, changed | Sent, changed | Sent, changed |
| Only words, no header. Cloud-init can't tell what it is | Sent, changed | Sent, changed | Sent, changed |
#!/bin/bash, comment before commands | Sent, changed | Sent, changed | Sent, changed |
#!/bin/bash with a colon in a string | Sent, changed | Sent, changed | Sent, changed |
Windows: rem cmd | Sent, changed | Sent, changed | Sent, changed |
Green is the right result, red the wrong one, grey not judged. "Sent, changed" means the form altered the text before sending it.
| Path | main | PR | PR + fix |
|---|---|---|---|
| Existing VM with a script in its secret, Save with nothing changed | Sent | Blocked | Sent |
| Template version: default template plus a word | Sent | Blocked | Blocked |
| Template version: a script | Sent | Blocked | Sent |
| Template version: valid cloud-config | Sent | Sent | Sent |
| Template version: blocked, fixed, saved again | No block to retry | Saved, payload same as a direct save | Saved, payload same as a direct save |
| VM: blocked, fixed, saved again | No block to retry | Saved | Saved |
| VM: bad user data, then Edit as YAML and Create | Sent | Sent | Sent |
When a script happens to parse as YAML, for example one with no comment between its commands, the form adds #cloud-config and can fold its lines. Cloud-init then won't run it as a script. Example sent by main, the PR and the fix alike:
#cloud-config #!/bin/bash # install nginx apt-get update systemctl enable --now nginx
Not this PR's bug, and the fix leaves it alone. It's worth its own issue, and I can file one if you want.
With bad user data, Edit as YAML then Create sends the VM on all three builds. The other form validators don't run there either, so I wouldn't ask for it in this PR. The YAML only shows a secretRef, and no secret write showed up in my stubbed run. I didn't work out whether that's the stub or the app.
From reading the code, that page and the "create new" modal save the text without parsing it. Bad text can still become a template, and it only blocks later at VM save. The issue is about VMs, so this is out of scope.
runcmd twice is blocked on the PR and the fix, while cloud-init's PyYAML accepts it and the last one wins. It's probably a mistake anyway, so I'd leave it.#cloud-config if it parses, as main does.I tested this merged onto current main, on a real v1.9 cluster. The case from the issue is blocked now, and the form saves again once the YAML is fixed, on both the VM and template forms. The check is too broad for user data and misses plain words though, see the comments.
Could you also update the description? It still says Windows is skipped, which 535a606 removed. The snippet in step 3 is valid YAML, so it doesn't block and saves password: test - invalid indentation. Adding a word after the default template, like in the issue video, does block.
Plus the three inline comments above. No preview link and no code blocks, as usual.
zz-pr1040 VMs, templates, versions or secrets, and ttt still running with its own cloud-config.saveVM, read but not run. The PR head itself in the matrix: its CodeMirror 5 form broke my harness, and I threw those results out. The method is identical and the author's screenshot shows the head blocking.