Skip to content

Commit 9849786

Browse files
authored
fix: accept gzipped base64 payloads in custom_data validation (#238)
* fix: accept gzipped base64 payloads in custom_data validation (#207) * test: add example for gzipped and plaintext custom_data (#207) --------- Co-authored-by: Martin Oehlert <453360+MO2k4@users.noreply.github.com>
1 parent 7685cce commit 9849786

3 files changed

Lines changed: 279 additions & 1 deletion

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
terraform {
2+
required_version = ">= 1.9, < 2.0"
3+
4+
required_providers {
5+
azapi = {
6+
source = "azure/azapi"
7+
version = "~> 2.0"
8+
}
9+
azurerm = {
10+
source = "hashicorp/azurerm"
11+
version = ">= 3.116, < 5.0"
12+
}
13+
random = {
14+
source = "hashicorp/random"
15+
version = "~> 3.7"
16+
}
17+
}
18+
}
19+
20+
# tflint-ignore: terraform_module_provider_declaration, terraform_output_separate, terraform_variable_separate
21+
provider "azurerm" {
22+
features {
23+
resource_group {
24+
prevent_deletion_if_contains_resources = false
25+
}
26+
key_vault {
27+
purge_soft_delete_on_destroy = true
28+
}
29+
}
30+
}
31+
32+
module "naming" {
33+
source = "Azure/naming/azurerm"
34+
version = "0.4.2"
35+
}
36+
37+
module "regions" {
38+
source = "Azure/avm-utl-regions/azurerm"
39+
version = "0.5.0"
40+
41+
availability_zones_filter = true
42+
}
43+
44+
locals {
45+
deployment_region = "canadacentral"
46+
tags = {
47+
scenario = "linux_custom_data_gzip"
48+
}
49+
}
50+
51+
resource "random_integer" "region_index" {
52+
max = length(module.regions.regions_by_name) - 1
53+
min = 0
54+
}
55+
56+
resource "random_integer" "zone_index" {
57+
max = length(module.regions.regions_by_name[local.deployment_region].zones)
58+
min = 1
59+
}
60+
61+
resource "azurerm_resource_group" "this_rg" {
62+
location = local.deployment_region
63+
name = module.naming.resource_group.name_unique
64+
tags = local.tags
65+
}
66+
67+
module "vm_sku" {
68+
source = "Azure/avm-utl-sku-finder/azapi"
69+
version = "0.3.0"
70+
71+
location = azurerm_resource_group.this_rg.location
72+
cache_results = true
73+
vm_filters = {
74+
min_vcpus = 2
75+
max_vcpus = 2
76+
encryption_at_host_supported = false
77+
accelerated_networking_enabled = true
78+
premium_io_supported = true
79+
location_zone = random_integer.zone_index.result
80+
}
81+
82+
depends_on = [random_integer.zone_index]
83+
}
84+
85+
module "natgateway" {
86+
source = "Azure/avm-res-network-natgateway/azurerm"
87+
version = "0.2.1"
88+
89+
location = azurerm_resource_group.this_rg.location
90+
name = module.naming.nat_gateway.name_unique
91+
resource_group_name = azurerm_resource_group.this_rg.name
92+
enable_telemetry = true
93+
public_ips = {
94+
public_ip_1 = {
95+
name = "nat_gw_pip1"
96+
}
97+
}
98+
tags = local.tags
99+
}
100+
101+
module "vnet" {
102+
source = "Azure/avm-res-network-virtualnetwork/azurerm"
103+
version = "=0.8.1"
104+
105+
address_space = ["10.0.0.0/16"]
106+
location = azurerm_resource_group.this_rg.location
107+
resource_group_name = azurerm_resource_group.this_rg.name
108+
name = module.naming.virtual_network.name_unique
109+
subnets = {
110+
vm_subnet_1 = {
111+
name = "${module.naming.subnet.name_unique}-1"
112+
address_prefixes = ["10.0.1.0/24"]
113+
nat_gateway = {
114+
id = module.natgateway.resource_id
115+
}
116+
}
117+
}
118+
tags = local.tags
119+
}
120+
121+
data "azurerm_client_config" "current" {}
122+
123+
module "avm_res_keyvault_vault" {
124+
source = "Azure/avm-res-keyvault-vault/azurerm"
125+
version = "=0.10.0"
126+
127+
location = azurerm_resource_group.this_rg.location
128+
name = "${module.naming.key_vault.name_unique}-cd-gzip"
129+
resource_group_name = azurerm_resource_group.this_rg.name
130+
tenant_id = data.azurerm_client_config.current.tenant_id
131+
network_acls = {
132+
default_action = "Allow"
133+
}
134+
role_assignments = {
135+
deployment_user_secrets = {
136+
role_definition_id_or_name = "Key Vault Secrets Officer"
137+
principal_id = data.azurerm_client_config.current.object_id
138+
}
139+
}
140+
tags = local.tags
141+
wait_for_rbac_before_secret_operations = {
142+
create = "60s"
143+
}
144+
}
145+
146+
# Plain text cloud-init (no gzip) — base64 only
147+
data "cloudinit_config" "plaintext" {
148+
gzip = false
149+
base64_encode = true
150+
151+
part {
152+
content_type = "text/cloud-config"
153+
content = <<-CLOUDINIT
154+
#cloud-config
155+
write_files:
156+
- path: /tmp/hello-plaintext.txt
157+
content: "Hello from plaintext cloud-init"
158+
CLOUDINIT
159+
}
160+
}
161+
162+
# Gzipped cloud-init — this is the scenario that was broken (Issue #207)
163+
data "cloudinit_config" "gzipped" {
164+
gzip = true
165+
base64_encode = true
166+
167+
part {
168+
content_type = "text/cloud-config"
169+
content = <<-CLOUDINIT
170+
#cloud-config
171+
write_files:
172+
- path: /tmp/hello-gzipped.txt
173+
content: "Hello from gzipped cloud-init"
174+
CLOUDINIT
175+
}
176+
}
177+
178+
# VM with plain text (non-gzipped) custom_data
179+
module "vm_plaintext_custom_data" {
180+
source = "../../"
181+
182+
location = azurerm_resource_group.this_rg.location
183+
name = "${module.naming.virtual_machine.name_unique}-plain"
184+
network_interfaces = {
185+
network_interface_1 = {
186+
name = "${module.naming.network_interface.name_unique}-plain"
187+
ip_configurations = {
188+
ip_configuration_1 = {
189+
name = "${module.naming.network_interface.name_unique}-plain-ipconfig1"
190+
private_ip_subnet_resource_id = module.vnet.subnets["vm_subnet_1"].resource_id
191+
}
192+
}
193+
}
194+
}
195+
resource_group_name = azurerm_resource_group.this_rg.name
196+
zone = random_integer.zone_index.result
197+
account_credentials = {
198+
key_vault_configuration = {
199+
resource_id = module.avm_res_keyvault_vault.resource_id
200+
}
201+
}
202+
custom_data = data.cloudinit_config.plaintext.rendered
203+
enable_telemetry = var.enable_telemetry
204+
encryption_at_host_enabled = false
205+
os_type = "Linux"
206+
sku_size = module.vm_sku.sku
207+
source_image_reference = {
208+
publisher = "Canonical"
209+
offer = "0001-com-ubuntu-server-focal"
210+
sku = "20_04-lts-gen2"
211+
version = "latest"
212+
}
213+
tags = local.tags
214+
215+
depends_on = [
216+
module.avm_res_keyvault_vault
217+
]
218+
}
219+
220+
# VM with gzipped custom_data — previously rejected by validation (Issue #207)
221+
module "vm_gzipped_custom_data" {
222+
source = "../../"
223+
224+
location = azurerm_resource_group.this_rg.location
225+
name = "${module.naming.virtual_machine.name_unique}-gzip"
226+
network_interfaces = {
227+
network_interface_1 = {
228+
name = "${module.naming.network_interface.name_unique}-gzip"
229+
ip_configurations = {
230+
ip_configuration_1 = {
231+
name = "${module.naming.network_interface.name_unique}-gzip-ipconfig1"
232+
private_ip_subnet_resource_id = module.vnet.subnets["vm_subnet_1"].resource_id
233+
}
234+
}
235+
}
236+
}
237+
resource_group_name = azurerm_resource_group.this_rg.name
238+
zone = random_integer.zone_index.result
239+
account_credentials = {
240+
key_vault_configuration = {
241+
resource_id = module.avm_res_keyvault_vault.resource_id
242+
}
243+
}
244+
custom_data = data.cloudinit_config.gzipped.rendered
245+
enable_telemetry = var.enable_telemetry
246+
encryption_at_host_enabled = false
247+
os_type = "Linux"
248+
sku_size = module.vm_sku.sku
249+
source_image_reference = {
250+
publisher = "Canonical"
251+
offer = "0001-com-ubuntu-server-focal"
252+
sku = "20_04-lts-gen2"
253+
version = "latest"
254+
}
255+
tags = local.tags
256+
257+
depends_on = [
258+
module.avm_res_keyvault_vault
259+
]
260+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# tflint-ignore: terraform_variable_separate, terraform_standard_module_structure
2+
variable "enable_telemetry" {
3+
type = bool
4+
default = true
5+
description = <<DESCRIPTION
6+
This variable controls whether or not telemetry is enabled for the module.
7+
For more information see https://aka.ms/avm/telemetryinfo.
8+
If it is set to false, then no telemetry will be collected.
9+
DESCRIPTION
10+
}

variables.tf

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,15 @@ variable "custom_data" {
441441
description = "(Optional) The Base64 encoded Custom Data for building this virtual machine. Changing this forces a new resource to be created"
442442

443443
validation {
444-
condition = var.custom_data == null ? true : can(base64decode(var.custom_data))
444+
# Gzipped payloads (e.g. from cloudinit_config with gzip=true) are valid
445+
# base64 but base64decode() rejects them because the decoded bytes are not
446+
# valid UTF-8. Gzip streams always start with magic bytes 0x1f 0x8b 0x08,
447+
# which base64-encode to the prefix "H4sI". We check for that prefix as a
448+
# fallback to accept gzipped cloud-init data.
449+
condition = var.custom_data == null ? true : (
450+
can(base64decode(var.custom_data)) ||
451+
startswith(var.custom_data, "H4sI")
452+
)
445453
error_message = "The `custom_data` must be either `null` or a valid Base64-Encoded string."
446454
}
447455
}

0 commit comments

Comments
 (0)