diff --git a/docs/data-sources/dns_zone.md b/docs/data-sources/dns_zone.md index 1b7a5cec0..d9527dee8 100644 --- a/docs/data-sources/dns_zone.md +++ b/docs/data-sources/dns_zone.md @@ -28,7 +28,7 @@ data "stackit_dns_zone" "example" { ### Optional -- `dns_name` (String) The zone name. E.g. `example.com` +- `dns_name` (String) The zone name. E.g. `example.com` (must not end with a trailing dot). - `zone_id` (String) The zone ID. ### Read-Only diff --git a/stackit/internal/services/dns/dns_acc_test.go b/stackit/internal/services/dns/dns_acc_test.go index 201ab118a..762244f1a 100644 --- a/stackit/internal/services/dns/dns_acc_test.go +++ b/stackit/internal/services/dns/dns_acc_test.go @@ -93,6 +93,20 @@ func TestAccDnsMinResource(t *testing.T) { ConfigVariables: configVarsInvalid(testConfigVarsMin), ExpectError: regexp.MustCompile(`not a valid dns name. Need at least two levels`), }, + // Creation fail: trailing dot is rejected on purpose + { + Config: resourceMinConfig, + ConfigVariables: func() config.Variables { + vars := maps.Clone(testConfigVarsMin) + + // Ensure it ends with a dot (even if the random value already had one, be explicit) + base := testutil.ConvertConfigVariable(vars["dns_name"]) + vars["dns_name"] = config.StringVariable(base + ".") + + return vars + }(), + ExpectError: regexp.MustCompile(`dns_name must not end with a trailing dot`), + }, // creation { Config: resourceMinConfig, diff --git a/stackit/internal/services/dns/zone/datasource.go b/stackit/internal/services/dns/zone/datasource.go index 645ae9433..15bf24672 100644 --- a/stackit/internal/services/dns/zone/datasource.go +++ b/stackit/internal/services/dns/zone/datasource.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" @@ -95,8 +96,16 @@ func (d *zoneDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r Computed: true, }, "dns_name": schema.StringAttribute{ - Description: "The zone name. E.g. `example.com`", + Description: "The zone name. E.g. `example.com` (must not end with a trailing dot).", Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.LengthAtMost(253), + stringvalidator.RegexMatches( + dnsNameNoTrailingDotRegex, + "dns_name must not end with a trailing dot", + ), + }, }, "description": schema.StringAttribute{ Description: "Description of the zone.", diff --git a/stackit/internal/services/dns/zone/resource.go b/stackit/internal/services/dns/zone/resource.go index b7c76e11f..ce82f22f1 100644 --- a/stackit/internal/services/dns/zone/resource.go +++ b/stackit/internal/services/dns/zone/resource.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "regexp" "strings" dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" @@ -36,6 +37,9 @@ var ( _ resource.ResourceWithImportState = &zoneResource{} ) +// dnsNameNoTrailingDotRegex defines the zone dns_name without trailing dot +var dnsNameNoTrailingDotRegex = regexp.MustCompile(`^.*[^.]$`) + type Model struct { Id types.String `tfsdk:"id"` // needed by TF ZoneId types.String `tfsdk:"zone_id"` @@ -144,6 +148,10 @@ func (r *zoneResource) Schema(_ context.Context, _ resource.SchemaRequest, resp Validators: []validator.String{ stringvalidator.LengthAtLeast(1), stringvalidator.LengthAtMost(253), + stringvalidator.RegexMatches( + dnsNameNoTrailingDotRegex, + "dns_name must not end with a trailing dot", + ), }, }, "description": schema.StringAttribute{ diff --git a/stackit/internal/services/dns/zone/resource_test.go b/stackit/internal/services/dns/zone/resource_test.go index d12cd90de..61fcaa459 100644 --- a/stackit/internal/services/dns/zone/resource_test.go +++ b/stackit/internal/services/dns/zone/resource_test.go @@ -435,3 +435,45 @@ func TestToPayloadUpdate(t *testing.T) { }) } } + +func TestDnsNameNoTrailingDot(t *testing.T) { + tests := []struct { + description string + input string + match bool + }{ + { + "normal domain without trailing dot", + "example.com", + true, + }, + { + "single layer without trailing dot", + "example", + true, + }, + { + "domain with trailing dot", + "example.com.", + false, + }, + { + "only trailing dot", + ".", + false, + }, + { + "single layer with trailing dot", + "example.", + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := dnsNameNoTrailingDotRegex.MatchString(tt.input) + if got != tt.match { + t.Fatalf("dnsNameNoTrailingDotRegex.MatchString(%q) = %v, want %v", tt.input, got, tt.match) + } + }) + } +}