diff --git a/stackit/internal/services/dns/recordset/resource.go b/stackit/internal/services/dns/recordset/resource.go index 9045d516..42895fbf 100644 --- a/stackit/internal/services/dns/recordset/resource.go +++ b/stackit/internal/services/dns/recordset/resource.go @@ -158,7 +158,7 @@ func (r *recordSetResource) Schema(_ context.Context, _ resource.SchemaRequest, Validators: []validator.List{ listvalidator.SizeAtLeast(1), listvalidator.UniqueValues(), - listvalidator.ValueStringsAre(validate.IP()), + listvalidator.ValueStringsAre(validate.RecordSet()), }, }, "ttl": schema.Int64Attribute{ diff --git a/stackit/internal/validate/validate.go b/stackit/internal/validate/validate.go index 7858c71a..86e6e9a3 100644 --- a/stackit/internal/validate/validate.go +++ b/stackit/internal/validate/validate.go @@ -10,7 +10,9 @@ import ( "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" ) @@ -73,6 +75,45 @@ func IP() *Validator { } } +func RecordSet() *Validator { + const typePath = "type" + return &Validator{ + description: "value must be a valid record set", + validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + recordType := basetypes.StringValue{} + req.Config.GetAttribute(ctx, path.Root(typePath), &recordType) + switch recordType.ValueString() { + case "A": + ip := net.ParseIP(req.ConfigValue.ValueString()) + if ip == nil || ip.To4() == nil { + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + "value must be an IPv4 address", + req.ConfigValue.ValueString(), + )) + } + case "AAAA": + ip := net.ParseIP(req.ConfigValue.ValueString()) + if ip == nil || ip.To4() != nil || ip.To16() == nil { + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + "value must be an IPv6 address", + req.ConfigValue.ValueString(), + )) + } + case "CNAME": + case "NS": + case "MX": + case "TXT": + case "ALIAS": + case "DNAME": + case "CAA": + default: + } + }, + } +} + func NoSeparator() *Validator { description := fmt.Sprintf("value must not contain identifier separator '%s'", core.Separator) diff --git a/stackit/internal/validate/validate_test.go b/stackit/internal/validate/validate_test.go index a639f3db..c555b55c 100644 --- a/stackit/internal/validate/validate_test.go +++ b/stackit/internal/validate/validate_test.go @@ -4,8 +4,11 @@ import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUUID(t *testing.T) { @@ -101,6 +104,157 @@ func TestIP(t *testing.T) { } } +func TestRecordSet(t *testing.T) { + tests := []struct { + description string + record string + recordType string + isValid bool + }{ + { + "A record ok IP4", + "111.222.111.222", + "A", + true, + }, + { + "A record fail IP6", + "2001:0db8:85a3:08d3::0370:7344", + "A", + false, + }, + { + "A record too short", + "0.1.2", + "A", + false, + }, + { + "A record Empty", + "", + "A", + false, + }, + { + "A record Not an IP", + "for-sure-not-an-IP", + "A", + false, + }, + { + "AAAA record fail IP4", + "111.222.111.222", + "AAAA", + false, + }, + { + "AAAA record ok IP6", + "2001:0db8:85a3:08d3::0370:7344", + "AAAA", + true, + }, + { + "AAAA record too short", + "0.1.2", + "AAAA", + false, + }, + { + "AAAA record Empty", + "", + "AAAA", + false, + }, + { + "AAAA record Not an IP", + "for-sure-not-an-IP", + "AAAA", + false, + }, + { + "CNAME record", + "some-record", + "CNAME", + true, + }, + { + "NS record", + "some-record", + "NS", + true, + }, + { + "MX record", + "some-record", + "MX", + true, + }, + { + "TXT record", + "some-record", + "TXT", + true, + }, + { + "ALIAS record", + "some-record", + "ALIAS", + true, + }, + { + "DNAME record", + "some-record", + "DNAME", + true, + }, + { + "CAA record", + "some-record", + "CAA", + true, + }, + { + "random record", + "some-record", + "random", + true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + r := validator.StringResponse{} + scheme := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "type": tftypes.String, + }, + } + value := map[string]tftypes.Value{ + "type": tftypes.NewValue(tftypes.String, tt.recordType), + } + record := tftypes.NewValue(scheme, value) + + RecordSet().ValidateString(context.Background(), validator.StringRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{}, + }, + }, + Raw: record, + }, + ConfigValue: types.StringValue(tt.record), + }, &r) + + if !tt.isValid && !r.Diagnostics.HasError() { + t.Fatalf("Should have failed") + } + if tt.isValid && r.Diagnostics.HasError() { + t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) + } + }) + } +} + func TestNoSeparator(t *testing.T) { tests := []struct { description string