diff --git a/aks-node-controller/README.md b/aks-node-controller/README.md index df2e1d5c8de..51337747968 100644 --- a/aks-node-controller/README.md +++ b/aks-node-controller/README.md @@ -256,7 +256,10 @@ application, including variants that share the `azurelinux` OS ID. Their distro-specific source changes do not trigger payload generation. The repository keeps a README placeholder so `go:embed` builds without any -script hotfix payloads. Generation replaces it with Ubuntu and Azure Linux YAMLs. +script hotfix payloads. Generation omits any rendered YAML with no `write_files` +entries (including missing, null, or empty lists) and logs the skipped platform. +If all platforms are empty, it writes a README placeholder to keep `go:embed` valid. +This does not add support for non-distro template conditions. ANC skips application when the local platform's YAML is absent; no separate activation flag is needed. Other payload read errors are logged. diff --git a/hotfix/hotfix_generate_test.py b/hotfix/hotfix_generate_test.py index 23b5ab5b11e..31ed80b03d3 100644 --- a/hotfix/hotfix_generate_test.py +++ b/hotfix/hotfix_generate_test.py @@ -56,6 +56,53 @@ class HotfixGenerateTests(unittest.TestCase): + def test_production_renderer_omits_empty_platform_payloads(self): + repository = Path(__file__).resolve().parents[1] + lines = (repository / hotfix_generate.TEMPLATE).read_text().splitlines( + keepends=True + ) + _, outer_else, end = hotfix_generate.find_block_boundaries(lines) + cases = [ + ("custom cloud only", hotfix_generate.build_hotfix_template( + {"initAKSCloud"}, lines[outer_else + 1:end] + ), set()), + ("missing", "#cloud-config\n", set()), + ("null", "write_files:\n", set()), + ("empty list", "write_files: []\n", set()), + ("ubuntu only", 'write_files:\n{{if not IsMariner}}\n' + '- path: /test\n content: hotfix\n{{end}}\n', {"ubuntu"}), + ("both", "write_files:\n- path: /test\n content: hotfix\n", + {"ubuntu", "mariner"}), + ] + for name, template, platforms in cases: + with self.subTest(name=name), tempfile.TemporaryDirectory() as directory: + template_path = Path(directory) / "template.yml" + template_path.write_text(template) + output = Path(directory) / "generated" + output.mkdir() + for platform in ("ubuntu", "mariner"): + (output / f"rendered_nodecustomdata_{platform}.yml").write_text( + "write_files:\n" # Stale empty files must also be removed. + ) + subprocess.run( + ["go", "run", "./hotfix/render-nodecustomdata", "--template", + str(template_path), "--output-dir", str(output)], + cwd=repository, check=True, capture_output=True, + ) + expected = { + f"rendered_nodecustomdata_{platform}.yml" for platform in platforms + } or {"README"} + self.assertEqual(expected, {path.name for path in output.iterdir()}) + if name == "custom cloud only": + probe = Path(directory) / "main.go" + probe.write_text('package main\nimport "embed"\n' + '//go:embed generated\nvar files embed.FS\n' + 'func main() {}\n') + subprocess.run( + ["go", "build", "-o", str(Path(directory) / "probe"), str(probe)], + cwd=repository, check=True, capture_output=True, + ) + def test_config_modules_are_independently_hotfixable(self): repository = Path(__file__).resolve().parents[1] lines = (repository / hotfix_generate.TEMPLATE).read_text().splitlines( diff --git a/hotfix/render-nodecustomdata/main.go b/hotfix/render-nodecustomdata/main.go index 798f8d36875..9d213fda857 100644 --- a/hotfix/render-nodecustomdata/main.go +++ b/hotfix/render-nodecustomdata/main.go @@ -11,6 +11,7 @@ import ( "github.com/Azure/agentbaker/pkg/agent" "github.com/Azure/agentbaker/pkg/agent/datamodel" + "gopkg.in/yaml.v3" ) type platform struct { @@ -41,6 +42,7 @@ func main() { fatalf("create output directory: %v", err) } + written := 0 for _, target := range platforms { rendered, err := agent.RenderLinuxNodeCustomDataTemplate( templateContent, @@ -53,10 +55,30 @@ func main() { *outputDir, "rendered_nodecustomdata_"+target.name+".yml", ) + var payload struct { + WriteFiles []yaml.Node `yaml:"write_files"` + } + if err := yaml.Unmarshal([]byte(rendered), &payload); err != nil { + fatalf("parse %s nodecustomdata: %v", target.name, err) + } + if len(payload.WriteFiles) == 0 { + fmt.Fprintf(os.Stderr, "Skipping %s nodecustomdata: no write_files entries\n", target.name) + if err := os.Remove(outputPath); err != nil && !os.IsNotExist(err) { + fatalf("remove empty %s nodecustomdata: %v", target.name, err) + } + continue + } //nolint:gosec // The caller intentionally selects the output directory; filenames are fixed by this tool. if err := os.WriteFile(outputPath, []byte(rendered), 0o600); err != nil { fatalf("write %s nodecustomdata: %v", target.name, err) } + written++ + } + if written == 0 { + // Keep at least one file for embedding when every platform is empty. + if err := os.WriteFile(filepath.Join(*outputDir, "README"), []byte("No applicable script hotfix payloads.\n"), 0o600); err != nil { + fatalf("write empty payload placeholder: %v", err) + } } }