diff --git a/integration/v4_to_v5/integration_test.go b/integration/v4_to_v5/integration_test.go index 187a9fa..b72ef46 100644 --- a/integration/v4_to_v5/integration_test.go +++ b/integration/v4_to_v5/integration_test.go @@ -11,13 +11,14 @@ import ( _ "github.com/cloudflare/tf-migrate/internal/resources/account_member" _ "github.com/cloudflare/tf-migrate/internal/resources/api_token" _ "github.com/cloudflare/tf-migrate/internal/resources/dns_record" + _ "github.com/cloudflare/tf-migrate/internal/resources/load_balancer_monitor" _ "github.com/cloudflare/tf-migrate/internal/resources/logpull_retention" _ "github.com/cloudflare/tf-migrate/internal/resources/r2_bucket" _ "github.com/cloudflare/tf-migrate/internal/resources/workers_kv" _ "github.com/cloudflare/tf-migrate/internal/resources/workers_kv_namespace" _ "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_access_service_token" - _ "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_gateway_policy" _ "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_dlp_custom_profile" + _ "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_gateway_policy" _ "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_list" _ "github.com/cloudflare/tf-migrate/internal/resources/zone_dnssec" ) @@ -48,6 +49,7 @@ func TestV4ToV5Migration(t *testing.T) { "account_member", "api_token", "dns_record", + "load_balancer_monitor", "logpull_retention", "r2_bucket", "workers_kv", diff --git a/integration/v4_to_v5/testdata/load_balancer_monitor/expected/load_balancer_monitor.tf b/integration/v4_to_v5/testdata/load_balancer_monitor/expected/load_balancer_monitor.tf new file mode 100644 index 0000000..1c1433a --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_monitor/expected/load_balancer_monitor.tf @@ -0,0 +1,23 @@ +resource "cloudflare_load_balancer_monitor" "test" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "https" + description = "Test HTTPS monitor" + method = "GET" + path = "/health" + interval = 30 + retries = 3 + timeout = 10 + expected_codes = "2xx" + expected_body = "healthy" + allow_insecure = true + + + header = { + "Host" = ["api.example.com"] + "Authorization" = ["Bearer token123"] + } +} + +resource "cloudflare_load_balancer_monitor" "minimal" { + account_id = "f037e56e89293a057740de681ac9abbe" +} diff --git a/integration/v4_to_v5/testdata/load_balancer_monitor/expected/terraform.tfstate b/integration/v4_to_v5/testdata/load_balancer_monitor/expected/terraform.tfstate new file mode 100644 index 0000000..151879d --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_monitor/expected/terraform.tfstate @@ -0,0 +1,61 @@ +{ + "version": 4, + "terraform_version": "1.0.0", + "serial": 1, + "lineage": "test", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "cloudflare_load_balancer_monitor", + "name": "test", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "abc123def456", + "account_id": "f037e56e89293a057740de681ac9abbe", + "type": "https", + "description": "Test HTTPS monitor", + "method": "GET", + "path": "/health", + "interval": 30.0, + "retries": 3.0, + "timeout": 10.0, + "expected_codes": "2xx", + "expected_body": "healthy", + "allow_insecure": true, + "header": { + "Host": ["api.example.com"], + "Authorization": ["Bearer token123"] + }, + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + } + } + ] + }, + { + "mode": "managed", + "type": "cloudflare_load_balancer_monitor", + "name": "minimal", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "minimal123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "type": "http", + "interval": 60.0, + "retries": 2.0, + "timeout": 5.0, + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + } + } + ] + } + ] +} diff --git a/integration/v4_to_v5/testdata/load_balancer_monitor/input/load_balancer_monitor.tf b/integration/v4_to_v5/testdata/load_balancer_monitor/input/load_balancer_monitor.tf new file mode 100644 index 0000000..0941a39 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_monitor/input/load_balancer_monitor.tf @@ -0,0 +1,27 @@ +resource "cloudflare_load_balancer_monitor" "test" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "https" + description = "Test HTTPS monitor" + method = "GET" + path = "/health" + interval = 30 + retries = 3 + timeout = 10 + expected_codes = "2xx" + expected_body = "healthy" + allow_insecure = true + + header { + header = "Host" + values = ["api.example.com"] + } + + header { + header = "Authorization" + values = ["Bearer token123"] + } +} + +resource "cloudflare_load_balancer_monitor" "minimal" { + account_id = "f037e56e89293a057740de681ac9abbe" +} diff --git a/integration/v4_to_v5/testdata/load_balancer_monitor/input/terraform.tfstate b/integration/v4_to_v5/testdata/load_balancer_monitor/input/terraform.tfstate new file mode 100644 index 0000000..f1325a4 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_monitor/input/terraform.tfstate @@ -0,0 +1,71 @@ +{ + "version": 4, + "terraform_version": "1.0.0", + "serial": 1, + "lineage": "test", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "cloudflare_load_balancer_monitor", + "name": "test", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "id": "abc123def456", + "account_id": "f037e56e89293a057740de681ac9abbe", + "type": "https", + "description": "Test HTTPS monitor", + "method": "GET", + "path": "/health", + "interval": 30, + "retries": 3, + "timeout": 10, + "expected_codes": "2xx", + "expected_body": "healthy", + "allow_insecure": true, + "consecutive_up": 0, + "consecutive_down": 0, + "header": [ + { + "header": "Host", + "values": ["api.example.com"] + }, + { + "header": "Authorization", + "values": ["Bearer token123"] + } + ], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + } + } + ] + }, + { + "mode": "managed", + "type": "cloudflare_load_balancer_monitor", + "name": "minimal", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "id": "minimal123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "type": "http", + "interval": 60, + "retries": 2, + "timeout": 5, + "consecutive_up": 0, + "consecutive_down": 0, + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + } + } + ] + } + ] +} diff --git a/internal/registry/registry.go b/internal/registry/registry.go index dd328fe..4b8e441 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -4,6 +4,7 @@ import ( "github.com/cloudflare/tf-migrate/internal/resources/account_member" "github.com/cloudflare/tf-migrate/internal/resources/api_token" "github.com/cloudflare/tf-migrate/internal/resources/dns_record" + "github.com/cloudflare/tf-migrate/internal/resources/load_balancer_monitor" "github.com/cloudflare/tf-migrate/internal/resources/logpull_retention" "github.com/cloudflare/tf-migrate/internal/resources/r2_bucket" "github.com/cloudflare/tf-migrate/internal/resources/workers_kv" @@ -22,7 +23,7 @@ func RegisterAllMigrations() { account_member.NewV4ToV5Migrator() api_token.NewV4ToV5Migrator() dns_record.NewV4ToV5Migrator() - zone_dnssec.NewV4ToV5Migrator() + load_balancer_monitor.NewV4ToV5Migrator() logpull_retention.NewV4ToV5Migrator() r2_bucket.NewV4ToV5Migrator() workers_kv.NewV4ToV5Migrator() @@ -31,4 +32,5 @@ func RegisterAllMigrations() { zero_trust_dlp_custom_profile.NewV4ToV5Migrator() zero_trust_gateway_policy.NewV4ToV5Migrator() zero_trust_list.NewV4ToV5Migrator() + zone_dnssec.NewV4ToV5Migrator() } diff --git a/internal/resources/load_balancer_monitor/v4_to_v5.go b/internal/resources/load_balancer_monitor/v4_to_v5.go new file mode 100644 index 0000000..654a7a6 --- /dev/null +++ b/internal/resources/load_balancer_monitor/v4_to_v5.go @@ -0,0 +1,176 @@ +package load_balancer_monitor + +import ( + "encoding/json" + + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/cloudflare/tf-migrate/internal" + "github.com/cloudflare/tf-migrate/internal/transform" + tfhcl "github.com/cloudflare/tf-migrate/internal/transform/hcl" + "github.com/cloudflare/tf-migrate/internal/transform/state" +) + +// V4ToV5Migrator handles migration of load balancer monitor resources from v4 to v5 +type V4ToV5Migrator struct{} + +func NewV4ToV5Migrator() transform.ResourceTransformer { + migrator := &V4ToV5Migrator{} + internal.RegisterMigrator("cloudflare_load_balancer_monitor", "v4", "v5", migrator) + return migrator +} + +func (m *V4ToV5Migrator) GetResourceType() string { + return "cloudflare_load_balancer_monitor" +} + +func (m *V4ToV5Migrator) CanHandle(resourceType string) bool { + return resourceType == "cloudflare_load_balancer_monitor" +} + +// Preprocess is not needed - we do everything in TransformConfig +func (m *V4ToV5Migrator) Preprocess(content string) string { + return content +} + +// TransformConfig transforms the HCL configuration from v4 to v5 +func (m *V4ToV5Migrator) TransformConfig(ctx *transform.Context, block *hclwrite.Block) (*transform.TransformResult, error) { + body := block.Body() + + // Convert header blocks to map attribute + m.transformHeaderBlocks(body) + + return &transform.TransformResult{ + Blocks: []*hclwrite.Block{block}, + RemoveOriginal: false, + }, nil +} + +// transformHeaderBlocks converts multiple v4 header blocks to a v5 map attribute +func (m *V4ToV5Migrator) transformHeaderBlocks(body *hclwrite.Body) { + headerBlocks := tfhcl.FindBlocksByType(body, "header") + if len(headerBlocks) == 0 { + return + } + + // Collect header entries as ObjectAttrTokens + var headerAttrs []hclwrite.ObjectAttrTokens + + for _, headerBlock := range headerBlocks { + headerBody := headerBlock.Body() + + // Get the "header" attribute (the header name) + headerAttr := headerBody.GetAttribute("header") + if headerAttr == nil { + continue + } + + // Get the "values" attribute (the header values) + valuesAttr := headerBody.GetAttribute("values") + if valuesAttr == nil { + continue + } + + headerAttrs = append(headerAttrs, hclwrite.ObjectAttrTokens{ + Name: headerAttr.Expr().BuildTokens(nil), + Value: valuesAttr.Expr().BuildTokens(nil), + }) + } + + if len(headerAttrs) == 0 { + return + } + + // Build the header map using TokensForObject + headerMapTokens := hclwrite.TokensForObject(headerAttrs) + + // Set the header attribute with the map + body.SetAttributeRaw("header", headerMapTokens) + + // Remove the header blocks + tfhcl.RemoveBlocksByType(body, "header") +} + +// TransformState transforms the state JSON from v4 to v5 +func (m *V4ToV5Migrator) TransformState(ctx *transform.Context, instance gjson.Result, resourcePath string) (string, error) { + result := instance.String() + attrs := instance.Get("attributes") + + if !attrs.Exists() { + return result, nil + } + + // Convert numeric fields from int to float64 + // Fields that are computed_optional should keep zero values + // Fields that are optional-only should remove zero values + computedOptionalFields := []string{ + "interval", + "retries", + "timeout", + } + + optionalOnlyFields := []string{ + "port", + "consecutive_down", + "consecutive_up", + } + + // Convert computed_optional fields (keep zero values) + for _, field := range computedOptionalFields { + if value := attrs.Get(field); value.Exists() { + floatVal := state.ConvertToFloat64(value) + result, _ = sjson.Set(result, "attributes."+field, floatVal) + } + } + + // Convert optional-only fields (remove zero values) + for _, field := range optionalOnlyFields { + if value := attrs.Get(field); value.Exists() { + floatVal := state.ConvertToFloat64(value) + // Only set if non-zero + if floatVal != 0.0 && floatVal != 0 { + result, _ = sjson.Set(result, "attributes."+field, floatVal) + } else { + // Remove zero values for optional-only fields + result, _ = sjson.Delete(result, "attributes."+field) + } + } + } + + // Transform header field from array-of-objects to map-of-arrays + if header := attrs.Get("header"); header.Exists() && header.IsArray() { + headerMap := make(map[string][]string) + + header.ForEach(func(_, value gjson.Result) bool { + headerName := value.Get("header").String() + values := value.Get("values") + + if headerName != "" && values.Exists() { + var valuesList []string + if values.IsArray() { + values.ForEach(func(_, v gjson.Result) bool { + valuesList = append(valuesList, v.String()) + return true + }) + } + headerMap[headerName] = valuesList + } + return true + }) + + if len(headerMap) > 0 { + headerJSON, _ := json.Marshal(headerMap) + result, _ = sjson.SetRaw(result, "attributes.header", string(headerJSON)) + } else { + // Remove empty header field + result, _ = sjson.Delete(result, "attributes.header") + } + } + + // Set schema_version to 0 for v5 + result, _ = sjson.Set(result, "schema_version", 0) + + return result, nil +} diff --git a/internal/resources/load_balancer_monitor/v4_to_v5_test.go b/internal/resources/load_balancer_monitor/v4_to_v5_test.go new file mode 100644 index 0000000..1c4cffc --- /dev/null +++ b/internal/resources/load_balancer_monitor/v4_to_v5_test.go @@ -0,0 +1,499 @@ +package load_balancer_monitor + +import ( + "testing" + + "github.com/cloudflare/tf-migrate/internal/testhelpers" +) + +func TestGetResourceType(t *testing.T) { + migrator := NewV4ToV5Migrator() + expected := "cloudflare_load_balancer_monitor" + if got := migrator.GetResourceType(); got != expected { + t.Errorf("GetResourceType() = %v, want %v", got, expected) + } +} + +func TestCanHandle(t *testing.T) { + migrator := NewV4ToV5Migrator() + + tests := []struct { + name string + resourceType string + want bool + }{ + {"handles correct type", "cloudflare_load_balancer_monitor", true}, + {"rejects other types", "cloudflare_load_balancer", false}, + {"rejects empty string", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := migrator.CanHandle(tt.resourceType); got != tt.want { + t.Errorf("CanHandle(%q) = %v, want %v", tt.resourceType, got, tt.want) + } + }) + } +} + +func TestPreprocess(t *testing.T) { + migrator := NewV4ToV5Migrator() + + tests := []struct { + name string + input string + expected string + }{ + { + name: "no header blocks - unchanged", + input: `resource "cloudflare_load_balancer_monitor" "test" { account_id = "abc" }`, + expected: `resource "cloudflare_load_balancer_monitor" "test" { account_id = "abc" }`, + }, + { + name: "empty content", + input: "", + expected: "", + }, + { + name: "content without load_balancer_monitor resources", + input: `resource "cloudflare_other" "test" { + header { + header = "Host" + values = ["example.com"] + } +}`, + expected: `resource "cloudflare_other" "test" { + header { + header = "Host" + values = ["example.com"] + } +}`, + }, + { + name: "header block with malformed header name - unchanged", + input: `resource "cloudflare_load_balancer_monitor" "test" { + account_id = "abc" + header { + badfield = "Host" + values = ["example.com"] + } +}`, + expected: `resource "cloudflare_load_balancer_monitor" "test" { + account_id = "abc" + header { + badfield = "Host" + values = ["example.com"] + } +}`, + }, + { + name: "header block with malformed values - unchanged", + input: `resource "cloudflare_load_balancer_monitor" "test" { + account_id = "abc" + header { + header = "Host" + badfield = ["example.com"] + } +}`, + expected: `resource "cloudflare_load_balancer_monitor" "test" { + account_id = "abc" + header { + header = "Host" + badfield = ["example.com"] + } +}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := migrator.Preprocess(tt.input) + if got != tt.expected { + t.Errorf("Preprocess() = %q, want %q", got, tt.expected) + } + }) + } +} + +func TestV4ToV5Transformation(t *testing.T) { + migrator := NewV4ToV5Migrator() + + // Test configuration transformations + t.Run("ConfigTransformation", func(t *testing.T) { + tests := []testhelpers.ConfigTestCase{ + { + Name: "minimal monitor - no transformation needed", + Input: ` +resource "cloudflare_load_balancer_monitor" "minimal" { + account_id = "f037e56e89293a057740de681ac9abbe" +}`, + Expected: `resource "cloudflare_load_balancer_monitor" "minimal" { + account_id = "f037e56e89293a057740de681ac9abbe" +}`, + }, + { + Name: "HTTP monitor with all fields", + Input: ` +resource "cloudflare_load_balancer_monitor" "http" { + account_id = "f037e56e89293a057740de681ac9abbe" + description = "Production HTTP monitor" + type = "http" + method = "GET" + path = "/health" + interval = 30 + retries = 3 + timeout = 10 + expected_codes = "2xx" + expected_body = "healthy" + follow_redirects = true +}`, + Expected: `resource "cloudflare_load_balancer_monitor" "http" { + account_id = "f037e56e89293a057740de681ac9abbe" + description = "Production HTTP monitor" + type = "http" + method = "GET" + path = "/health" + interval = 30 + retries = 3 + timeout = 10 + expected_codes = "2xx" + expected_body = "healthy" + follow_redirects = true +}`, + }, + { + Name: "monitor with single header block", + Input: ` +resource "cloudflare_load_balancer_monitor" "with_header" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "http" + path = "/health" + + header { + header = "Host" + values = ["example.com"] + } +}`, + Expected: `resource "cloudflare_load_balancer_monitor" "with_header" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "http" + path = "/health" + + header = { + "Host" = ["example.com"] + } +}`, + }, + { + Name: "monitor with multiple header blocks", + Input: ` +resource "cloudflare_load_balancer_monitor" "with_headers" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "https" + + header { + header = "Host" + values = ["api.example.com"] + } + + header { + header = "Authorization" + values = ["Bearer token123"] + } + + header { + header = "X-Custom" + values = ["value1", "value2", "value3"] + } +}`, + Expected: `resource "cloudflare_load_balancer_monitor" "with_headers" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "https" + + header = { + "Host" = ["api.example.com"] + "Authorization" = ["Bearer token123"] + "X-Custom" = ["value1", "value2", "value3"] + } +}`, + }, + { + Name: "TCP monitor with port", + Input: ` +resource "cloudflare_load_balancer_monitor" "tcp" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "tcp" + port = 3306 + method = "connection_established" + interval = 60 + timeout = 5 +}`, + Expected: `resource "cloudflare_load_balancer_monitor" "tcp" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "tcp" + port = 3306 + method = "connection_established" + interval = 60 + timeout = 5 +}`, + }, + { + Name: "HTTPS monitor with allow_insecure", + Input: ` +resource "cloudflare_load_balancer_monitor" "https_insecure" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "https" + path = "/api/status" + allow_insecure = true + expected_codes = "200" +}`, + Expected: `resource "cloudflare_load_balancer_monitor" "https_insecure" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "https" + path = "/api/status" + allow_insecure = true + expected_codes = "200" +}`, + }, + { + Name: "monitor with consecutive checks", + Input: ` +resource "cloudflare_load_balancer_monitor" "consecutive" { + account_id = "f037e56e89293a057740de681ac9abbe" + consecutive_up = 2 + consecutive_down = 3 +}`, + Expected: `resource "cloudflare_load_balancer_monitor" "consecutive" { + account_id = "f037e56e89293a057740de681ac9abbe" + consecutive_up = 2 + consecutive_down = 3 +}`, + }, + { + Name: "multiple monitors in one file", + Input: ` +resource "cloudflare_load_balancer_monitor" "first" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "http" + + header { + header = "Host" + values = ["first.example.com"] + } +} + +resource "cloudflare_load_balancer_monitor" "second" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "https" + + header { + header = "Host" + values = ["second.example.com"] + } +}`, + Expected: `resource "cloudflare_load_balancer_monitor" "first" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "http" + + header = { + "Host" = ["first.example.com"] + } +} + +resource "cloudflare_load_balancer_monitor" "second" { + account_id = "f037e56e89293a057740de681ac9abbe" + type = "https" + + header = { + "Host" = ["second.example.com"] + } +}`, + }, + } + + testhelpers.RunConfigTransformTests(t, tests, migrator) + }) + + // Test state transformations + t.Run("StateTransformation", func(t *testing.T) { + tests := []testhelpers.StateTestCase{ + { + Name: "minimal monitor state", + Input: `{ + "schema_version": 1, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe" + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe" + } +}`, + }, + { + Name: "monitor with numeric fields - converted to float64", + Input: `{ + "schema_version": 1, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "interval": 60, + "port": 8080, + "retries": 2, + "timeout": 5, + "consecutive_down": 3, + "consecutive_up": 2 + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "interval": 60.0, + "port": 8080.0, + "retries": 2.0, + "timeout": 5.0, + "consecutive_down": 3.0, + "consecutive_up": 2.0 + } +}`, + }, + { + Name: "monitor with header array-of-objects - converted to map-of-arrays", + Input: `{ + "schema_version": 1, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "header": [ + { + "header": "Host", + "values": ["example.com"] + }, + { + "header": "X-Custom", + "values": ["value1", "value2"] + } + ] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "header": { + "Host": ["example.com"], + "X-Custom": ["value1", "value2"] + } + } +}`, + }, + { + Name: "full monitor with all transformations", + Input: `{ + "schema_version": 1, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "type": "https", + "interval": 30, + "port": 443, + "retries": 3, + "timeout": 10, + "consecutive_down": 2, + "consecutive_up": 1, + "header": [ + { + "header": "Host", + "values": ["api.example.com"] + }, + { + "header": "Authorization", + "values": ["Bearer token"] + } + ] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "type": "https", + "interval": 30.0, + "port": 443.0, + "retries": 3.0, + "timeout": 10.0, + "consecutive_down": 2.0, + "consecutive_up": 1.0, + "header": { + "Host": ["api.example.com"], + "Authorization": ["Bearer token"] + } + } +}`, + }, + { + Name: "monitor with empty header array - removed", + Input: `{ + "schema_version": 1, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "header": [] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe" + } +}`, + }, + { + Name: "monitor with single header", + Input: `{ + "schema_version": 1, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "header": [ + { + "header": "Host", + "values": ["example.com", "www.example.com"] + } + ] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "abc123", + "account_id": "f037e56e89293a057740de681ac9abbe", + "header": { + "Host": ["example.com", "www.example.com"] + } + } +}`, + }, + { + Name: "state without attributes - unchanged except schema_version", + Input: `{ + "schema_version": 1 +}`, + Expected: `{ + "schema_version": 1 +}`, + }, + } + + testhelpers.RunStateTransformTests(t, tests, migrator) + }) +}