diff --git a/hotfix/hotfix_generate.py b/hotfix/hotfix_generate.py index 432c96eb0ae..04690279b64 100644 --- a/hotfix/hotfix_generate.py +++ b/hotfix/hotfix_generate.py @@ -421,7 +421,10 @@ def parse_write_files_blocks(traditional_lines): if in_block: current_block.append(line) - match = re.search(r'GetVariableProperty\s+"cloudInitData"\s+"(\w+)"', stripped) + match = re.search( + r'GetVariableProperty\s+"cloudInit(?:File)?Data"\s+"(\w+)"', + stripped, + ) if match: current_varkeys.add(match.group(1)) diff --git a/hotfix/hotfix_generate_test.py b/hotfix/hotfix_generate_test.py index 7626a0863c7..975da7b919a 100644 --- a/hotfix/hotfix_generate_test.py +++ b/hotfix/hotfix_generate_test.py @@ -65,7 +65,7 @@ def test_production_renderer_omits_empty_platform_payloads(self): cases = [ ("custom cloud only", hotfix_generate.build_hotfix_template( {"initAKSCloud"}, lines[outer_else + 1:end] - ), set()), + ), {"ubuntu", "mariner"}), ("missing", "#cloud-config\n", set()), ("null", "write_files:\n", set()), ("empty list", "write_files: []\n", set()), @@ -93,7 +93,7 @@ def test_production_renderer_omits_empty_platform_payloads(self): 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": + if name == "missing": probe = Path(directory) / "main.go" probe.write_text('package main\nimport "embed"\n' '//go:embed generated\nvar files embed.FS\n' @@ -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("{{GetCloudInitFileEncodingLine}}", template) + self.assertIn("| GetCloudInitFileContent}}", template) self.assertNotIn("{{if", template) def test_mariner_package_update_is_not_embedded_in_custom_data(self): @@ -244,6 +245,22 @@ def test_build_hotfix_template_selects_only_requested_blocks(self): self.assertIn("provisionSource", rendered) self.assertNotIn("provisionSourceUbuntu", rendered) + def test_parse_write_files_blocks_accepts_legacy_and_current_maps(self): + for variable_map in ("cloudInitData", "cloudInitFileData"): + with self.subTest(variable_map=variable_map): + source = TRADITIONAL_TEMPLATE.replace("cloudInitData", variable_map) + blocks = hotfix_generate.parse_write_files_blocks( + source.splitlines(keepends=True) + ) + self.assertEqual(2, len(blocks)) + self.assertEqual({"provisionSource"}, blocks[0][0]) + self.assertIn("provisionSourceUbuntu", blocks[1][0]) + selected = hotfix_generate.build_hotfix_template( + {"provisionSource"}, source.splitlines(keepends=True) + ) + self.assertIn(f'"{variable_map}" "provisionSource"', selected) + self.assertNotIn("provisionSourceUbuntu", selected) + def test_detect_changed_varkeys_expands_distro_group(self): with tempfile.TemporaryDirectory() as temp_dir: artifacts = Path(temp_dir) diff --git a/parts/linux/cloud-init/artifacts/README.MD b/parts/linux/cloud-init/artifacts/README.MD index 9080837885c..1e3c76e64a5 100644 --- a/parts/linux/cloud-init/artifacts/README.MD +++ b/parts/linux/cloud-init/artifacts/README.MD @@ -3,6 +3,19 @@ For tutorial for the descriptions of components.json, please refer to this [doc] For CA refresh logging and Linux opt-in certificate diagnostics, see [CA refresh diagnostics](../../../../doc/ca-refresh.md). +## CustomData file encoding + +Traditional Linux CustomData is gzip-compressed once around the whole +cloud-config document. Its `write_files` content uses YAML double-quoted +scalars to preserve script bytes, including whitespace and Unicode line +separators. Flatcar, ACL, and scriptless delivery retain per-file gzip encoding. +This changes packaging only; no new VHD binary or guest-side checksum gate is +required. + +Template entries use `cloudInitFileData` with `GetCloudInitFileEncodingLine` +and `GetCloudInitFileContent`. Legacy `cloudInitData` lookups still return +gzip/base64 content, including when mixed with new entries in a hotfix template. + ## SSH-disable telemetry When `DISABLE_SSH=true` and `basePrep` runs, `disableSSH` emits structured diff --git a/parts/linux/cloud-init/nodecustomdata.yml b/parts/linux/cloud-init/nodecustomdata.yml index 200eeb0ce5f..f00fe886a93 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 + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "initAKSCloud"}} + content: {{GetVariableProperty "cloudInitFileData" "initAKSCloud" | GetCloudInitFileContent}} {{end}} {{- else }} - path: {{GetCSEHelpersScriptFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSource"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionSource" | GetCloudInitFileContent}} {{if IsACL }} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceACL"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionSourceACL" | GetCloudInitFileContent}} {{- else if IsAzlOSGuard}} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceAzlOSGuard"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionSourceAzlOSGuard" | GetCloudInitFileContent}} {{- else if IsMariner}} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceMariner"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionSourceMariner" | GetCloudInitFileContent}} {{- else if IsFlatcar }} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceFlatcar"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionSourceFlatcar" | GetCloudInitFileContent}} {{- else }} - path: {{GetCSEHelpersScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionSourceUbuntu"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionSourceUbuntu" | GetCloudInitFileContent}} {{end}} {{ if not IsCustomImage -}} - path: /opt/azure/containers/provision_start.sh permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionStartScript"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionStartScript" | GetCloudInitFileContent}} {{- end }} - path: /opt/azure/containers/provision.sh permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionScript"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionScript" | GetCloudInitFileContent}} - path: {{GetCSEInstallScriptFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstalls"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionInstalls" | GetCloudInitFileContent}} {{if IsACL }} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsACL"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionInstallsACL" | GetCloudInitFileContent}} {{- else if IsAzlOSGuard}} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsAzlOSGuard"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionInstallsAzlOSGuard" | GetCloudInitFileContent}} {{- else if IsMariner}} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsMariner"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionInstallsMariner" | GetCloudInitFileContent}} {{- else if IsFlatcar }} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsFlatcar"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionInstallsFlatcar" | GetCloudInitFileContent}} {{- else }} - path: {{GetCSEInstallScriptDistroFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionInstallsUbuntu"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionInstallsUbuntu" | GetCloudInitFileContent}} {{end}} - path: {{GetCSEConfigScriptFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigs"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionConfigs" | GetCloudInitFileContent}} - path: {{GetCSEConfigGPUScriptFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsGPU"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionConfigsGPU" | GetCloudInitFileContent}} - path: {{GetCSEConfigLocalDNSScriptFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsLocalDNS"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionConfigsLocalDNS" | GetCloudInitFileContent}} - path: {{GetCSEConfigKubeletScriptFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsKubelet"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionConfigsKubelet" | GetCloudInitFileContent}} - path: {{GetCSEConfigNetworkScriptFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsNetwork"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionConfigsNetwork" | GetCloudInitFileContent}} - path: {{GetCSEConfigAddonsScriptFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsAddons"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionConfigsAddons" | GetCloudInitFileContent}} - path: {{GetCSEConfigChronyScriptFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "provisionConfigsChrony"}} + content: {{GetVariableProperty "cloudInitFileData" "provisionConfigsChrony" | GetCloudInitFileContent}} {{if IsAKSCustomCloud}} - path: {{GetInitAKSCloudFilepath}} permissions: "0744" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "initAKSCloud"}} + content: {{GetVariableProperty "cloudInitFileData" "initAKSCloud" | GetCloudInitFileContent}} {{end}} - path: /etc/systemd/system/reconcile-private-hosts.service permissions: "0644" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "reconcilePrivateHostsService"}} + content: {{GetVariableProperty "cloudInitFileData" "reconcilePrivateHostsService" | GetCloudInitFileContent}} - path: /etc/systemd/system/kubelet.service permissions: "0600" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "kubeletSystemdService"}} + content: {{GetVariableProperty "cloudInitFileData" "kubeletSystemdService" | GetCloudInitFileContent}} - path: /opt/azure-network/configure-azure-network.sh permissions: "0755" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "configureAzureNetworkScript"}} + content: {{GetVariableProperty "cloudInitFileData" "configureAzureNetworkScript" | GetCloudInitFileContent}} - path: /etc/udev/rules.d/99-azure-network.rules permissions: "0644" - encoding: gzip + {{GetCloudInitFileEncodingLine}} owner: root - content: !!binary | - {{GetVariableProperty "cloudInitData" "azureNetworkUdevRule"}} + content: {{GetVariableProperty "cloudInitFileData" "azureNetworkUdevRule" | GetCloudInitFileContent}} {{- end }} diff --git a/pkg/agent/baker.go b/pkg/agent/baker.go index 91eacc7f9fb..f774917adbb 100644 --- a/pkg/agent/baker.go +++ b/pkg/agent/baker.go @@ -17,6 +17,7 @@ import ( "strconv" "strings" "text/template" + "unicode/utf8" "github.com/Azure/agentbaker/parts" "github.com/Azure/agentbaker/pkg/agent/datamodel" @@ -346,17 +347,19 @@ 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) } - - return fmt.Sprintf("{\"customData\": \"%s\"}", str) + str, err := RenderLinuxNodeCustomDataTemplate(b, config) + if err != nil { + panic(err) + } + customData, err := json.Marshal(map[string]string{"customData": str}) + if err != nil { + panic(err) + } + return string(customData) } const ( @@ -694,10 +697,46 @@ 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() +} + +func getCloudInitFileEncodingLine(config *datamodel.NodeBootstrappingConfiguration) string { + if usePlainCloudInit(config) { + return "" + } + return "encoding: " + encodingGZIP +} + +func getCloudInitFileContent(config *datamodel.NodeBootstrappingConfiguration, content string) (string, error) { + if usePlainCloudInit(config) { + if !utf8.ValidString(content) { + return "", fmt.Errorf("cloud-init file content contains invalid UTF-8") + } + // YAML quoting preserves Unicode line breaks that JSON leaves unescaped. + encoded, err := yaml.Marshal(&yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Style: yaml.DoubleQuotedStyle, + Value: content, + }) + return strings.TrimSuffix(string(encoded), "\n"), 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["GetCloudInitFileEncodingLine"] = func() string { + return getCloudInitFileEncodingLine(config) + } + funcMap["GetCloudInitFileContent"] = func(content string) (string, error) { + return getCloudInitFileContent(config, content) + } + funcMap["GetParameter"] = func(s string) interface{} { if v, ok := params[s].(paramsMap); ok && v != nil { if v["value"] == nil { @@ -732,19 +771,30 @@ func getBakerFuncMap(config *datamodel.NodeBootstrappingConfiguration, params pa } funcMap["GetVariableProperty"] = func(v, p string) interface{} { - if v, ok := variables[v].(paramsMap); ok && v != nil { - if v[p] == nil { - // return empty string so we don't get from go template - return "" - } - return v[p] - } - return "" + return getVariableProperty(variables, v, p) } return funcMap } +func getVariableProperty(variables paramsMap, variable, name string) interface{} { + if values, ok := variables[variable].(paramsMap); ok && values[name] != nil { + return values[name] + } + if variable != "cloudInitData" { + return "" + } + files, ok := variables["cloudInitFileData"].(paramsMap) + if !ok { + return "" + } + content, ok := files[name].(string) + if !ok { + return "" + } + return getBase64EncodedGzippedCustomScriptFromStr(content) +} + /* normalizeResourceGroupNameForLabel normalizes resource group name to be used as a label, similar to what the ARM template used to do. 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..1b45060eca8 --- /dev/null +++ b/pkg/agent/cloud_init_encoding_test.go @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +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) { + input, err := parts.Templates.ReadFile(kubernetesNodeCustomDataYaml) + require.NoError(t, err) + for _, distro := range []datamodel.Distro{ + datamodel.AKSUbuntuContainerd2204Gen2, + datamodel.AKSUbuntuContainerd2404Gen2, + 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"} + } + assertTraditionalCloudInitPayload(t, input, config) + }) + } + } + } + } +} + +func assertTraditionalCloudInitPayload(t *testing.T, input []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(input, 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)) + var document struct { + WriteFiles []map[string]any `yaml:"write_files"` + } + require.NoError(t, yaml.Unmarshal(decoded, &document)) + for _, file := range document.WriteFiles { + _, hasEncoding := file["encoding"] + require.False(t, hasEncoding, "plain files must omit encoding rather than use an empty value") + } + beforeFiles, err := buildIgnitionTarEntries(before) + require.NoError(t, err) + afterFiles, err := buildIgnitionTarEntries(after) + require.NoError(t, err) + require.Equal(t, beforeFiles, afterFiles) + beforePayload := getBase64EncodedGzippedCustomScriptFromStr(original) + require.Less(t, len(payload), len(beforePayload)) + t.Logf("customData: before=%d after=%d limit=%d", len(beforePayload), len(payload), MaxCustomDataLength) + // Ubuntu custom-cloud overflow is tracked separately in #9790. + isUbuntu := config.AgentPoolProfile.Distro == datamodel.AKSUbuntuContainerd2204Gen2 || + config.AgentPoolProfile.Distro == datamodel.AKSUbuntuContainerd2404Gen2 + if config.ContainerService.Properties.CustomCloudEnv == nil || !isUbuntu { + require.Less(t, len(payload), MaxCustomDataLength) + } +} + +func TestCloudInitEncodingLeavesOtherModesUnchanged(t *testing.T) { + input, 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(input, config) + require.NoError(t, err) + require.Equal(t, original, rendered) + require.Equal(t, original, getCustomDataFromJSON(generator.getLinuxNodeCustomDataJSONObject(config))) + }) + } + } +} + +func TestTraditionalCloudInitCustomImages(t *testing.T) { + input, err := parts.Templates.ReadFile(kubernetesNodeCustomDataYaml) + require.NoError(t, err) + for _, distro := range []datamodel.Distro{ + datamodel.AKSUbuntuContainerd2004CVMGen2, + datamodel.CustomizedImage, + datamodel.CustomizedImageLinuxGuard, + datamodel.CustomizedImageTrustedLaunch, + } { + t.Run(string(distro), func(t *testing.T) { + assertTraditionalCloudInitPayload(t, input, newNodeCustomDataRenderConfig(distro)) + }) + } +} + +func TestCloudInitLegacyAndQuotedEntries(t *testing.T) { + input := []byte(`#cloud-config +write_files: +- path: /opt/legacy.sh + permissions: "0744" + owner: root + encoding: gzip + content: !!binary | + {{GetVariableProperty "cloudInitData" "provisionSource"}} +- path: /opt/quoted.sh + permissions: "0744" + owner: root + {{GetCloudInitFileEncodingLine}} + content: {{GetVariableProperty "cloudInitFileData" "provisionSource" | GetCloudInitFileContent}} +`) + for _, distro := range []datamodel.Distro{ + datamodel.AKSUbuntuContainerd2204Gen2, + datamodel.AKSAzureLinuxV3Gen2, + datamodel.AKSFlatcarGen2, + datamodel.AKSACLGen2TL, + } { + for _, scriptless := range []bool{false, true} { + t.Run(fmt.Sprintf("%s/scriptless=%t", distro, scriptless), func(t *testing.T) { + config := newNodeCustomDataRenderConfig(distro) + config.EnableScriptlessCSECmd = scriptless + rendered, err := RenderLinuxNodeCustomDataTemplate(input, config) + require.NoError(t, err) + var cloud cloudInit + require.NoError(t, yaml.Unmarshal([]byte(rendered), &cloud)) + files, err := buildIgnitionTarEntries(cloud) + require.NoError(t, err) + require.Len(t, files, 2) + require.Equal(t, []byte(getCustomScript(kubernetesCSEHelpersScript, config)), files[0].contents) + require.Equal(t, files[0].contents, files[1].contents) + require.Equal(t, encodingGZIP, cloud.WriteFiles[0].Encoding) + if usePlainCloudInit(config) { + require.Empty(t, cloud.WriteFiles[1].Encoding) + } else { + require.Equal(t, encodingGZIP, cloud.WriteFiles[1].Encoding) + } + }) + } + } +} + +func legacyCloudInitFuncMap(config *datamodel.NodeBootstrappingConfiguration) template.FuncMap { + funcs := getBakerFuncMap(config, getParameters(config), getCustomDataVariablesWithEncoder(config, false)) + funcs["GetCloudInitFileEncodingLine"] = func() string { return "encoding: " + 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 +write_files: +- path: /opt/example + {{GetCloudInitFileEncodingLine}} + content: {{.Name | GetCloudInitFileContent}} +`) + for _, content := range []string{ + "", + " \tleading and trailing \t ", + "tab\tquote\"backslash\\", + "trailing newline\n\n", + "\u00e9\r\n", + "next\u0085line\u2028paragraph\u2029end", + "nul\x00escape\x1b", + "echo \"$HOME\"; printf '%s\\n' 'a: # & < >'\n", + strings.Repeat("long line: # \"\\ ", 1024), + } { + 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 TestPlainCloudInitFileContentRejectsInvalidUTF8(t *testing.T) { + input := []byte("#cloud-config\nwrite_files:\n- path: /opt/example\n content: {{.Name | GetCloudInitFileContent}}\n") + config := newNodeCustomDataRenderConfig(datamodel.AKSUbuntuContainerd2204Gen2) + config.AgentPoolProfile.Name = string([]byte{'a', 0xff, 'b'}) + output, err := RenderLinuxNodeCustomDataTemplate(input, config) + require.ErrorContains(t, err, "invalid UTF-8") + require.Empty(t, output) +} 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..afc46054701 100644 --- a/pkg/agent/variables.go +++ b/pkg/agent/variables.go @@ -12,73 +12,88 @@ 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) + } + + cloudInitFiles["cloudInitFileData"] = cloudInitData + if plain { + // Legacy templates still read gzip data through cloudInitData. + // Compress those lookups on demand instead of encoding every file twice. + cloudInitFiles["cloudInitData"] = paramsMap{} } return cloudInitFiles