Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

framework validators and sweeper helpers #30092

Merged
merged 12 commits into from
Apr 6, 2023
38 changes: 38 additions & 0 deletions internal/framework/validators/arn.go
johnsonaj marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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"
)

type arnValidator struct{}

func (validator arnValidator) Description(_ context.Context) string {
return "An Amazon Resource Name"
}

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 !arn.IsARN(request.ConfigValue.ValueString()) {
response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic(
request.Path,
validator.Description(ctx),
"value must be a valid ARN",
))
return
}
}

func ARN() validator.String {
return arnValidator{}
}
59 changes: 59 additions & 0 deletions internal/framework/validators/arn_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
67 changes: 59 additions & 8 deletions internal/framework/validators/timestamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/types/timestamp"
)

type utcTimestampValidator struct{}
Expand All @@ -24,7 +23,8 @@ func (validator utcTimestampValidator) ValidateString(ctx context.Context, reque
return
}

if err := 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),
Expand All @@ -38,11 +38,62 @@ 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
}

t := timestamp.New(request.ConfigValue.ValueString())
if err := t.ValidateOnceADayWindowFormat(); 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
t := timestamp.New(request.ConfigValue.ValueString())
if err := t.ValidateOnceAWeekWindowFormat(); err != nil {
response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic(
request.Path,
validator.Description(ctx),
err.Error(),
))
return
}
}

func OnceAWeekWindowFormat() validator.String {
return onceAWeekWindowFormatValidator{}
}
96 changes: 96 additions & 0 deletions internal/framework/validators/timestamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
10 changes: 10 additions & 0 deletions internal/provider/fwprovider/intercept.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion internal/sweep/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
56 changes: 56 additions & 0 deletions internal/types/timestamp/timestamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package timestamp

import (
"fmt"
"regexp"
"strings"
"time"
)

// Timestamp is a timestamp string type
type Timestamp string

func New(t string) Timestamp {
return Timestamp(t)
}

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])"
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
}

// 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])"
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
}
Loading