feat: embed distro-specific rendered script hotfixes in ANC - #9382
feat: embed distro-specific rendered script hotfixes in ANC#9382Devinwong wants to merge 10 commits into
Conversation
Generate distro-specific nodecustomdata YAML with AgentBaker's canonical renderer and apply it transactionally from the selected ANC package. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Windows Unit Test Results 3 files 13 suites 50s ⏱️ Results for commit 96e31d6. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
🟡 Changes recommended
Critical stale-binary fallback and unsupported-script issues, plus payload completeness and durability concerns, remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Embeds distro-specific rendered Linux script hotfix payloads in ANC and applies them during provisioning.
Changes:
- Adds canonical AgentBaker rendering and hotfix generation.
- Adds transactional ANC payload validation and application.
- Adds unit, integration, E2E, documentation, and workflow coverage.
Required fixes:
- Critical —
aks-node-controller/app.go:691: A failed package update can leave a previously staged hotfix binary selected, reapplying stale payloads instead of falling back to VHD scripts. Remove or validate stale binaries before launcher selection. - Moderate —
aks-node-controller/scripthotfix/applier.go:351: Apply file permissions before syncing, or sync again afterward, so executable modes are durable before commit. - Moderate —
hotfix/hotfix_generate.py:384: Track every requested key and fail generation when any mapped artifact lacks a selectedwrite_filesblock. - Critical —
hotfix/hotfix_generate.py:83:initAKSCustomCloudlacks the canonical variable andwrite_filesblock required to generate its payload. Remove the mapping or add canonical support.
File summaries
| File | Description |
|---|---|
pkg/agent/nodecustomdata_render_test.go |
Tests distro-aware rendering. |
pkg/agent/baker.go |
Exposes canonical rendering. |
hotfix/render-nodecustomdata/main.go |
Renders platform payloads. |
hotfix/hotfix_generate.py |
Generates ANC hotfix assets. |
hotfix/hotfix_generate_test.py |
Tests generation behavior. |
e2e/vmss.go |
Builds fixture-enabled ANC binaries. |
e2e/vmss_test.go |
Tests fixture generation. |
e2e/types.go |
Defines hotfix fixtures. |
e2e/scenario_test.go |
Adds Ubuntu hotfix E2E coverage. |
aks-node-controller/scripthotfix/generated/rendered_nodecustomdata_ubuntu.yml |
Adds the Ubuntu baseline payload. |
aks-node-controller/scripthotfix/generated/rendered_nodecustomdata_mariner.yml |
Adds the Mariner baseline payload. |
aks-node-controller/scripthotfix/generated/rendered_nodecustomdata_flatcar.yml |
Adds the Flatcar baseline payload. |
aks-node-controller/scripthotfix/generated/rendered_nodecustomdata_azlosguard.yml |
Adds the OS Guard baseline payload. |
aks-node-controller/scripthotfix/generated/rendered_nodecustomdata_acl.yml |
Adds the ACL baseline payload. |
aks-node-controller/scripthotfix/generated/active |
Marks the baseline payload inactive. |
aks-node-controller/scripthotfix/applier.go |
Validates and transactionally applies payloads. |
aks-node-controller/scripthotfix/applier_test.go |
Tests validation, application, and rollback. |
aks-node-controller/README.md |
Documents hotfix delivery. |
aks-node-controller/app.go |
Applies embedded payloads before provisioning. |
aks-node-controller/app_test.go |
Tests application and fail-open behavior. |
.github/workflows/hotfix-generate.yml |
Generates and commits hotfix artifacts. |
Review details
- Files reviewed: 15/21 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Share parsing, decoding, and transactional file application with the existing nodecustomdata path while keeping embedded distro selection independent of scripts_version. Preserve active generated payloads and pointers when no new hotfix is requested. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Three unresolved issues affect rollback safety, ACL package delivery, and custom-image protections.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
aks-node-controller/app.go:699
- 🔴 High Risk — 🏗️ Architecture: Not every application error is safely fail-open.
commitStagedNodeCustomDataWithRenamecan return an error containingrollback failedafter earlier destinations were already replaced; continuing here then runs a mixed old/hotfixed script set rather than the VHD-baked fallback. Distinguish an uncertain/partial-commit error and stop provisioning (or synchronously restore the preserved backups); continue only after validation/staging failures or a successful rollback.
if err != nil {
// Hotfixes are fail-open: the VHD-baked scripts remain available, so an
// embedded payload must not block provisioning.
slog.Warn("failed to apply embedded hotfix payload; continuing with existing scripts",
"error", err)
- Files reviewed: 17/23 changed files
- Comments generated: 2
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Hotfix generation is non-cumulative, and failed rollbacks can leave provisioning with inconsistent scripts.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
aks-node-controller/app.go:699
- 🔴 High Risk — 🔄 Backward Compatibility: this fail-open branch also catches the
rollback failederror fromcommitStagedNodeCustomData. In that case at least one destination can still contain the new payload while later entries remain old, so continuing into CSE runs an inconsistent script set rather than the promised VHD-baked fallback. Distinguish incomplete rollback from pre-commit/fully-rolled-back failures and abort provisioning (or finish restoring the preserved backups) for that case.
if err != nil {
// Hotfixes are fail-open: the VHD-baked scripts remain available, so an
// embedded payload must not block provisioning.
slog.Warn("failed to apply embedded hotfix payload; continuing with existing scripts",
"error", err)
aks-node-controller/nodecustomdata.go:305
- 🟡 Medium Risk — the file is synced before its executable mode is applied, so the staged artifact is not fully durable when it is committed. After a crash or abrupt reboot, a hotfixed script can retain
CreateTemp's non-executable mode even though its content was flushed. Apply the mode beforeSyncso both data and metadata are persisted before rename.
if err := temp.Sync(); err != nil {
- Files reviewed: 17/23 changed files
- Comments generated: 1
- Review effort level: Balanced
- errorlint: use errors.Is(err, io.EOF) for wrapped-error safety - govet shadow: rename shadowed err to mkErr in stageNodeCustomDataEntry - gochecknoglobals: annotate generatedNodeCustomData test-injection global - gochecknoglobals/gosec: scope platforms slice to main() and tighten rendered-file WriteFile perms to 0o600 - rename applyEmbeddedHotfixPayload to applyEmbeddedHotfixIfNeeded and document newRenderConfig placeholder fields per review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Critical custom-image compatibility issues can overwrite a script that production rendering intentionally preserves.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 17/23 changed files
- Comments generated: 2
- Review effort level: Balanced
…payloads detect_changed_varkeys previously diffed against the moving base branch tip, so a later hotfix only re-rendered its own script and silently dropped an earlier hotfix's rendered block (non-cumulative regression). Diff against the immutable VHD baseline tag derived from linux_sig_version.json instead, so every generated payload re-renders all scripts changed since the VHD. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
writeNodeCustomDataTempFile synced while the temp file still had CreateTemp's 0600 mode, then chmod'd after close with no re-sync. A crash after the later rename could durably leave the provisioning script non-executable. Chmod before Sync so the final mode is captured by the fsync. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…atcar ACL and Flatcar have no apt/dnf/tdnf, so PMC package-based ANC self-update is intentionally unsupported there; they run VHD-baked ANC only. Give those IDs an explicit, self-documenting error instead of the generic 'unsupported OS'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adding azurecontainerlinux/flatcar to detectPackageManager created a 3rd literal occurrence, tripping the goconst linter. Extract osReleaseIDAzureContainerLinux and osReleaseIDFlatcar constants and use them in both the classifier and the package-manager detector. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The launcher chooses between the VHD-baked binary and the staged
${BIN_PATH}-hotfix binary before running provision. That branch decides
whether the embedded script payload is ever applied, but no existing test
created ${BIN_PATH}-hotfix, so the selection branch had no coverage.
Add three cases:
- staged hotfix binary present and executable runs provision
- non-executable staged path falls back to the VHD-baked binary
- the full seam: the baked binary stages the replacement while handling
download-hotfix, then provision runs on the staged binary
Verified by mutation: forcing the selection branch to false fails the
first and third cases.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4ae1dd11-ec06-4c26-bfc8-d1ef1f89fcf9
…an error When check-hotfix cannot reach the LPS and the node config carries no cold-start hotfixes map, there is nothing to stage, so the existing on-disk pointer is left intact and provisioning continues normally via the fail-open path. This is the expected state for a node seeded without an injected map, but it was reported as outcomeFailed, which helpersEventLevel maps to EventLevelError -- emitting an error-level CheckHotfix guest-agent event during a completely healthy provision. Introduce a distinct benign outcome, noColdStartPointer, so the "LPS was unreachable" signal is still preserved for diagnosis (along with the wrapped fetch error in the telemetry message) without misreporting a successful fail-open as a failure. Genuine failures -- parse errors, write errors, failed cold-start reads and authoritative non-benign 4xx rejections -- continue to report outcomeFailed at error level. Observed on a standalone validation node: the LPS is unreachable there, so every healthy node emitted an error-level event. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4ae1dd11-ec06-4c26-bfc8-d1ef1f89fcf9
What this changes
This adjusts #9101's ANC script-hotfix design to embed fully rendered, distro-specific
nodecustomdataYAML files in the ANC package instead of a JSON manifest plus separate payload files.The hotfix generator now selects changed
write_filesblocks from the traditional Linuxnodecustomdata.ymlpath and renders them through AgentBaker's canonical Go-template functions. ANC selects the YAML matching the running distro and transactionally replaces only files already present on the VHD. The embedded path reuses ANC's existing nodecustomdata model, decoder, and file-application engine rather than maintaining a second applier.Architecture
%%{init: {"themeVariables": {"fontSize": "18px"}}}%% flowchart TB subgraph Legend[Legend] direction LR L1[New]:::new L2[Updated]:::updated L3[Existing]:::existing end A["Changed provisioning scripts"]:::existing --> B["hotfix_generate.py"]:::updated B --> C["Render distro-specific<br/>nodecustomdata YAML"]:::new C --> D["Publish ANC package to PMC"]:::existing E["LPS base-version map<br/>default selector"]:::existing --> G["Install selected ANC package"]:::existing F["ABsvc version pointer<br/>cold-start fallback"]:::existing -.-> G D --> G G --> H["ANC applies embedded YAML<br/>when active"]:::new H --> I["Normal CSE provisioning"]:::existing H -. fail open .-> J["Keep VHD-baked scripts"]:::existing J --> I classDef new fill:#dafbe1,stroke:#1a7f37,color:#0b1f0f,stroke-width:2px,font-size:18px classDef updated fill:#fff8c5,stroke:#9a6700,color:#24292f,stroke-width:2px,font-size:18px classDef existing fill:#ddf4ff,stroke:#0969da,color:#0b1f0f,stroke-width:2px,font-size:18pxGenerated payload example
Each generated file is complete, template-free cloud-config. Conditional distro blocks are resolved by the same
getBakerFuncMapand node-custom-data variables used by production rendering rather than reimplemented in Python.Package selection and fallback
The LPS base-version map is the primary hotfix selector. ABsvc seeds the on-node configuration with a small single-version pointer so first-node provisioning still has a fallback when LPS is not ready. ABsvc never carries the rendered script contents; those are stored in the corresponding PMC package, avoiding CustomData size growth.
The package version identifies the exact hotfix content for a VHD version base, so this path does not generate or depend on
scripts_version. Package installation and payload application remain fail-open to the original VHD scripts.Application guarantees
activemarker avoids distro detection and file access when a package contains no script hotfix.Workflow behavior
The
hotfix-generateworkflow now tracks the generated ANC YAML directory and hotfix version JSON. It no longer editsparts/linux/cloud-init/nodecustomdata.yml. Script changes make the embedded ANC package a production change, causing the normal base-version patch bump and PMC package publication path. When a PR has no new script or ANC hotfix, the existing generated payload and version pointer are left unchanged; retirement remains explicit.Validation
go test ./pkg/agent ./hotfix/render-nodecustomdataaks-node-controllernodecustomdata and embedded payload testspython3 -m unittest hotfix.hotfix_generate_testgo vetfor the changed root, ANC, and E2E packagesThe broader ANC suite was also attempted locally; unrelated Windows-only failures remain where existing tests require
/bin/bashor hold temporary watcher files during cleanup.