From 18c6ca86a65853bd6f094d1a5da59e7a586771a2 Mon Sep 17 00:00:00 2001 From: sinmentis Date: Fri, 23 Oct 2026 16:49:06 +1300 Subject: [PATCH 1/2] fix: reduce legacy Linux custom data encoding overhead Avoid encoding each script separately when the complete traditional payload is already gzipped. Preserve file contents and alternate delivery modes. AB#40305782 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 162f68f8-110a-4d60-b4fe-b9fb42253542 --- hotfix/hotfix_generate_test.py | 3 +- hotfix/render-nodecustomdata/main_test.go | 16 +- parts/linux/cloud-init/nodecustomdata.yml | 135 +++++++---------- pkg/agent/baker.go | 42 ++++-- pkg/agent/baker_test.go | 11 +- pkg/agent/cloud_init_encoding_test.go | 139 ++++++++++++++++++ pkg/agent/live_patching_delivery_test.go | 4 +- .../mariner_live_patching_delivery_test.go | 4 +- pkg/agent/utils.go | 6 +- pkg/agent/utils_test.go | 10 +- pkg/agent/variables.go | 116 ++++++++------- 11 files changed, 323 insertions(+), 163 deletions(-) create mode 100644 pkg/agent/cloud_init_encoding_test.go diff --git a/hotfix/hotfix_generate_test.py b/hotfix/hotfix_generate_test.py index 7626a0863c7..cb147854378 100644 --- a/hotfix/hotfix_generate_test.py +++ b/hotfix/hotfix_generate_test.py @@ -161,7 +161,8 @@ def test_config_modules_are_independently_hotfixable(self): self.assertEqual({variable}, blocks[0][0]) self.assertIn(f"- path: {{{{{path_function}}}}}", template) self.assertIn('permissions: "0744"', template) - self.assertIn("encoding: gzip", template) + self.assertIn("encoding: {{GetCloudInitFileEncoding}}", template) + self.assertIn("| GetCloudInitFileContent}}", template) self.assertNotIn("{{if", template) def test_mariner_package_update_is_not_embedded_in_custom_data(self): diff --git a/hotfix/render-nodecustomdata/main_test.go b/hotfix/render-nodecustomdata/main_test.go index d490001e6c4..d7a674a1a1e 100644 --- a/hotfix/render-nodecustomdata/main_test.go +++ b/hotfix/render-nodecustomdata/main_test.go @@ -18,18 +18,16 @@ func TestRenderSelectedHotfixScripts(t *testing.T) { const initCloudBlock = `{{if IsAKSCustomCloud}} - path: {{GetInitAKSCloudFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "initAKSCloud"}} + content: {{GetVariableProperty "cloudInitData" "initAKSCloud" | GetCloudInitFileContent}} {{end}} ` const helperBlock = `- path: {{GetCSEHelpersScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSource"}} + content: {{GetVariableProperty "cloudInitData" "provisionSource" | GetCloudInitFileContent}} ` for _, distro := range []datamodel.Distro{ datamodel.AKSUbuntuContainerd2204Gen2, @@ -47,8 +45,9 @@ func TestRenderSelectedHotfixScripts(t *testing.T) { require.NoError(t, err) var payload struct { WriteFiles []struct { - Path string `yaml:"path"` - Content string `yaml:"content"` + Path string `yaml:"path"` + Encoding string `yaml:"encoding"` + Content string `yaml:"content"` } `yaml:"write_files"` } require.NoError(t, yaml.Unmarshal([]byte(rendered), &payload)) @@ -62,6 +61,7 @@ func TestRenderSelectedHotfixScripts(t *testing.T) { require.Len(t, payload.WriteFiles, 1+wantInitCount) for _, file := range payload.WriteFiles { require.NotEmpty(t, file.Content) + require.Empty(t, file.Encoding) } }) } diff --git a/parts/linux/cloud-init/nodecustomdata.yml b/parts/linux/cloud-init/nodecustomdata.yml index 200eeb0ce5f..d8d4ef91e5a 100644 --- a/parts/linux/cloud-init/nodecustomdata.yml +++ b/parts/linux/cloud-init/nodecustomdata.yml @@ -28,201 +28,174 @@ write_files: {{if IsAKSCustomCloud}} - path: {{GetInitAKSCloudFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "initAKSCloud"}} + content: {{GetVariableProperty "cloudInitData" "initAKSCloud" | GetCloudInitFileContent}} {{end}} {{- else }} - path: {{GetCSEHelpersScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSource"}} + content: {{GetVariableProperty "cloudInitData" "provisionSource" | GetCloudInitFileContent}} {{if IsACL }} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceACL"}} + content: {{GetVariableProperty "cloudInitData" "provisionSourceACL" | GetCloudInitFileContent}} {{- else if IsAzlOSGuard}} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceAzlOSGuard"}} + content: {{GetVariableProperty "cloudInitData" "provisionSourceAzlOSGuard" | GetCloudInitFileContent}} {{- else if IsMariner}} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceMariner"}} + content: {{GetVariableProperty "cloudInitData" "provisionSourceMariner" | GetCloudInitFileContent}} {{- else if IsFlatcar }} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceFlatcar"}} + content: {{GetVariableProperty "cloudInitData" "provisionSourceFlatcar" | GetCloudInitFileContent}} {{- else }} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceUbuntu"}} + content: {{GetVariableProperty "cloudInitData" "provisionSourceUbuntu" | GetCloudInitFileContent}} {{end}} {{ if not IsCustomImage -}} - path: /opt/azure/containers/provision_start.sh permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionStartScript"}} + content: {{GetVariableProperty "cloudInitData" "provisionStartScript" | GetCloudInitFileContent}} {{- end }} - path: /opt/azure/containers/provision.sh permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionScript"}} + content: {{GetVariableProperty "cloudInitData" "provisionScript" | GetCloudInitFileContent}} - path: {{GetCSEInstallScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstalls"}} + content: {{GetVariableProperty "cloudInitData" "provisionInstalls" | GetCloudInitFileContent}} {{if IsACL }} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsACL"}} + content: {{GetVariableProperty "cloudInitData" "provisionInstallsACL" | GetCloudInitFileContent}} {{- else if IsAzlOSGuard}} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsAzlOSGuard"}} + content: {{GetVariableProperty "cloudInitData" "provisionInstallsAzlOSGuard" | GetCloudInitFileContent}} {{- else if IsMariner}} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsMariner"}} + content: {{GetVariableProperty "cloudInitData" "provisionInstallsMariner" | GetCloudInitFileContent}} {{- else if IsFlatcar }} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsFlatcar"}} + content: {{GetVariableProperty "cloudInitData" "provisionInstallsFlatcar" | GetCloudInitFileContent}} {{- else }} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsUbuntu"}} + content: {{GetVariableProperty "cloudInitData" "provisionInstallsUbuntu" | GetCloudInitFileContent}} {{end}} - path: {{GetCSEConfigScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigs"}} + content: {{GetVariableProperty "cloudInitData" "provisionConfigs" | GetCloudInitFileContent}} - path: {{GetCSEConfigGPUScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsGPU"}} + content: {{GetVariableProperty "cloudInitData" "provisionConfigsGPU" | GetCloudInitFileContent}} - path: {{GetCSEConfigLocalDNSScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsLocalDNS"}} + content: {{GetVariableProperty "cloudInitData" "provisionConfigsLocalDNS" | GetCloudInitFileContent}} - path: {{GetCSEConfigKubeletScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsKubelet"}} + content: {{GetVariableProperty "cloudInitData" "provisionConfigsKubelet" | GetCloudInitFileContent}} - path: {{GetCSEConfigNetworkScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsNetwork"}} + content: {{GetVariableProperty "cloudInitData" "provisionConfigsNetwork" | GetCloudInitFileContent}} - path: {{GetCSEConfigAddonsScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsAddons"}} + content: {{GetVariableProperty "cloudInitData" "provisionConfigsAddons" | GetCloudInitFileContent}} - path: {{GetCSEConfigChronyScriptFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsChrony"}} + content: {{GetVariableProperty "cloudInitData" "provisionConfigsChrony" | GetCloudInitFileContent}} {{if IsAKSCustomCloud}} - path: {{GetInitAKSCloudFilepath}} permissions: "0744" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "initAKSCloud"}} + content: {{GetVariableProperty "cloudInitData" "initAKSCloud" | GetCloudInitFileContent}} {{end}} - path: /etc/systemd/system/reconcile-private-hosts.service permissions: "0644" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "reconcilePrivateHostsService"}} + content: {{GetVariableProperty "cloudInitData" "reconcilePrivateHostsService" | GetCloudInitFileContent}} - path: /etc/systemd/system/kubelet.service permissions: "0600" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "kubeletSystemdService"}} + content: {{GetVariableProperty "cloudInitData" "kubeletSystemdService" | GetCloudInitFileContent}} - path: /opt/azure-network/configure-azure-network.sh permissions: "0755" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "configureAzureNetworkScript"}} + content: {{GetVariableProperty "cloudInitData" "configureAzureNetworkScript" | GetCloudInitFileContent}} - path: /etc/udev/rules.d/99-azure-network.rules permissions: "0644" - encoding: gzip + encoding: {{GetCloudInitFileEncoding}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "azureNetworkUdevRule"}} + content: {{GetVariableProperty "cloudInitData" "azureNetworkUdevRule" | GetCloudInitFileContent}} {{- end }} diff --git a/pkg/agent/baker.go b/pkg/agent/baker.go index 91eacc7f9fb..e21c13e2926 100644 --- a/pkg/agent/baker.go +++ b/pkg/agent/baker.go @@ -346,17 +346,20 @@ func buildScriptlessCustomData(cloudInitTemplate, fileListTemplate, separator st // GetLinuxNodeCustomDataJSONObject returns Linux customData JSON object in the form. // { "customData": "" }. func (t *TemplateGenerator) getLinuxNodeCustomDataJSONObject(config *datamodel.NodeBootstrappingConfiguration) string { - // get parameters - parameters := getParameters(config) - // get variable cloudInit - variables := getCustomDataVariables(config) - str, e := t.getSingleLineForTemplate(kubernetesNodeCustomDataYaml, config.AgentPoolProfile, getBakerFuncMap(config, parameters, variables), true) - - if e != nil { - panic(e) + b, err := parts.Templates.ReadFile(kubernetesNodeCustomDataYaml) + if err != nil { + panic(err) + } + str, err := RenderLinuxNodeCustomDataTemplate(b, config) + if err != nil { + panic(err) } - return fmt.Sprintf("{\"customData\": \"%s\"}", str) + customData, err := json.Marshal(map[string]string{"customData": str}) + if err != nil { + panic(err) + } + return string(customData) } const ( @@ -677,6 +680,8 @@ func (t *TemplateGenerator) getSingleLine(textFilename string, profile interface // same variables and functions used by the production AgentBaker path. // Callers must supply a complete configuration, as required by the production // rendering helpers. +// File entries must use GetCloudInitFileEncoding and GetCloudInitFileContent +// to select the encoding for the delivery mode. func RenderLinuxNodeCustomDataTemplate(templateContent []byte, config *datamodel.NodeBootstrappingConfiguration) (string, error) { parameters := getParameters(config) variables := getCustomDataVariables(config) @@ -694,10 +699,29 @@ func RenderLinuxNodeCustomDataTemplate(templateContent []byte, config *datamodel return buffer.String(), nil } +// Traditional custom data is gzipped as a whole, so its files need no inner encoding. +func usePlainCloudInit(config *datamodel.NodeBootstrappingConfiguration) bool { + return !config.EnableScriptlessCSECmd && !config.IsFlatcar() && !config.IsACL() +} + // getTemplateFuncMap returns the general purpose template func map from getContainerServiceFuncMap. func getBakerFuncMap(config *datamodel.NodeBootstrappingConfiguration, params paramsMap, variables paramsMap) template.FuncMap { funcMap := getContainerServiceFuncMap(config) + funcMap["GetCloudInitFileEncoding"] = func() string { + if usePlainCloudInit(config) { + return `""` + } + return encodingGZIP + } + funcMap["GetCloudInitFileContent"] = func(content string) (string, error) { + if usePlainCloudInit(config) { + encoded, err := json.Marshal(content) + return string(encoded), err + } + return "!!binary |\n " + content, nil + } + funcMap["GetParameter"] = func(s string) interface{} { if v, ok := params[s].(paramsMap); ok && v != nil { if v["value"] == nil { diff --git a/pkg/agent/baker_test.go b/pkg/agent/baker_test.go index d0d34b9e0cd..0d256ee7100 100644 --- a/pkg/agent/baker_test.go +++ b/pkg/agent/baker_test.go @@ -27,6 +27,7 @@ import ( . "github.com/onsi/gomega" "github.com/stretchr/testify/require" "github.com/vincent-petithory/dataurl" + "gopkg.in/yaml.v3" ) // this regex looks for groups of the following forms, returning KEY and VALUE as submatches. @@ -1551,8 +1552,14 @@ var _ = Describe("getLinuxNodeBootstrappingPayload", func() { expectedCustomData := getCustomDataFromJSON(templateGenerator.getLinuxNodeCustomDataJSONObject(config)) Expect(string(decompressedPayload)).To(Equal(expectedCustomData)) - Expect(string(decompressedPayload)).NotTo(ContainSubstring(aksNodeCustomDataFilepath)) - Expect(string(decompressedPayload)).NotTo(ContainSubstring(aksNbcCmdFilepath)) + // Script contents can reference these paths; only delivery as write_files + // entries would indicate that the scriptless path was used. + var cloudConfig cloudInit + Expect(yaml.Unmarshal(decompressedPayload, &cloudConfig)).To(Succeed()) + for _, file := range cloudConfig.WriteFiles { + Expect(file.Path).NotTo(Equal(aksNodeCustomDataFilepath)) + Expect(file.Path).NotTo(Equal(aksNbcCmdFilepath)) + } }) }) diff --git a/pkg/agent/cloud_init_encoding_test.go b/pkg/agent/cloud_init_encoding_test.go new file mode 100644 index 00000000000..0f20758a141 --- /dev/null +++ b/pkg/agent/cloud_init_encoding_test.go @@ -0,0 +1,139 @@ +package agent + +import ( + "encoding/base64" + "fmt" + "strings" + "testing" + "text/template" + + "github.com/Azure/agentbaker/parts" + "github.com/Azure/agentbaker/pkg/agent/datamodel" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestTraditionalCloudInitUsesSingleCompression(t *testing.T) { + template, err := parts.Templates.ReadFile(kubernetesNodeCustomDataYaml) + require.NoError(t, err) + generator := InitializeTemplateGenerator() + for _, distro := range []datamodel.Distro{datamodel.AKSUbuntuContainerd2204Gen2, datamodel.AKSAzureLinuxV2Gen2, datamodel.AKSAzureLinuxV3Gen2} { + for _, custom := range []bool{false, true} { + for _, dns := range []bool{false, true} { + for _, preProvision := range []bool{false, true} { + t.Run(fmt.Sprintf("%s/custom=%t/localdns=%t/preProvision=%t", distro, custom, dns, preProvision), func(t *testing.T) { + config := newNodeCustomDataRenderConfig(distro) + config.PreProvisionOnly = preProvision + config.AgentPoolProfile.LocalDNSProfile = &datamodel.LocalDNSProfile{EnableLocalDNS: dns} + if custom { + config.ContainerService.Properties.CustomCloudEnv = &datamodel.CustomCloudEnv{Name: "akscustom"} + } + original, err := generator.getSingleLine(kubernetesNodeCustomDataYaml, config.AgentPoolProfile, legacyCloudInitFuncMap(config), true) + require.NoError(t, err) + payload := generator.getLinuxNodeBootstrappingPayload(config) + compressed, err := base64.StdEncoding.Strict().DecodeString(payload) + require.NoError(t, err) + decoded, err := getGzipDecodedValue(compressed) + require.NoError(t, err) + require.True(t, strings.HasPrefix(string(decoded), "#cloud-config\n")) + rendered, err := RenderLinuxNodeCustomDataTemplate(template, config) + require.NoError(t, err) + require.Equal(t, rendered, string(decoded)) + var before, after cloudInit + require.NoError(t, yaml.Unmarshal([]byte(original), &before)) + require.NoError(t, yaml.Unmarshal(decoded, &after)) + beforeFiles, err := buildIgnitionTarEntries(before) + require.NoError(t, err) + afterFiles, err := buildIgnitionTarEntries(after) + require.NoError(t, err) + require.Equal(t, beforeFiles, afterFiles) + for _, file := range after.WriteFiles { + require.Empty(t, file.Encoding) + } + require.Less(t, len(payload), len(getBase64EncodedGzippedCustomScriptFromStr(original))) + t.Logf("customData: %d encoded characters", len(payload)) + // Ubuntu custom-cloud overflow is tracked separately in #9790. + if !custom || distro != datamodel.AKSUbuntuContainerd2204Gen2 { + require.Less(t, len(payload), MaxCustomDataLength) + } + }) + } + } + } + } +} + +func TestCloudInitEncodingLeavesOtherModesUnchanged(t *testing.T) { + template, err := parts.Templates.ReadFile(kubernetesNodeCustomDataYaml) + require.NoError(t, err) + generator := InitializeTemplateGenerator() + for _, distro := range []datamodel.Distro{datamodel.AKSUbuntuContainerd2204Gen2, datamodel.AKSAzureLinuxV3Gen2, datamodel.AKSFlatcarGen2, datamodel.AKSACLGen2TL} { + for _, scriptless := range []bool{false, true} { + if !scriptless && distro != datamodel.AKSFlatcarGen2 && distro != datamodel.AKSACLGen2TL { + continue + } + t.Run(fmt.Sprintf("%s/scriptless=%t", distro, scriptless), func(t *testing.T) { + config := newNodeCustomDataRenderConfig(distro) + config.EnableScriptlessCSECmd = scriptless + config.ContainerService.Properties.CustomCloudEnv = &datamodel.CustomCloudEnv{Name: "akscustom"} + original, err := generator.getSingleLine(kubernetesNodeCustomDataYaml, config.AgentPoolProfile, legacyCloudInitFuncMap(config), true) + require.NoError(t, err) + rendered, err := RenderLinuxNodeCustomDataTemplate(template, config) + require.NoError(t, err) + require.Equal(t, original, rendered) + require.Equal(t, original, getCustomDataFromJSON(generator.getLinuxNodeCustomDataJSONObject(config))) + }) + } + } +} + +func legacyCloudInitFuncMap(config *datamodel.NodeBootstrappingConfiguration) template.FuncMap { + funcs := getBakerFuncMap(config, getParameters(config), getCustomDataVariablesWithEncoder(config, false)) + funcs["GetCloudInitFileEncoding"] = func() string { return encodingGZIP } + funcs["GetCloudInitFileContent"] = func(content string) string { return "!!binary |\n " + content } + return funcs +} + +func TestCloudInitRenderingPreservesAdditionalFields(t *testing.T) { + input := []byte("#cloud-config\nruncmd:\n- echo hello\nwrite_files:\n- path: /opt/example\n append: true\n defer: true\n content: \"a\\tb\\n\\n\"\n") + output, err := RenderLinuxNodeCustomDataTemplate(input, newNodeCustomDataRenderConfig(datamodel.AKSUbuntuContainerd2204Gen2)) + require.NoError(t, err) + require.Equal(t, string(input), output) +} + +func TestPlainCloudInitFileContentPreservesWhitespace(t *testing.T) { + input := []byte("#cloud-config\nwrite_files:\n- path: /opt/example\n encoding: {{GetCloudInitFileEncoding}}\n content: {{.Name | GetCloudInitFileContent}}\n") + for _, content := range []string{"tab\tquote\"backslash\\", "trailing newline\n\n", "\u00e9\r\n"} { + config := newNodeCustomDataRenderConfig(datamodel.AKSUbuntuContainerd2204Gen2) + config.AgentPoolProfile.Name = content + output, err := RenderLinuxNodeCustomDataTemplate(input, config) + require.NoError(t, err) + var parsed cloudInit + require.NoError(t, yaml.Unmarshal([]byte(output), &parsed)) + require.Len(t, parsed.WriteFiles, 1) + require.Empty(t, parsed.WriteFiles[0].Encoding) + require.Equal(t, content, parsed.WriteFiles[0].Content) + } +} + +func BenchmarkTraditionalCloudInitEncoding(b *testing.B) { + for _, old := range []bool{true, false} { + b.Run(fmt.Sprintf("legacy=%t", old), func(b *testing.B) { + config := newNodeCustomDataRenderConfig(datamodel.AKSUbuntuContainerd2204Gen2) + generator := InitializeTemplateGenerator() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if old { + rendered, err := generator.getSingleLineForTemplate(kubernetesNodeCustomDataYaml, config.AgentPoolProfile, legacyCloudInitFuncMap(config), true) + if err != nil { + b.Fatal(err) + } + customData := getCustomDataFromJSON(fmt.Sprintf("{\"customData\": \"%s\"}", rendered)) + _ = getBase64EncodedGzippedCustomScriptFromStr(customData) + } else { + _ = generator.getLinuxNodeBootstrappingPayload(config) + } + } + }) + } +} diff --git a/pkg/agent/live_patching_delivery_test.go b/pkg/agent/live_patching_delivery_test.go index b616b2bcf0e..04ca8b846c2 100644 --- a/pkg/agent/live_patching_delivery_test.go +++ b/pkg/agent/live_patching_delivery_test.go @@ -18,7 +18,9 @@ func TestLivePatchingUsesVHDInsteadOfCustomData(t *testing.T) { require.NotContains(t, rendered, "/opt/azure/containers/ubuntu-snapshot-update.sh") require.NotContains(t, rendered, "/opt/azure/containers/security-update.sh") require.NotContains(t, rendered, "/opt/azure/containers/npd-update.sh") - size := len(getBase64EncodedGzippedCustomScriptFromStr(rendered)) + payload := InitializeTemplateGenerator().getLinuxNodeBootstrappingPayload(config) + require.Equal(t, getBase64EncodedGzippedCustomScriptFromStr(rendered), payload) + size := len(payload) t.Logf("traditional custom data: %d encoded characters", size) require.Less(t, size, MaxCustomDataLength) } diff --git a/pkg/agent/mariner_live_patching_delivery_test.go b/pkg/agent/mariner_live_patching_delivery_test.go index e7cf41fc494..7b9cb9293f3 100644 --- a/pkg/agent/mariner_live_patching_delivery_test.go +++ b/pkg/agent/mariner_live_patching_delivery_test.go @@ -20,7 +20,9 @@ func TestMarinerLivePatchingUsesVHDInsteadOfCustomData(t *testing.T) { require.NotContains(t, rendered, "/opt/azure/containers/mariner-package-update.sh") require.NotContains(t, rendered, "/opt/azure/containers/security-update.sh") require.NotContains(t, rendered, "/opt/azure/containers/npd-update.sh") - size := len(getBase64EncodedGzippedCustomScriptFromStr(rendered)) + payload := InitializeTemplateGenerator().getLinuxNodeBootstrappingPayload(config) + require.Equal(t, getBase64EncodedGzippedCustomScriptFromStr(rendered), payload) + size := len(payload) t.Logf("traditional custom data: %d encoded characters", size) require.Less(t, size, MaxCustomDataLength) }) diff --git a/pkg/agent/utils.go b/pkg/agent/utils.go index 7b6d4072fa9..24fb83b4004 100644 --- a/pkg/agent/utils.go +++ b/pkg/agent/utils.go @@ -181,6 +181,10 @@ func escapeSingleLine(escapedStr string) string { // getBase64EncodedGzippedCustomScript will return a base64 of the CSE. func getBase64EncodedGzippedCustomScript(csFilename string, config *datamodel.NodeBootstrappingConfiguration) string { + return getBase64EncodedGzippedCustomScriptFromStr(getCustomScript(csFilename, config)) +} + +func getCustomScript(csFilename string, config *datamodel.NodeBootstrappingConfiguration) string { b, err := parts.Templates.ReadFile(csFilename) if err != nil { // this should never happen and this is a bug. @@ -202,7 +206,7 @@ func getBase64EncodedGzippedCustomScript(csFilename string, config *datamodel.No } csStr := buffer.String() csStr = strings.ReplaceAll(csStr, "\r\n", "\n") - return getBase64EncodedGzippedCustomScriptFromStr(csStr) + return csStr } // This is "best-effort" - removes MOST of the comments with obvious formats, to lower the space required by CustomData component. diff --git a/pkg/agent/utils_test.go b/pkg/agent/utils_test.go index bf884dad2c1..dcc08d78316 100644 --- a/pkg/agent/utils_test.go +++ b/pkg/agent/utils_test.go @@ -1284,7 +1284,7 @@ func TestRemoveComments_ShellPatterns(t *testing.T) { // syntax error after stripping. // // The script list is dynamically derived by parsing variables.go and const.go source -// to find all .sh files passed to getBase64EncodedGzippedCustomScript(). If a new script +// to find all .sh files passed to encodeScript(). If a new script // is added to the CSE pipeline, it is automatically covered by this test. func TestCSEScriptRoundTrip(t *testing.T) { cseScripts := discoverCSEScripts(t) @@ -1304,21 +1304,21 @@ func TestCSEScriptRoundTrip(t *testing.T) { } // discoverCSEScripts parses variables.go to find all constant names passed to -// getBase64EncodedGzippedCustomScript(), then resolves those constants to file +// encodeScript(), then resolves those constants to file // paths from const.go, filtering to .sh files only. func discoverCSEScripts(t *testing.T) []string { t.Helper() root := repoRoot() - // Step 1: Read variables.go and extract constant names from getBase64EncodedGzippedCustomScript() calls + // Step 1: Read variables.go and extract constant names from encodeScript() calls variablesPath := filepath.Join(root, "pkg", "agent", "variables.go") variablesBytes, err := os.ReadFile(variablesPath) if err != nil { t.Fatalf("failed to read variables.go: %v", err) } - // Match: getBase64EncodedGzippedCustomScript(constantName, config) - callRe := regexp.MustCompile(`getBase64EncodedGzippedCustomScript\((\w+),`) + // Match: encodeScript(constantName, config) + callRe := regexp.MustCompile(`encodeScript\((\w+),`) matches := callRe.FindAllStringSubmatch(string(variablesBytes), -1) constNames := make(map[string]bool) for _, m := range matches { diff --git a/pkg/agent/variables.go b/pkg/agent/variables.go index 7efc595a410..8821684886f 100644 --- a/pkg/agent/variables.go +++ b/pkg/agent/variables.go @@ -12,73 +12,81 @@ import ( // getCustomDataVariables returns cloudinit data used by Linux. func getCustomDataVariables(config *datamodel.NodeBootstrappingConfiguration) paramsMap { + return getCustomDataVariablesWithEncoder(config, usePlainCloudInit(config)) +} + +func getCustomDataVariablesWithEncoder(config *datamodel.NodeBootstrappingConfiguration, plain bool) paramsMap { + encodeScript := getBase64EncodedGzippedCustomScript + if plain { + encodeScript = getCustomScript + } cs := config.ContainerService cloudInitFiles := map[string]interface{}{ "cloudInitData": paramsMap{ - "provisionStartScript": getBase64EncodedGzippedCustomScript(kubernetesCSEStartScript, config), - "provisionScript": getBase64EncodedGzippedCustomScript(kubernetesCSEMainScript, config), - "provisionSource": getBase64EncodedGzippedCustomScript(kubernetesCSEHelpersScript, config), - "provisionSourceUbuntu": getBase64EncodedGzippedCustomScript(kubernetesCSEHelpersScriptUbuntu, config), - "provisionSourceMariner": getBase64EncodedGzippedCustomScript(kubernetesCSEHelpersScriptMariner, config), - "provisionSourceAzlOSGuard": getBase64EncodedGzippedCustomScript(kubernetesCSEHelpersScriptAzlOSGuard, config), - "provisionSourceFlatcar": getBase64EncodedGzippedCustomScript(kubernetesCSEHelpersScriptFlatcar, config), - "provisionSourceACL": getBase64EncodedGzippedCustomScript(kubernetesCSEHelpersScriptACL, config), - "provisionInstalls": getBase64EncodedGzippedCustomScript(kubernetesCSEInstall, config), - "provisionInstallsUbuntu": getBase64EncodedGzippedCustomScript(kubernetesCSEInstallUbuntu, config), - "provisionInstallsMariner": getBase64EncodedGzippedCustomScript(kubernetesCSEInstallMariner, config), - "provisionInstallsAzlOSGuard": getBase64EncodedGzippedCustomScript(kubernetesCSEInstallAzlOSGuard, config), - "provisionInstallsFlatcar": getBase64EncodedGzippedCustomScript(kubernetesCSEInstallFlatcar, config), - "provisionInstallsACL": getBase64EncodedGzippedCustomScript(kubernetesCSEInstallACL, config), - "provisionConfigs": getBase64EncodedGzippedCustomScript(kubernetesCSEConfig, config), - "provisionConfigsGPU": getBase64EncodedGzippedCustomScript(kubernetesCSEConfigGPU, config), - "provisionConfigsLocalDNS": getBase64EncodedGzippedCustomScript(kubernetesCSEConfigLocalDNS, config), - "provisionConfigsKubelet": getBase64EncodedGzippedCustomScript(kubernetesCSEConfigKubelet, config), - "provisionConfigsNetwork": getBase64EncodedGzippedCustomScript(kubernetesCSEConfigNetwork, config), - "provisionConfigsAddons": getBase64EncodedGzippedCustomScript(kubernetesCSEConfigAddons, config), - "provisionConfigsChrony": getBase64EncodedGzippedCustomScript(kubernetesCSEConfigChrony, config), - "provisionSendLogs": getBase64EncodedGzippedCustomScript(kubernetesCSESendLogs, config), - "provisionRedactCloudConfig": getBase64EncodedGzippedCustomScript(kubernetesCSERedactCloudConfig, config), - "customSearchDomainsScript": getBase64EncodedGzippedCustomScript(kubernetesCustomSearchDomainsScript, config), - "dhcpv6SystemdService": getBase64EncodedGzippedCustomScript(dhcpv6SystemdService, config), - "dhcpv6ConfigurationScript": getBase64EncodedGzippedCustomScript(dhcpv6ConfigurationScript, config), - "kubeletSystemdService": getBase64EncodedGzippedCustomScript(kubeletSystemdService, config), - "reconcilePrivateHostsScript": getBase64EncodedGzippedCustomScript(reconcilePrivateHostsScript, config), - "reconcilePrivateHostsService": getBase64EncodedGzippedCustomScript(reconcilePrivateHostsService, config), - "ensureNoDupEbtablesScript": getBase64EncodedGzippedCustomScript(ensureNoDupEbtablesScript, config), - "ensureNoDupEbtablesService": getBase64EncodedGzippedCustomScript(ensureNoDupEbtablesService, config), - "bindMountScript": getBase64EncodedGzippedCustomScript(bindMountScript, config), - "bindMountSystemdService": getBase64EncodedGzippedCustomScript(bindMountSystemdService, config), - "migPartitionSystemdService": getBase64EncodedGzippedCustomScript(migPartitionSystemdService, config), - "migPartitionScript": getBase64EncodedGzippedCustomScript(migPartitionScript, config), - "ensureIMDSRestrictionScript": getBase64EncodedGzippedCustomScript(ensureIMDSRestrictionScript, config), - "snapshotUpdateScript": getBase64EncodedGzippedCustomScript(snapshotUpdateScript, config), - "snapshotUpdateService": getBase64EncodedGzippedCustomScript(snapshotUpdateSystemdService, config), - "snapshotUpdateTimer": getBase64EncodedGzippedCustomScript(snapshotUpdateSystemdTimer, config), - "packageUpdateScriptMariner": getBase64EncodedGzippedCustomScript(packageUpdateScriptMariner, config), - "packageUpdateServiceMariner": getBase64EncodedGzippedCustomScript(packageUpdateSystemdServiceMariner, config), - "packageUpdateTimerMariner": getBase64EncodedGzippedCustomScript(packageUpdateSystemdTimerMariner, config), - "componentManifestFile": getBase64EncodedGzippedCustomScript(componentManifestFile, config), - "validateKubeletCredentialsScript": getBase64EncodedGzippedCustomScript(validateKubeletCredentialsScript, config), - "secureTLSBootstrapService": getBase64EncodedGzippedCustomScript(secureTLSBootstrapService, config), - "cloudInitStatusCheckScript": getBase64EncodedGzippedCustomScript(cloudInitStatusCheckScript, config), - "measureTLSBootstrappingLatencyScript": getBase64EncodedGzippedCustomScript(measureTLSBootstrappingLatencyScript, config), - "measureTLSBootstrappingLatencyService": getBase64EncodedGzippedCustomScript(measureTLSBootstrappingLatencyService, config), - "configureAzureNetworkScript": getBase64EncodedGzippedCustomScript(configureAzureNetworkScript, config), - "azureNetworkUdevRule": getBase64EncodedGzippedCustomScript(azureNetworkUdevRule, config), + "provisionStartScript": encodeScript(kubernetesCSEStartScript, config), + "provisionScript": encodeScript(kubernetesCSEMainScript, config), + "provisionSource": encodeScript(kubernetesCSEHelpersScript, config), + "provisionSourceUbuntu": encodeScript(kubernetesCSEHelpersScriptUbuntu, config), + "provisionSourceMariner": encodeScript(kubernetesCSEHelpersScriptMariner, config), + "provisionSourceAzlOSGuard": encodeScript(kubernetesCSEHelpersScriptAzlOSGuard, config), + "provisionSourceFlatcar": encodeScript(kubernetesCSEHelpersScriptFlatcar, config), + "provisionSourceACL": encodeScript(kubernetesCSEHelpersScriptACL, config), + "provisionInstalls": encodeScript(kubernetesCSEInstall, config), + "provisionInstallsUbuntu": encodeScript(kubernetesCSEInstallUbuntu, config), + "provisionInstallsMariner": encodeScript(kubernetesCSEInstallMariner, config), + "provisionInstallsAzlOSGuard": encodeScript(kubernetesCSEInstallAzlOSGuard, config), + "provisionInstallsFlatcar": encodeScript(kubernetesCSEInstallFlatcar, config), + "provisionInstallsACL": encodeScript(kubernetesCSEInstallACL, config), + "provisionConfigs": encodeScript(kubernetesCSEConfig, config), + "provisionConfigsGPU": encodeScript(kubernetesCSEConfigGPU, config), + "provisionConfigsLocalDNS": encodeScript(kubernetesCSEConfigLocalDNS, config), + "provisionConfigsKubelet": encodeScript(kubernetesCSEConfigKubelet, config), + "provisionConfigsNetwork": encodeScript(kubernetesCSEConfigNetwork, config), + "provisionConfigsAddons": encodeScript(kubernetesCSEConfigAddons, config), + "provisionConfigsChrony": encodeScript(kubernetesCSEConfigChrony, config), + "provisionSendLogs": encodeScript(kubernetesCSESendLogs, config), + "provisionRedactCloudConfig": encodeScript(kubernetesCSERedactCloudConfig, config), + "customSearchDomainsScript": encodeScript(kubernetesCustomSearchDomainsScript, config), + "dhcpv6SystemdService": encodeScript(dhcpv6SystemdService, config), + "dhcpv6ConfigurationScript": encodeScript(dhcpv6ConfigurationScript, config), + "kubeletSystemdService": encodeScript(kubeletSystemdService, config), + "reconcilePrivateHostsScript": encodeScript(reconcilePrivateHostsScript, config), + "reconcilePrivateHostsService": encodeScript(reconcilePrivateHostsService, config), + "ensureNoDupEbtablesScript": encodeScript(ensureNoDupEbtablesScript, config), + "ensureNoDupEbtablesService": encodeScript(ensureNoDupEbtablesService, config), + "bindMountScript": encodeScript(bindMountScript, config), + "bindMountSystemdService": encodeScript(bindMountSystemdService, config), + "migPartitionSystemdService": encodeScript(migPartitionSystemdService, config), + "migPartitionScript": encodeScript(migPartitionScript, config), + "ensureIMDSRestrictionScript": encodeScript(ensureIMDSRestrictionScript, config), + "snapshotUpdateScript": encodeScript(snapshotUpdateScript, config), + "snapshotUpdateService": encodeScript(snapshotUpdateSystemdService, config), + "snapshotUpdateTimer": encodeScript(snapshotUpdateSystemdTimer, config), + "packageUpdateScriptMariner": encodeScript(packageUpdateScriptMariner, config), + "packageUpdateServiceMariner": encodeScript(packageUpdateSystemdServiceMariner, config), + "packageUpdateTimerMariner": encodeScript(packageUpdateSystemdTimerMariner, config), + "componentManifestFile": encodeScript(componentManifestFile, config), + "validateKubeletCredentialsScript": encodeScript(validateKubeletCredentialsScript, config), + "secureTLSBootstrapService": encodeScript(secureTLSBootstrapService, config), + "cloudInitStatusCheckScript": encodeScript(cloudInitStatusCheckScript, config), + "measureTLSBootstrappingLatencyScript": encodeScript(measureTLSBootstrappingLatencyScript, config), + "measureTLSBootstrappingLatencyService": encodeScript(measureTLSBootstrappingLatencyService, config), + "configureAzureNetworkScript": encodeScript(configureAzureNetworkScript, config), + "azureNetworkUdevRule": encodeScript(azureNetworkUdevRule, config), }, } cloudInitData := cloudInitFiles["cloudInitData"].(paramsMap) //nolint:errcheck // no error is actually here - cloudInitData["initAKSCloud"] = getBase64EncodedGzippedCustomScript(initAKSCloudScript, config) + cloudInitData["initAKSCloud"] = encodeScript(initAKSCloudScript, config) if config.IsFlatcar() || config.IsACL() { cloudInitData["provisionRedactCloudConfig"] = "" // Flatcar and ACL do not have cloud-init } if !cs.Properties.IsVHDDistroForAllNodes() { - cloudInitData["kmsSystemdService"] = getBase64EncodedGzippedCustomScript(kmsSystemdService, config) - cloudInitData["aptPreferences"] = getBase64EncodedGzippedCustomScript(aptPreferences, config) - cloudInitData["dockerClearMountPropagationFlags"] = getBase64EncodedGzippedCustomScript(dockerClearMountPropagationFlags, config) + cloudInitData["kmsSystemdService"] = encodeScript(kmsSystemdService, config) + cloudInitData["aptPreferences"] = encodeScript(aptPreferences, config) + cloudInitData["dockerClearMountPropagationFlags"] = encodeScript(dockerClearMountPropagationFlags, config) } return cloudInitFiles From d4da1bb717df4fac1a8861bdaacaa93aac5ce104 Mon Sep 17 00:00:00 2001 From: sinmentis Date: Fri, 23 Oct 2026 16:57:36 +1300 Subject: [PATCH 2/2] refactor: extract cloud-init encoding and test helpers Keep template registration and the test matrix below the cognitive complexity limit without changing behavior. AB#40305782 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 162f68f8-110a-4d60-b4fe-b9fb42253542 --- pkg/agent/baker.go | 26 +++++++---- pkg/agent/cloud_init_encoding_test.go | 63 +++++++++++++++------------ 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/pkg/agent/baker.go b/pkg/agent/baker.go index e21c13e2926..9e65daaa76d 100644 --- a/pkg/agent/baker.go +++ b/pkg/agent/baker.go @@ -704,22 +704,30 @@ func usePlainCloudInit(config *datamodel.NodeBootstrappingConfiguration) bool { return !config.EnableScriptlessCSECmd && !config.IsFlatcar() && !config.IsACL() } +func getCloudInitFileEncoding(config *datamodel.NodeBootstrappingConfiguration) string { + if usePlainCloudInit(config) { + return `""` + } + return encodingGZIP +} + +func getCloudInitFileContent(config *datamodel.NodeBootstrappingConfiguration, content string) (string, error) { + if usePlainCloudInit(config) { + encoded, err := json.Marshal(content) + return string(encoded), err + } + return "!!binary |\n " + content, nil +} + // getTemplateFuncMap returns the general purpose template func map from getContainerServiceFuncMap. func getBakerFuncMap(config *datamodel.NodeBootstrappingConfiguration, params paramsMap, variables paramsMap) template.FuncMap { funcMap := getContainerServiceFuncMap(config) funcMap["GetCloudInitFileEncoding"] = func() string { - if usePlainCloudInit(config) { - return `""` - } - return encodingGZIP + return getCloudInitFileEncoding(config) } funcMap["GetCloudInitFileContent"] = func(content string) (string, error) { - if usePlainCloudInit(config) { - encoded, err := json.Marshal(content) - return string(encoded), err - } - return "!!binary |\n " + content, nil + return getCloudInitFileContent(config, content) } funcMap["GetParameter"] = func(s string) interface{} { diff --git a/pkg/agent/cloud_init_encoding_test.go b/pkg/agent/cloud_init_encoding_test.go index 0f20758a141..42458c64d23 100644 --- a/pkg/agent/cloud_init_encoding_test.go +++ b/pkg/agent/cloud_init_encoding_test.go @@ -16,7 +16,6 @@ import ( func TestTraditionalCloudInitUsesSingleCompression(t *testing.T) { template, err := parts.Templates.ReadFile(kubernetesNodeCustomDataYaml) require.NoError(t, err) - generator := InitializeTemplateGenerator() for _, distro := range []datamodel.Distro{datamodel.AKSUbuntuContainerd2204Gen2, datamodel.AKSAzureLinuxV2Gen2, datamodel.AKSAzureLinuxV3Gen2} { for _, custom := range []bool{false, true} { for _, dns := range []bool{false, true} { @@ -28,34 +27,7 @@ func TestTraditionalCloudInitUsesSingleCompression(t *testing.T) { if custom { config.ContainerService.Properties.CustomCloudEnv = &datamodel.CustomCloudEnv{Name: "akscustom"} } - original, err := generator.getSingleLine(kubernetesNodeCustomDataYaml, config.AgentPoolProfile, legacyCloudInitFuncMap(config), true) - require.NoError(t, err) - payload := generator.getLinuxNodeBootstrappingPayload(config) - compressed, err := base64.StdEncoding.Strict().DecodeString(payload) - require.NoError(t, err) - decoded, err := getGzipDecodedValue(compressed) - require.NoError(t, err) - require.True(t, strings.HasPrefix(string(decoded), "#cloud-config\n")) - rendered, err := RenderLinuxNodeCustomDataTemplate(template, config) - require.NoError(t, err) - require.Equal(t, rendered, string(decoded)) - var before, after cloudInit - require.NoError(t, yaml.Unmarshal([]byte(original), &before)) - require.NoError(t, yaml.Unmarshal(decoded, &after)) - beforeFiles, err := buildIgnitionTarEntries(before) - require.NoError(t, err) - afterFiles, err := buildIgnitionTarEntries(after) - require.NoError(t, err) - require.Equal(t, beforeFiles, afterFiles) - for _, file := range after.WriteFiles { - require.Empty(t, file.Encoding) - } - require.Less(t, len(payload), len(getBase64EncodedGzippedCustomScriptFromStr(original))) - t.Logf("customData: %d encoded characters", len(payload)) - // Ubuntu custom-cloud overflow is tracked separately in #9790. - if !custom || distro != datamodel.AKSUbuntuContainerd2204Gen2 { - require.Less(t, len(payload), MaxCustomDataLength) - } + assertTraditionalCloudInitPayload(t, template, config) }) } } @@ -63,6 +35,39 @@ func TestTraditionalCloudInitUsesSingleCompression(t *testing.T) { } } +func assertTraditionalCloudInitPayload(t *testing.T, template []byte, config *datamodel.NodeBootstrappingConfiguration) { + t.Helper() + generator := InitializeTemplateGenerator() + original, err := generator.getSingleLine(kubernetesNodeCustomDataYaml, config.AgentPoolProfile, legacyCloudInitFuncMap(config), true) + require.NoError(t, err) + payload := generator.getLinuxNodeBootstrappingPayload(config) + compressed, err := base64.StdEncoding.Strict().DecodeString(payload) + require.NoError(t, err) + decoded, err := getGzipDecodedValue(compressed) + require.NoError(t, err) + require.True(t, strings.HasPrefix(string(decoded), "#cloud-config\n")) + rendered, err := RenderLinuxNodeCustomDataTemplate(template, config) + require.NoError(t, err) + require.Equal(t, rendered, string(decoded)) + var before, after cloudInit + require.NoError(t, yaml.Unmarshal([]byte(original), &before)) + require.NoError(t, yaml.Unmarshal(decoded, &after)) + beforeFiles, err := buildIgnitionTarEntries(before) + require.NoError(t, err) + afterFiles, err := buildIgnitionTarEntries(after) + require.NoError(t, err) + require.Equal(t, beforeFiles, afterFiles) + for _, file := range after.WriteFiles { + require.Empty(t, file.Encoding) + } + require.Less(t, len(payload), len(getBase64EncodedGzippedCustomScriptFromStr(original))) + t.Logf("customData: %d encoded characters", len(payload)) + // Ubuntu custom-cloud overflow is tracked separately in #9790. + if config.ContainerService.Properties.CustomCloudEnv == nil || config.AgentPoolProfile.Distro != datamodel.AKSUbuntuContainerd2204Gen2 { + require.Less(t, len(payload), MaxCustomDataLength) + } +} + func TestCloudInitEncodingLeavesOtherModesUnchanged(t *testing.T) { template, err := parts.Templates.ReadFile(kubernetesNodeCustomDataYaml) require.NoError(t, err)