From 22ce03523cf2abc93a19d45eea8e989761241f68 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 17 Mar 2023 10:02:04 -0500 Subject: [PATCH 01/12] flex: FlattenFrameworkStringSetLegacy() --- internal/flex/framework.go | 12 ++++++++++ internal/flex/framework_test.go | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/internal/flex/framework.go b/internal/flex/framework.go index 192d17e71d6..4a5421bdeb6 100644 --- a/internal/flex/framework.go +++ b/internal/flex/framework.go @@ -201,6 +201,18 @@ func FlattenFrameworkStringValueSetLegacy(_ context.Context, vs []string) types. return types.SetValueMust(types.StringType, elems) } +// FlattenFrameworkStringSetLegacy is the Plugin Framework variant of FlattenStringSet. +// A nil slice is converted to an empty (non-null) Set. +func FlattenFrameworkStringSetLegacy(_ context.Context, vs []*string) types.Set { + elems := make([]attr.Value, len(vs)) + + for i, v := range vs { + elems[i] = types.StringValue(aws.ToString(v)) + } + + return types.SetValueMust(types.StringType, elems) +} + // FlattenFrameworkStringValueMapLegacy has no Plugin SDK equivalent as schema.ResourceData.Set can be passed string value maps directly. // A nil map is converted to an empty (non-null) Map. func FlattenFrameworkStringValueMapLegacy(_ context.Context, m map[string]string) types.Map { diff --git a/internal/flex/framework_test.go b/internal/flex/framework_test.go index 2ee82184b54..5d6d626543e 100644 --- a/internal/flex/framework_test.go +++ b/internal/flex/framework_test.go @@ -493,6 +493,45 @@ func TestFlattenFrameworkStringValueSetLegacy(t *testing.T) { } } +func TestFlattenFrameworkStringSetLegacy(t *testing.T) { + t.Parallel() + + type testCase struct { + input []*string + expected types.Set + } + tests := map[string]testCase{ + "two elements": { + input: []*string{aws.String("GET"), aws.String("HEAD")}, + expected: types.SetValueMust(types.StringType, []attr.Value{ + types.StringValue("GET"), + types.StringValue("HEAD"), + }), + }, + "zero elements": { + input: []*string{}, + expected: types.SetValueMust(types.StringType, []attr.Value{}), + }, + "nil array": { + input: nil, + expected: types.SetValueMust(types.StringType, []attr.Value{}), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := FlattenFrameworkStringSetLegacy(context.Background(), test.input) + + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("unexpected diff (+wanted, -got): %s", diff) + } + }) + } +} + func TestFlattenFrameworkStringValueMapLegacy(t *testing.T) { t.Parallel() From ff842ad3371d1e2f0ceeceec9b283fdb2c61829c Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 17 Mar 2023 10:13:45 -0500 Subject: [PATCH 02/12] add framework ARN validator --- internal/framework/validators/arn.go | 40 +++++++++++++++ internal/framework/validators/arn_test.go | 59 +++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 internal/framework/validators/arn.go create mode 100644 internal/framework/validators/arn_test.go diff --git a/internal/framework/validators/arn.go b/internal/framework/validators/arn.go new file mode 100644 index 00000000000..7dc9c9f588b --- /dev/null +++ b/internal/framework/validators/arn.go @@ -0,0 +1,40 @@ +package validators + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +type arnValidator struct{} + +func (validator arnValidator) Description(_ context.Context) string { + return "value must be a valid ARN" +} + +func (validator arnValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +func (validator arnValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + if errs := verify.ValidateARN(request.ConfigValue.ValueString()); errs != nil { + for _, v := range errs { + response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + request.Path, + validator.Description(ctx), + v.Error(), + )) + } + return + } +} + +func ARN() validator.String { + return arnValidator{} +} diff --git a/internal/framework/validators/arn_test.go b/internal/framework/validators/arn_test.go new file mode 100644 index 00000000000..291826d721b --- /dev/null +++ b/internal/framework/validators/arn_test.go @@ -0,0 +1,59 @@ +package validators_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + fwvalidators "github.com/hashicorp/terraform-provider-aws/internal/framework/validators" +) + +func TestARNValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectError bool + } + + tests := map[string]testCase{ + "unknown String": { + val: types.StringUnknown(), + }, + "null String": { + val: types.StringNull(), + }, + "valid arn": { + val: types.StringValue("arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess"), + }, + "invalid_arn": { + val: types.StringValue("arn"), + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.StringResponse{} + fwvalidators.ARN().ValidateString(context.Background(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} From 59e025a2da0649caba0a2e45e6195a9505d23404 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 17 Mar 2023 10:26:37 -0500 Subject: [PATCH 03/12] add framework timestamp validators --- internal/framework/validators/timestamp.go | 64 +++++++++++-- .../framework/validators/timestamp_test.go | 96 +++++++++++++++++++ internal/verify/validate.go | 55 ++++++++--- 3 files changed, 195 insertions(+), 20 deletions(-) diff --git a/internal/framework/validators/timestamp.go b/internal/framework/validators/timestamp.go index 01ba7891185..b137ed8e473 100644 --- a/internal/framework/validators/timestamp.go +++ b/internal/framework/validators/timestamp.go @@ -2,11 +2,10 @@ package validators import ( "context" - "fmt" - "time" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-provider-aws/internal/verify" ) type utcTimestampValidator struct{} @@ -24,7 +23,7 @@ func (validator utcTimestampValidator) ValidateString(ctx context.Context, reque return } - if err := validateUTCTimestamp(request.ConfigValue.ValueString()); err != nil { + if err := verify.ValidateUTCTimestamp(request.ConfigValue.ValueString()); err != nil { response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( request.Path, validator.Description(ctx), @@ -38,11 +37,60 @@ func UTCTimestamp() validator.String { return utcTimestampValidator{} } -func validateUTCTimestamp(value string) error { - _, err := time.Parse(time.RFC3339, value) - if err != nil { - return fmt.Errorf("must be in RFC3339 time format %q. Example: %s", time.RFC3339, err) +type onceADayWindowFormatValidator struct{} + +func (validator onceADayWindowFormatValidator) Description(_ context.Context) string { + return "value must be a valid time format" +} + +func (validator onceADayWindowFormatValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +func (validator onceADayWindowFormatValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + if err := verify.ValidateOnceADayWindowFormat(request.ConfigValue.ValueString()); err != nil { + response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + request.Path, + validator.Description(ctx), + err.Error(), + )) + return + } +} + +func OnceADayWindowFormat() validator.String { + return onceADayWindowFormatValidator{} +} + +type onceAWeekWindowFormatValidator struct{} + +func (validator onceAWeekWindowFormatValidator) Description(_ context.Context) string { + return "value must be a valid time format" +} + +func (validator onceAWeekWindowFormatValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +func (validator onceAWeekWindowFormatValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return } - return nil + if err := verify.ValidateOnceAWeekWindowFormat(request.ConfigValue.ValueString()); err != nil { + response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + request.Path, + validator.Description(ctx), + err.Error(), + )) + return + } +} + +func OnceAWeekWindowFormat() validator.String { + return onceAWeekWindowFormatValidator{} } diff --git a/internal/framework/validators/timestamp_test.go b/internal/framework/validators/timestamp_test.go index d40ca24ca6d..083e5e0c9ec 100644 --- a/internal/framework/validators/timestamp_test.go +++ b/internal/framework/validators/timestamp_test.go @@ -57,3 +57,99 @@ func TestUTCTimestampValidator(t *testing.T) { }) } } + +func TestOnceADayWindowFormatValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectError bool + } + + tests := map[string]testCase{ + "unknown String": { + val: types.StringUnknown(), + }, + "null String": { + val: types.StringNull(), + }, + "valid format": { + val: types.StringValue("04:00-05:00"), + }, + "invalid format": { + val: types.StringValue("24:00-25:00"), + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.StringResponse{} + fwvalidators.OnceADayWindowFormat().ValidateString(context.Background(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} + +func TestOnceAWeekWindowFormatValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectError bool + } + + tests := map[string]testCase{ + "unknown String": { + val: types.StringUnknown(), + }, + "null String": { + val: types.StringNull(), + }, + "valid format": { + val: types.StringValue("sun:04:00-sun:05:00"), + }, + "invalid format": { + val: types.StringValue("sun:04:00-sun:04:60"), + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.StringResponse{} + fwvalidators.OnceAWeekWindowFormat().ValidateString(context.Background(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/internal/verify/validate.go b/internal/verify/validate.go index c4a01431b87..829298975fd 100644 --- a/internal/verify/validate.go +++ b/internal/verify/validate.go @@ -294,29 +294,50 @@ func ValidMulticastIPAddress(v interface{}, k string) (ws []string, errors []err return } -func ValidOnceADayWindowFormat(v interface{}, k string) (ws []string, errors []error) { +func ValidateOnceADayWindowFormat(value string) error { // valid time format is "hh24:mi" validTimeFormat := "([0-1][0-9]|2[0-3]):([0-5][0-9])" validTimeFormatConsolidated := "^(" + validTimeFormat + "-" + validTimeFormat + "|)$" - value := v.(string) if !regexp.MustCompile(validTimeFormatConsolidated).MatchString(value) { - errors = append(errors, fmt.Errorf( - "%q must satisfy the format of \"hh24:mi-hh24:mi\".", k)) + return fmt.Errorf("(%s) must satisfy the format of \"hh24:mi-hh24:mi\"", value) + } + + return nil +} + +func ValidOnceADayWindowFormat(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if err := ValidateOnceADayWindowFormat(value); err != nil { + errors = append(errors, err) + return } + return } -func ValidOnceAWeekWindowFormat(v interface{}, k string) (ws []string, errors []error) { +func ValidateOnceAWeekWindowFormat(value string) error { // valid time format is "ddd:hh24:mi" validTimeFormat := "(sun|mon|tue|wed|thu|fri|sat):([0-1][0-9]|2[0-3]):([0-5][0-9])" validTimeFormatConsolidated := "^(" + validTimeFormat + "-" + validTimeFormat + "|)$" - value := strings.ToLower(v.(string)) - if !regexp.MustCompile(validTimeFormatConsolidated).MatchString(value) { - errors = append(errors, fmt.Errorf( - "%q must satisfy the format of \"ddd:hh24:mi-ddd:hh24:mi\".", k)) + val := strings.ToLower(value) + if !regexp.MustCompile(validTimeFormatConsolidated).MatchString(val) { + return fmt.Errorf("(%s) must satisfy the format of \"ddd:hh24:mi-ddd:hh24:mi\"", val) + } + + return nil +} + +func ValidOnceAWeekWindowFormat(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if err := ValidateOnceAWeekWindowFormat(value); err != nil { + errors = append(errors, err) + return } + return } @@ -368,15 +389,25 @@ func ValidTypeStringNullableFloat(v interface{}, k string) (ws []string, es []er return } +func ValidateUTCTimestamp(value string) error { + _, err := time.Parse(time.RFC3339, value) + if err != nil { + return fmt.Errorf("must be in RFC3339 time format %q. Example: %s", time.RFC3339, err) + } + + return nil +} + // ValidUTCTimestamp validates a string in UTC Format required by APIs including: // https://docs.aws.amazon.com/iot/latest/apireference/API_CloudwatchMetricAction.html // https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_RestoreDBInstanceToPointInTime.html func ValidUTCTimestamp(v interface{}, k string) (ws []string, errors []error) { value := v.(string) - _, err := time.Parse(time.RFC3339, value) - if err != nil { - errors = append(errors, fmt.Errorf("%q must be in RFC3339 time format %q. Example: %s", k, time.RFC3339, err)) + if err := ValidateUTCTimestamp(value); err != nil { + errors = append(errors, err) + return } + return } From d70035c25d2c1837460d27410e0cccad56c2e7ad Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 17 Mar 2023 10:28:02 -0500 Subject: [PATCH 04/12] add UpgradeState() to framework wrapper --- internal/provider/fwprovider/intercept.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/provider/fwprovider/intercept.go b/internal/provider/fwprovider/intercept.go index 6b9936863b8..d28bcccb5f7 100644 --- a/internal/provider/fwprovider/intercept.go +++ b/internal/provider/fwprovider/intercept.go @@ -275,6 +275,16 @@ func (w *wrappedResource) ValidateConfig(ctx context.Context, request resource.V } } +func (w *wrappedResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { + if v, ok := w.inner.(resource.ResourceWithUpgradeState); ok { + ctx = w.bootstrapContext(ctx, w.meta) + + return v.UpgradeState(ctx) + } + + return nil +} + // tagsInterceptor implements transparent tagging. type tagsInterceptor struct { tags *types.ServicePackageResourceTags From c11bbc49723284bf5815375ad3bf11a01f87feed Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 17 Mar 2023 10:29:59 -0500 Subject: [PATCH 05/12] add sweeper helper for framwork supplemental attributes --- internal/sweep/framework.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/sweep/framework.go b/internal/sweep/framework.go index 71ae13b66b1..f68b30f43e4 100644 --- a/internal/sweep/framework.go +++ b/internal/sweep/framework.go @@ -19,9 +19,11 @@ import ( type FrameworkSupplementalAttribute struct { Path string - Value string + Value any } +type FrameworkSupplementalAttributes []FrameworkSupplementalAttribute + type SweepFrameworkResource struct { factory func(context.Context) (fwresource.ResourceWithConfigure, error) id string @@ -34,6 +36,19 @@ type SweepFrameworkResource struct { supplementalAttributes []FrameworkSupplementalAttribute } +func NewFrameworkSupplementalAttributes() FrameworkSupplementalAttributes { + return FrameworkSupplementalAttributes{} +} + +func (f *FrameworkSupplementalAttributes) Add(path string, value any) { + item := FrameworkSupplementalAttribute{ + Path: path, + Value: value, + } + + *f = append(*f, item) +} + func NewSweepFrameworkResource(factory func(context.Context) (fwresource.ResourceWithConfigure, error), id string, meta interface{}, supplementalAttributes ...FrameworkSupplementalAttribute) *SweepFrameworkResource { return &SweepFrameworkResource{ factory: factory, From 04f797088f595b8014d1d02af2d50542eab4c60b Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 17 Mar 2023 11:19:00 -0500 Subject: [PATCH 06/12] add attribute key to ARN validator error message --- internal/framework/validators/arn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/framework/validators/arn.go b/internal/framework/validators/arn.go index 7dc9c9f588b..6dce056a0d8 100644 --- a/internal/framework/validators/arn.go +++ b/internal/framework/validators/arn.go @@ -23,7 +23,7 @@ func (validator arnValidator) ValidateString(ctx context.Context, request valida return } - if errs := verify.ValidateARN(request.ConfigValue.ValueString()); errs != nil { + if errs := verify.ValidateARN(request.Path.String(), request.ConfigValue.ValueString()); errs != nil { for _, v := range errs { response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( request.Path, From 3606ae8c980e1b423a024cf3f079dded7fa5b01c Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 17 Mar 2023 12:32:09 -0500 Subject: [PATCH 07/12] simplify framework ARN validator --- internal/framework/validators/arn.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/internal/framework/validators/arn.go b/internal/framework/validators/arn.go index 6dce056a0d8..2910f04f668 100644 --- a/internal/framework/validators/arn.go +++ b/internal/framework/validators/arn.go @@ -3,15 +3,15 @@ package validators import ( "context" + "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-provider-aws/internal/verify" ) type arnValidator struct{} func (validator arnValidator) Description(_ context.Context) string { - return "value must be a valid ARN" + return "An Amazon Resource Name" } func (validator arnValidator) MarkdownDescription(ctx context.Context) string { @@ -23,14 +23,12 @@ func (validator arnValidator) ValidateString(ctx context.Context, request valida return } - if errs := verify.ValidateARN(request.Path.String(), request.ConfigValue.ValueString()); errs != nil { - for _, v := range errs { - response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( - request.Path, - validator.Description(ctx), - v.Error(), - )) - } + if !arn.IsARN(request.ConfigValue.ValueString()) { + response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + request.Path, + validator.Description(ctx), + "value must be a valid ARN", + )) return } } From f9d844af68f7abce44315c449d41ba7b973760de Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 4 Apr 2023 10:19:18 -0500 Subject: [PATCH 08/12] add timestamp type --- internal/framework/validators/timestamp.go | 11 ++- internal/types/timestamp/timestamp.go | 53 +++++++++++++ internal/types/timestamp/timestamp_test.go | 92 ++++++++++++++++++++++ internal/verify/validate.go | 45 ++--------- 4 files changed, 160 insertions(+), 41 deletions(-) create mode 100644 internal/types/timestamp/timestamp.go create mode 100644 internal/types/timestamp/timestamp_test.go diff --git a/internal/framework/validators/timestamp.go b/internal/framework/validators/timestamp.go index b137ed8e473..6ec7fb84794 100644 --- a/internal/framework/validators/timestamp.go +++ b/internal/framework/validators/timestamp.go @@ -5,7 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/internal/types/timestamp" ) type utcTimestampValidator struct{} @@ -23,7 +23,8 @@ func (validator utcTimestampValidator) ValidateString(ctx context.Context, reque return } - if err := verify.ValidateUTCTimestamp(request.ConfigValue.ValueString()); err != nil { + t := timestamp.New(request.ConfigValue.ValueString()) + if err := t.ValidateUTCFormat(); err != nil { response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( request.Path, validator.Description(ctx), @@ -52,7 +53,8 @@ func (validator onceADayWindowFormatValidator) ValidateString(ctx context.Contex return } - if err := verify.ValidateOnceADayWindowFormat(request.ConfigValue.ValueString()); err != nil { + t := timestamp.New(request.ConfigValue.ValueString()) + if err := t.ValidateOnceADayWindowFormat(); err != nil { response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( request.Path, validator.Description(ctx), @@ -81,7 +83,8 @@ func (validator onceAWeekWindowFormatValidator) ValidateString(ctx context.Conte return } - if err := verify.ValidateOnceAWeekWindowFormat(request.ConfigValue.ValueString()); err != nil { + t := timestamp.New(request.ConfigValue.ValueString()) + if err := t.ValidateOnceAWeekWindowFormat(); err != nil { response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( request.Path, validator.Description(ctx), diff --git a/internal/types/timestamp/timestamp.go b/internal/types/timestamp/timestamp.go new file mode 100644 index 00000000000..bf10631d2cf --- /dev/null +++ b/internal/types/timestamp/timestamp.go @@ -0,0 +1,53 @@ +package timestamp + +import ( + "fmt" + "regexp" + "strings" + "time" +) + +type Timestamp string + +func New(t string) Timestamp { + return Timestamp(t) +} + +func (t Timestamp) String() string { + return string(t) +} + +func (t Timestamp) ValidateOnceADayWindowFormat() error { + // valid time format is "hh24:mi" + validTimeFormat := "([0-1][0-9]|2[0-3]):([0-5][0-9])" + validTimeFormatConsolidated := "^(" + validTimeFormat + "-" + validTimeFormat + "|)$" + + if !regexp.MustCompile(validTimeFormatConsolidated).MatchString(t.String()) { + return fmt.Errorf("(%s) must satisfy the format of \"hh24:mi-hh24:mi\"", t.String()) + } + + return nil +} + +func (t Timestamp) ValidateOnceAWeekWindowFormat() error { + // valid time format is "ddd:hh24:mi" + validTimeFormat := "(sun|mon|tue|wed|thu|fri|sat):([0-1][0-9]|2[0-3]):([0-5][0-9])" + validTimeFormatConsolidated := "^(" + validTimeFormat + "-" + validTimeFormat + "|)$" + + val := strings.ToLower(t.String()) + if !regexp.MustCompile(validTimeFormatConsolidated).MatchString(val) { + return fmt.Errorf("(%s) must satisfy the format of \"ddd:hh24:mi-ddd:hh24:mi\"", val) + } + + return nil +} + +// ValidateUTCFormat parses timestamp in RFC3339 format +func (t Timestamp) ValidateUTCFormat() error { + _, err := time.Parse(time.RFC3339, t.String()) + if err != nil { + return fmt.Errorf("must be in RFC3339 time format %q. Example: %s", time.RFC3339, err) + } + + return nil +} diff --git a/internal/types/timestamp/timestamp_test.go b/internal/types/timestamp/timestamp_test.go new file mode 100644 index 00000000000..6ea3bec2fe8 --- /dev/null +++ b/internal/types/timestamp/timestamp_test.go @@ -0,0 +1,92 @@ +package timestamp + +import "testing" + +func TestValidateOnceADayWindowFormat(t *testing.T) { + t.Parallel() + type tc struct { + value string + expectError bool + } + tests := map[string]tc{ + "invalid hour": { + value: "24:00-25:00", + expectError: true, + }, + "invalid minute": { + value: "04:00-04:60", + expectError: true, + }, + "valid": { + value: "04:00-05:00", + }, + "empty": { + value: "", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + ts := New(test.value) + err := ts.ValidateOnceADayWindowFormat() + + if err == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if err != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", err) + } + }) + } +} + +func TestValidateOnceAWeekWindowFormat(t *testing.T) { + t.Parallel() + type tc struct { + value string + expectError bool + } + tests := map[string]tc{ + "invalid day of week": { + value: "san:04:00-san:05:00", + expectError: true, + }, + "invalid hour": { + value: "sun:24:00-san:25:00", + expectError: true, + }, + "invalid minute": { + value: "sun:04:00-sun:04:60", + expectError: true, + }, + "valid": { + value: "sun:04:00-sun:05:00", + }, + "case insensitive day": { + value: "Sun:04:00-Sun:05:00", + }, + "empty": { + value: "", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + ts := New(test.value) + err := ts.ValidateOnceAWeekWindowFormat() + + if err == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if err != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", err) + } + }) + } +} diff --git a/internal/verify/validate.go b/internal/verify/validate.go index 829298975fd..85edeb16492 100644 --- a/internal/verify/validate.go +++ b/internal/verify/validate.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/types/timestamp" ) var accountIDRegexp = regexp.MustCompile(`^(aws|aws-managed|third-party|\d{12})$`) @@ -294,22 +295,11 @@ func ValidMulticastIPAddress(v interface{}, k string) (ws []string, errors []err return } -func ValidateOnceADayWindowFormat(value string) error { - // valid time format is "hh24:mi" - validTimeFormat := "([0-1][0-9]|2[0-3]):([0-5][0-9])" - validTimeFormatConsolidated := "^(" + validTimeFormat + "-" + validTimeFormat + "|)$" - - if !regexp.MustCompile(validTimeFormatConsolidated).MatchString(value) { - return fmt.Errorf("(%s) must satisfy the format of \"hh24:mi-hh24:mi\"", value) - } - - return nil -} - func ValidOnceADayWindowFormat(v interface{}, k string) (ws []string, errors []error) { value := v.(string) - if err := ValidateOnceADayWindowFormat(value); err != nil { + t := timestamp.New(value) + if err := t.ValidateOnceADayWindowFormat(); err != nil { errors = append(errors, err) return } @@ -317,23 +307,11 @@ func ValidOnceADayWindowFormat(v interface{}, k string) (ws []string, errors []e return } -func ValidateOnceAWeekWindowFormat(value string) error { - // valid time format is "ddd:hh24:mi" - validTimeFormat := "(sun|mon|tue|wed|thu|fri|sat):([0-1][0-9]|2[0-3]):([0-5][0-9])" - validTimeFormatConsolidated := "^(" + validTimeFormat + "-" + validTimeFormat + "|)$" - - val := strings.ToLower(value) - if !regexp.MustCompile(validTimeFormatConsolidated).MatchString(val) { - return fmt.Errorf("(%s) must satisfy the format of \"ddd:hh24:mi-ddd:hh24:mi\"", val) - } - - return nil -} - func ValidOnceAWeekWindowFormat(v interface{}, k string) (ws []string, errors []error) { value := v.(string) - if err := ValidateOnceAWeekWindowFormat(value); err != nil { + t := timestamp.New(value) + if err := t.ValidateOnceAWeekWindowFormat(); err != nil { errors = append(errors, err) return } @@ -389,21 +367,14 @@ func ValidTypeStringNullableFloat(v interface{}, k string) (ws []string, es []er return } -func ValidateUTCTimestamp(value string) error { - _, err := time.Parse(time.RFC3339, value) - if err != nil { - return fmt.Errorf("must be in RFC3339 time format %q. Example: %s", time.RFC3339, err) - } - - return nil -} - // ValidUTCTimestamp validates a string in UTC Format required by APIs including: // https://docs.aws.amazon.com/iot/latest/apireference/API_CloudwatchMetricAction.html // https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_RestoreDBInstanceToPointInTime.html func ValidUTCTimestamp(v interface{}, k string) (ws []string, errors []error) { value := v.(string) - if err := ValidateUTCTimestamp(value); err != nil { + + t := timestamp.New(value) + if err := t.ValidateUTCFormat(); err != nil { errors = append(errors, err) return } From 9b2e1f975612f8cc09af8c4c6b6b2e42ae0ae922 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 4 Apr 2023 10:31:09 -0500 Subject: [PATCH 09/12] add utc format test --- internal/types/timestamp/timestamp.go | 3 ++ internal/types/timestamp/timestamp_test.go | 42 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/internal/types/timestamp/timestamp.go b/internal/types/timestamp/timestamp.go index bf10631d2cf..454cf8f4f1c 100644 --- a/internal/types/timestamp/timestamp.go +++ b/internal/types/timestamp/timestamp.go @@ -7,6 +7,7 @@ import ( "time" ) +// Timestamp is a timestamp string type type Timestamp string func New(t string) Timestamp { @@ -17,6 +18,7 @@ func (t Timestamp) String() string { return string(t) } +// ValidateOnceADayWindowFormat validates once a day window format func (t Timestamp) ValidateOnceADayWindowFormat() error { // valid time format is "hh24:mi" validTimeFormat := "([0-1][0-9]|2[0-3]):([0-5][0-9])" @@ -29,6 +31,7 @@ func (t Timestamp) ValidateOnceADayWindowFormat() error { return nil } +// ValidateOnceAWeekWindowFormat validates once a week window date format func (t Timestamp) ValidateOnceAWeekWindowFormat() error { // valid time format is "ddd:hh24:mi" validTimeFormat := "(sun|mon|tue|wed|thu|fri|sat):([0-1][0-9]|2[0-3]):([0-5][0-9])" diff --git a/internal/types/timestamp/timestamp_test.go b/internal/types/timestamp/timestamp_test.go index 6ea3bec2fe8..dac975cc757 100644 --- a/internal/types/timestamp/timestamp_test.go +++ b/internal/types/timestamp/timestamp_test.go @@ -90,3 +90,45 @@ func TestValidateOnceAWeekWindowFormat(t *testing.T) { }) } } + +func TestValidateUTCFormat(t *testing.T) { + t.Parallel() + type tc struct { + value string + expectError bool + } + tests := map[string]tc{ + "invalid no TZ": { + value: "2015-03-07 23:45:00", + expectError: true, + }, + "invalid date order": { + value: "27-03-2019 23:45:00", + expectError: true, + }, + "invalid format": { + value: "Mon, 02 Jan 2006 15:04:05 -0700", + expectError: true, + }, + "valid": { + value: "2006-01-02T15:04:05Z", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + ts := New(test.value) + err := ts.ValidateUTCFormat() + + if err == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if err != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", err) + } + }) + } +} From 7e7127352e1efe9d1f61ef5b64583996d149d8b3 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 4 Apr 2023 10:33:28 -0500 Subject: [PATCH 10/12] linter: fix error --- internal/types/timestamp/timestamp_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/types/timestamp/timestamp_test.go b/internal/types/timestamp/timestamp_test.go index dac975cc757..609f4add25b 100644 --- a/internal/types/timestamp/timestamp_test.go +++ b/internal/types/timestamp/timestamp_test.go @@ -26,7 +26,9 @@ func TestValidateOnceADayWindowFormat(t *testing.T) { } for name, test := range tests { + name, test := name, test t.Run(name, func(t *testing.T) { + t.Parallel() ts := New(test.value) @@ -74,6 +76,7 @@ func TestValidateOnceAWeekWindowFormat(t *testing.T) { } for name, test := range tests { + name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -116,6 +119,7 @@ func TestValidateUTCFormat(t *testing.T) { } for name, test := range tests { + name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() From 5404375e5bfb5814690c60b006ae77032c7a2046 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 4 Apr 2023 11:03:55 -0500 Subject: [PATCH 11/12] linter: remove leading whitespace --- internal/types/timestamp/timestamp_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/types/timestamp/timestamp_test.go b/internal/types/timestamp/timestamp_test.go index 609f4add25b..76d6b0401c0 100644 --- a/internal/types/timestamp/timestamp_test.go +++ b/internal/types/timestamp/timestamp_test.go @@ -28,7 +28,6 @@ func TestValidateOnceADayWindowFormat(t *testing.T) { for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - t.Parallel() ts := New(test.value) From 5c81f2cf9822886f1c2ebe15b668b0647c5fb7b9 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Wed, 5 Apr 2023 11:52:29 -0500 Subject: [PATCH 12/12] remove conflicts --- internal/flex/framework.go | 12 ---------- internal/flex/framework_test.go | 39 --------------------------------- 2 files changed, 51 deletions(-) diff --git a/internal/flex/framework.go b/internal/flex/framework.go index 4a5421bdeb6..192d17e71d6 100644 --- a/internal/flex/framework.go +++ b/internal/flex/framework.go @@ -201,18 +201,6 @@ func FlattenFrameworkStringValueSetLegacy(_ context.Context, vs []string) types. return types.SetValueMust(types.StringType, elems) } -// FlattenFrameworkStringSetLegacy is the Plugin Framework variant of FlattenStringSet. -// A nil slice is converted to an empty (non-null) Set. -func FlattenFrameworkStringSetLegacy(_ context.Context, vs []*string) types.Set { - elems := make([]attr.Value, len(vs)) - - for i, v := range vs { - elems[i] = types.StringValue(aws.ToString(v)) - } - - return types.SetValueMust(types.StringType, elems) -} - // FlattenFrameworkStringValueMapLegacy has no Plugin SDK equivalent as schema.ResourceData.Set can be passed string value maps directly. // A nil map is converted to an empty (non-null) Map. func FlattenFrameworkStringValueMapLegacy(_ context.Context, m map[string]string) types.Map { diff --git a/internal/flex/framework_test.go b/internal/flex/framework_test.go index 5d6d626543e..2ee82184b54 100644 --- a/internal/flex/framework_test.go +++ b/internal/flex/framework_test.go @@ -493,45 +493,6 @@ func TestFlattenFrameworkStringValueSetLegacy(t *testing.T) { } } -func TestFlattenFrameworkStringSetLegacy(t *testing.T) { - t.Parallel() - - type testCase struct { - input []*string - expected types.Set - } - tests := map[string]testCase{ - "two elements": { - input: []*string{aws.String("GET"), aws.String("HEAD")}, - expected: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("GET"), - types.StringValue("HEAD"), - }), - }, - "zero elements": { - input: []*string{}, - expected: types.SetValueMust(types.StringType, []attr.Value{}), - }, - "nil array": { - input: nil, - expected: types.SetValueMust(types.StringType, []attr.Value{}), - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := FlattenFrameworkStringSetLegacy(context.Background(), test.input) - - if diff := cmp.Diff(got, test.expected); diff != "" { - t.Errorf("unexpected diff (+wanted, -got): %s", diff) - } - }) - } -} - func TestFlattenFrameworkStringValueMapLegacy(t *testing.T) { t.Parallel()