From 2aeff7a19fa5bc125139d2cba2bd1cfc30704b86 Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Fri, 12 Apr 2024 14:34:55 -0400 Subject: [PATCH] Add validations for pagerduty_service properties inside blocks --- pagerdutyplugin/resource_pagerduty_service.go | 78 +++++++------ util/enumtypes/int64.go | 105 ++++++++++++++++++ util/enumtypes/string.go | 100 +++++++++++++++++ util/rangetypes/int64.go | 91 +++++++++++++++ util/tztypes/string.go | 90 +++++++++++++++ util/util.go | 12 +- 6 files changed, 437 insertions(+), 39 deletions(-) create mode 100644 util/enumtypes/int64.go create mode 100644 util/enumtypes/string.go create mode 100644 util/rangetypes/int64.go create mode 100644 util/tztypes/string.go diff --git a/pagerdutyplugin/resource_pagerduty_service.go b/pagerdutyplugin/resource_pagerduty_service.go index 9231d8881..12a9ca040 100644 --- a/pagerdutyplugin/resource_pagerduty_service.go +++ b/pagerdutyplugin/resource_pagerduty_service.go @@ -9,6 +9,9 @@ import ( "github.com/PagerDuty/go-pagerduty" "github.com/PagerDuty/terraform-provider-pagerduty/util" + "github.com/PagerDuty/terraform-provider-pagerduty/util/enumtypes" + "github.com/PagerDuty/terraform-provider-pagerduty/util/rangetypes" + "github.com/PagerDuty/terraform-provider-pagerduty/util/tztypes" "github.com/PagerDuty/terraform-provider-pagerduty/util/validate" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -128,24 +131,18 @@ func (r *resourceService) Schema(ctx context.Context, req resource.SchemaRequest listvalidator.SizeBetween(1, 1), listvalidator.ConflictsWith(path.MatchRoot("alert_grouping")), listvalidator.ConflictsWith(path.MatchRoot("alert_grouping_timeout")), - // util.ValidateAlertGroupingParametersType("time", "intelligent", "rules"), - // util.ValidateAlertGroupingParametersConfigAggregate("all", "any"), - // util.ValidateAlertGroupingParametersConfigTimeWindow(300, 3600), }, ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ - "type": types.StringType, - // Validators: []validator.String{ stringvalidator.OneOf("time", "intelligent", "rules"), }, + "type": alertGroupingParametersTypeType, "config": types.ListType{ // Validators: []validator.List{listvalidator.SizeAtMost(1)}, ElemType: types.ObjectType{ AttrTypes: map[string]attr.Type{ - "timeout": types.Int64Type, - "fields": types.ListType{ElemType: types.StringType}, - "aggregate": types.StringType, - // Validators: []validator.String{ stringvalidator.OneOf("all", "any"), }, - "time_window": types.Int64Type, - // Validators: []validator.Int64{ int64validator.Between(300, 3600), }, + "timeout": types.Int64Type, + "fields": types.ListType{ElemType: types.StringType}, + "aggregate": alertGroupingParametersConfigAggregateType, + "time_window": alertGroupingParametersConfigTimeWindowType, }, }, }, @@ -163,8 +160,7 @@ func (r *resourceService) Schema(ctx context.Context, req resource.SchemaRequest ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "enabled": types.BoolType, - "timeout": types.Int64Type, - // Validators: []validator.Int64{ int64validator.OneOf(120, 180, 300, 600, 900), }, + "timeout": autoPauseNotificationsParametersTimeoutType, }, }, }, @@ -241,8 +237,7 @@ func (r *resourceService) Schema(ctx context.Context, req resource.SchemaRequest "type": types.StringType, "start_time": types.StringType, "end_time": types.StringType, - "time_zone": types.StringType, - // Validators: []validator.String{util.ValidateTimezone()}, + "time_zone": tztypes.StringType{}, "days_of_week": types.ListType{ // Validators: []validator.List{ listvalidator.SizeAtMost(7), }, ElemType: types.StringType, @@ -277,8 +272,6 @@ func (r *resourceService) Create(ctx context.Context, req resource.CreateRequest return } - log.Printf("[CG] %#v", service) - err = retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { serviceResponse, err := r.client.GetServiceWithContext(ctx, service.ID, &pagerduty.GetServiceOptions{ Includes: []string{"auto_pause_notifications_parameters"}, @@ -289,7 +282,6 @@ func (r *resourceService) Create(ctx context.Context, req resource.CreateRequest } return retry.RetryableError(err) } - log.Printf("[CG] serviceResponse: %#v", serviceResponse) model = flattenService(ctx, serviceResponse, &resp.Diagnostics) if resp.Diagnostics.HasError() { return retry.NonRetryableError(fmt.Errorf("%#v", resp.Diagnostics)) @@ -301,7 +293,6 @@ func (r *resourceService) Create(ctx context.Context, req resource.CreateRequest return } - log.Printf("[CG] Model: %#v", service) resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) } @@ -595,7 +586,6 @@ func buildSupportHours(ctx context.Context, list types.List, diags *diag.Diagnos if !obj.DaysOfWeek.IsNull() { daysOfWeekStr := []string{} - log.Printf("[CG] %#v", obj.DaysOfWeek) if d := obj.DaysOfWeek.ElementsAs(ctx, &daysOfWeekStr, false); d.HasError() { diags.Append(d...) return nil @@ -613,6 +603,17 @@ func buildSupportHours(ctx context.Context, list types.List, diags *diag.Diagnos return supportHours } +var ( + alertGroupingParametersTypeType = enumtypes.StringType{ + OneOf: []string{"time", "intelligent", "rules"}} + alertGroupingParametersConfigAggregateType = enumtypes.StringType{ + OneOf: []string{"all", "any"}} + alertGroupingParametersConfigTimeWindowType = rangetypes.Int64Type{ + Start: 300, End: 3600} + autoPauseNotificationsParametersTimeoutType = enumtypes.Int64Type{ + OneOf: []int64{120, 180, 300, 600, 900}} +) + func flattenService(ctx context.Context, service *pagerduty.Service, diags *diag.Diagnostics) resourceServiceModel { model := resourceServiceModel{ ID: types.StringValue(service.ID), @@ -663,18 +664,19 @@ func flattenService(ctx context.Context, service *pagerduty.Service, diags *diag func flattenAlertGroupingParameters(ctx context.Context, params *pagerduty.AlertGroupingParameters, diags *diag.Diagnostics) types.List { alertGroupParamsConfigObjectType := types.ObjectType{ AttrTypes: map[string]attr.Type{ - "aggregate": types.StringType, + "aggregate": alertGroupingParametersConfigAggregateType, "fields": types.ListType{ElemType: types.StringType}, "timeout": types.Int64Type, - "time_window": types.Int64Type, + "time_window": alertGroupingParametersConfigTimeWindowType, }, } alertGroupingParametersObjectType := types.ObjectType{ AttrTypes: map[string]attr.Type{ - "type": types.StringType, + "type": alertGroupingParametersTypeType, "config": types.ListType{ElemType: alertGroupParamsConfigObjectType}, }, } + nullList := types.ListNull(alertGroupingParametersObjectType) if params == nil { return nullList @@ -693,9 +695,9 @@ func flattenAlertGroupingParameters(ctx context.Context, params *pagerduty.Alert timeout = types.Int64Value(int64(*params.Config.Timeout)) } - aggregate := types.StringNull() + aggregate := enumtypes.NewStringNull(alertGroupingParametersConfigAggregateType) if params.Config.Aggregate != "" { - aggregate = types.StringValue(params.Config.Aggregate) + aggregate = enumtypes.NewStringValue(params.Config.Aggregate, alertGroupingParametersConfigAggregateType) } configObj, d := types.ObjectValue(alertGroupParamsConfigObjectType.AttrTypes, map[string]attr.Value{ @@ -716,32 +718,42 @@ func flattenAlertGroupingParameters(ctx context.Context, params *pagerduty.Alert } obj, d := types.ObjectValue(alertGroupingParametersObjectType.AttrTypes, map[string]attr.Value{ - "type": types.StringValue(params.Type), + "type": enumtypes.NewStringValue(params.Type, alertGroupingParametersTypeType), "config": configList, }) + diags.Append(d...) if d.HasError() { - diags.Append(d...) return nullList } - return types.ListValueMust(alertGroupingParametersObjectType, []attr.Value{obj}) + list, d := types.ListValue(alertGroupingParametersObjectType, []attr.Value{obj}) + diags.Append(d...) + if d.HasError() { + return nullList + } + + return list } func flattenAutoPauseNotificationsParameters(params *pagerduty.AutoPauseNotificationsParameters, diags *diag.Diagnostics) types.List { autoPauseNotificationsParametersObjectType := types.ObjectType{ AttrTypes: map[string]attr.Type{ "enabled": types.BoolType, - "timeout": types.Int64Type, + "timeout": autoPauseNotificationsParametersTimeoutType, }, } + nullList := types.ListNull(autoPauseNotificationsParametersObjectType) if params == nil { return nullList } - timeout := types.Int64Null() + timeout := enumtypes.NewInt64Null(autoPauseNotificationsParametersTimeoutType) if params.Enabled { - timeout = types.Int64Value(int64(params.Timeout)) + timeout = enumtypes.NewInt64Value( + int64(params.Timeout), + autoPauseNotificationsParametersTimeoutType, + ) } obj, d := types.ObjectValue(autoPauseNotificationsParametersObjectType.AttrTypes, map[string]attr.Value{ @@ -886,7 +898,7 @@ func flattenSupportHours(hours *pagerduty.SupportHours, diags *diag.Diagnostics) "type": types.StringType, "start_time": types.StringType, "end_time": types.StringType, - "time_zone": types.StringType, + "time_zone": tztypes.StringType{}, "days_of_week": types.ListType{ElemType: types.StringType}, }, } @@ -908,7 +920,7 @@ func flattenSupportHours(hours *pagerduty.SupportHours, diags *diag.Diagnostics) "type": types.StringValue(hours.Type), "start_time": types.StringValue(hours.StartTime), "end_time": types.StringValue(hours.EndTime), - "time_zone": types.StringValue(hours.Timezone), + "time_zone": tztypes.NewStringValue(hours.Timezone), "days_of_week": dowList, }) if d.HasError() { diff --git a/util/enumtypes/int64.go b/util/enumtypes/int64.go new file mode 100644 index 000000000..0aab6bf94 --- /dev/null +++ b/util/enumtypes/int64.go @@ -0,0 +1,105 @@ +package enumtypes + +import ( + "context" + "fmt" + "math/big" + "slices" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type Int64Value struct { + basetypes.Int64Value + EnumType Int64Type +} + +func NewInt64Null(t Int64Type) Int64Value { + return Int64Value{Int64Value: basetypes.NewInt64Null(), EnumType: t} +} + +func NewInt64Value(v int64, t Int64Type) Int64Value { + return Int64Value{Int64Value: basetypes.NewInt64Value(v), EnumType: t} +} + +func (s Int64Value) Type(_ context.Context) attr.Type { + return s.EnumType +} + +type Int64Type struct { + basetypes.Int64Type + OneOf []int64 +} + +func (t Int64Type) Int64() string { + return "enumtypes.Int64Type" +} + +func (t Int64Type) Equal(o attr.Type) bool { + if t2, ok := o.(Int64Type); ok { + return slices.Equal(t.OneOf, t2.OneOf) + } + return t.Int64Type.Equal(o) +} + +func (t Int64Type) Validate(ctx context.Context, in tftypes.Value, path path.Path) (diags diag.Diagnostics) { + if in.Type() == nil { + return + } + + if !in.Type().Is(tftypes.Number) { + err := fmt.Errorf("expected Int64 value, received %T with value: %v", in, in) + diags.AddAttributeError( + path, + "Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+ + "Please report the following to the provider developer:\n\n"+err.Error(), + ) + return diags + } + + if !in.IsKnown() || in.IsNull() { + return diags + } + + var valueFloat big.Float + if err := in.As(&valueFloat); err != nil { + diags.AddAttributeError( + path, + "Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+ + "Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + valueInt64, _ := valueFloat.Int64() + + found := false + for _, v := range t.OneOf { + if v == valueInt64 { + found = true + break + } + } + + if !found { + diags.AddAttributeError( + path, + "Invalid Int64 Value", + fmt.Sprintf( + "A string value was provided that is not valid.\n"+ + "Given Value: %v\n"+ + "Expecting One Of: %v", + valueInt64, + t.OneOf, + ), + ) + return + } + + return +} diff --git a/util/enumtypes/string.go b/util/enumtypes/string.go new file mode 100644 index 000000000..1d3a522c0 --- /dev/null +++ b/util/enumtypes/string.go @@ -0,0 +1,100 @@ +package enumtypes + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type StringValue struct { + basetypes.StringValue + EnumType StringType +} + +func NewStringNull(t StringType) StringValue { + return StringValue{StringValue: basetypes.NewStringNull(), EnumType: t} +} + +func NewStringValue(v string, t StringType) StringValue { + return StringValue{StringValue: basetypes.NewStringValue(v), EnumType: t} +} + +func (s StringValue) Type(_ context.Context) attr.Type { + return s.EnumType +} + +type StringType struct { + basetypes.StringType + OneOf []string +} + +func (t StringType) String() string { + return "enumtypes.StringType" +} + +func (t StringType) Equal(o attr.Type) bool { + if t2, ok := o.(StringType); ok { + return slices.Equal(t.OneOf, t2.OneOf) + } + return t.StringType.Equal(o) +} + +func (t StringType) Validate(ctx context.Context, in tftypes.Value, path path.Path) (diags diag.Diagnostics) { + if in.Type() == nil { + return + } + + if !in.Type().Is(tftypes.String) { + err := fmt.Errorf("expected String value, received %T with value: %v", in, in) + diags.AddAttributeError( + path, + "Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+ + "Please report the following to the provider developer:\n\n"+err.Error(), + ) + return diags + } + + if !in.IsKnown() || in.IsNull() { + return diags + } + + var valueString string + if err := in.As(&valueString); err != nil { + diags.AddAttributeError( + path, + "Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+ + "Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + found := false + for _, v := range t.OneOf { + if v == valueString { + found = true + break + } + } + + if !found { + diags.AddAttributeError( + path, + "Invalid String Value", + "A string value was provided that is not valid.\n"+ + "Given Value: "+valueString+"\n"+ + "Expecting One Of: "+strings.Join(t.OneOf, ", "), + ) + return + } + + return +} diff --git a/util/rangetypes/int64.go b/util/rangetypes/int64.go new file mode 100644 index 000000000..c23467fb1 --- /dev/null +++ b/util/rangetypes/int64.go @@ -0,0 +1,91 @@ +package rangetypes + +import ( + "context" + "fmt" + "math/big" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type Int64Value struct { + basetypes.Int64Value + RangeType Int64Type +} + +func NewInt64Null(t Int64Type) Int64Value { + return Int64Value{Int64Value: basetypes.NewInt64Null(), RangeType: t} +} + +func NewInt64Value(v int64, t Int64Type) Int64Value { + return Int64Value{Int64Value: basetypes.NewInt64Value(v), RangeType: t} +} + +func (s Int64Value) Type(_ context.Context) attr.Type { + return s.RangeType +} + +type Int64Type struct { + basetypes.Int64Type + Start int64 + End int64 +} + +func (t Int64Type) String() string { + return "rangetypes.Int64Type" +} + +func (t Int64Type) Equal(o attr.Type) bool { + if t2, ok := o.(Int64Type); ok { + return t.Start == t2.Start && t.End == t2.End + } + return t.Int64Type.Equal(o) +} + +func (t Int64Type) addTypeValidationError(err error, path path.Path, diags *diag.Diagnostics) { + diags.AddAttributeError( + path, + "Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+ + "Please report the following to the provider developer:\n\n"+err.Error(), + ) +} + +func (t Int64Type) Validate(ctx context.Context, in tftypes.Value, path path.Path) (diags diag.Diagnostics) { + if in.Type() == nil { + return + } + + if !in.Type().Is(tftypes.Number) { + err := fmt.Errorf("expected Int64 value, received %T with value: %v", in, in) + t.addTypeValidationError(err, path, &diags) + return + } + + if !in.IsKnown() || in.IsNull() { + return + } + + var valueFloat big.Float + if err := in.As(&valueFloat); err != nil { + t.addTypeValidationError(err, path, &diags) + return + } + valueInt64, _ := valueFloat.Int64() + + if valueInt64 < t.Start || valueInt64 > int64(t.End) { + diags.AddAttributeError( + path, + "Invalid Int64 Value", + fmt.Sprintf("A value was provided that is not inside valid range (%v, %v).\n"+ + "Given Value: %v", t.Start, t.End, valueInt64), + ) + return + } + + return +} diff --git a/util/tztypes/string.go b/util/tztypes/string.go new file mode 100644 index 000000000..315d9edaf --- /dev/null +++ b/util/tztypes/string.go @@ -0,0 +1,90 @@ +package tztypes + +import ( + "context" + "fmt" + + "github.com/PagerDuty/terraform-provider-pagerduty/util" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type StringValue struct { + basetypes.StringValue +} + +func NewStringNull() StringValue { + return StringValue{StringValue: basetypes.NewStringNull()} +} + +func NewStringValue(v string) StringValue { + return StringValue{StringValue: basetypes.NewStringValue(v)} +} + +func (s StringValue) Type(_ context.Context) attr.Type { + return StringType{} +} + +type StringType struct { + basetypes.StringType +} + +func (t StringType) String() string { + return "tztypes.StringType" +} + +func (t StringType) Equal(o attr.Type) bool { + _, ok := o.(StringType) + if ok { + return true + } + + return t.StringType.Equal(o) +} + +func (t StringType) Validate(ctx context.Context, in tftypes.Value, path path.Path) (diags diag.Diagnostics) { + if in.Type() == nil { + return + } + + if !in.Type().Is(tftypes.String) { + err := fmt.Errorf("expected String value, received %T with value: %v", in, in) + diags.AddAttributeError( + path, + "Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+ + "Please report the following to the provider developer:\n\n"+err.Error(), + ) + return diags + } + + if !in.IsKnown() || in.IsNull() { + return diags + } + + var valueString string + if err := in.As(&valueString); err != nil { + diags.AddAttributeError( + path, + "Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+ + "Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + if !util.IsValidTZ(valueString) { + diags.AddAttributeError( + path, + "Invalid String Value", + "A string value was provided that is not a valid timezone.\n"+ + "Given Value: "+valueString, + ) + return + } + + return +} diff --git a/util/util.go b/util/util.go index 4f24958ba..6971a312b 100644 --- a/util/util.go +++ b/util/util.go @@ -278,16 +278,16 @@ func ResourcePagerDutyParseColonCompoundID(id string) (string, string, error) { return parts[0], parts[1], nil } +func IsValidTZ(v string) bool { + foundAt := sort.SearchStrings(validTZ, v) + return foundAt < len(validTZ) && validTZ[foundAt] == v +} + func ValidateTZValueDiagFunc(v interface{}, p cty.Path) diag.Diagnostics { var diags diag.Diagnostics value := v.(string) - valid := false - - foundAt := sort.SearchStrings(validTZ, value) - if foundAt < len(validTZ) && validTZ[foundAt] == value { - valid = true - } + valid := IsValidTZ(value) if !valid { diags = append(diags, diag.Diagnostic{