internal/retry/retryable_errors.go (507 lines of code) (raw):

package retry import ( "context" "fmt" "regexp" "strings" "time" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-go/tftypes" ) var ( _ basetypes.ObjectTypable = RetryType{} _ basetypes.ObjectValuable = RetryValue{} defaultRetryableStatusCodes = []int{} defaultRetryableReadAfterCreateStatusCodes = []int{404, 403} ) const ( DefaultIntervalSeconds = 10 DefaultMaxIntervalSeconds = 180 DefaultMultiplier = 1.5 DefaultRandomizationFactor = 0.5 ) var _ basetypes.ObjectTypable = RetryType{} type RetryType struct { basetypes.ObjectType } func (t RetryType) Equal(o attr.Type) bool { other, ok := o.(RetryType) if !ok { return false } return t.ObjectType.Equal(other.ObjectType) } func (t RetryType) String() string { return "RetryType" } func (t RetryType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { var diags diag.Diagnostics attributes := in.Attributes() errorMessageRegexAttribute, ok := attributes["error_message_regex"] if !ok { diags.AddError( "Attribute Missing", `error_message_regex is missing from object`) return nil, diags } errorMessageRegexVal, ok := errorMessageRegexAttribute.(basetypes.ListValue) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`error_message_regex expected to be basetypes.ListValue, was: %T`, errorMessageRegexAttribute)) } intervalSecondsAttribute, ok := attributes["interval_seconds"] if !ok { diags.AddError( "Attribute Missing", `interval_seconds is missing from object`) return nil, diags } intervalSecondsVal, ok := intervalSecondsAttribute.(basetypes.Int64Value) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`interval_seconds expected to be basetypes.Int64Value, was: %T`, intervalSecondsAttribute)) } maxIntervalSecondsAttribute, ok := attributes["max_interval_seconds"] if !ok { diags.AddError( "Attribute Missing", `max_interval_seconds is missing from object`) return nil, diags } maxIntervalSecondsVal, ok := maxIntervalSecondsAttribute.(basetypes.Int64Value) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`max_interval_seconds expected to be basetypes.Int64Value, was: %T`, maxIntervalSecondsAttribute)) } multiplierAttribute, ok := attributes["multiplier"] if !ok { diags.AddError( "Attribute Missing", `multiplier is missing from object`) return nil, diags } multiplierVal, ok := multiplierAttribute.(basetypes.Float64Value) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`multiplier expected to be basetypes.Float64Value, was: %T`, multiplierAttribute)) } randomizationFactorAttribute, ok := attributes["randomization_factor"] if !ok { diags.AddError( "Attribute Missing", `randomization_factor is missing from object`) return nil, diags } randomizationFactorVal, ok := randomizationFactorAttribute.(basetypes.Float64Value) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`randomization_factor expected to be basetypes.Float64Value, was: %T`, randomizationFactorAttribute)) } if diags.HasError() { return nil, diags } return RetryValue{ ErrorMessageRegex: errorMessageRegexVal, IntervalSeconds: intervalSecondsVal, MaxIntervalSeconds: maxIntervalSecondsVal, Multiplier: multiplierVal, RandomizationFactor: randomizationFactorVal, state: attr.ValueStateKnown, }, diags } func NewRetryValueNull() RetryValue { return RetryValue{ state: attr.ValueStateNull, } } func NewRetryValueUnknown() RetryValue { return RetryValue{ state: attr.ValueStateUnknown, } } func NewRetryValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (RetryValue, diag.Diagnostics) { var diags diag.Diagnostics // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 ctx := context.Background() for name, attributeType := range attributeTypes { attribute, ok := attributes[name] if !ok { diags.AddError( "Missing RetryValue Attribute Value", "While creating a RetryValue value, a missing attribute value was detected. "+ "A RetryValue must contain values for all attributes, even if null or unknown. "+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ fmt.Sprintf("RetryValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), ) continue } if !attributeType.Equal(attribute.Type(ctx)) { diags.AddError( "Invalid RetryValue Attribute Type", "While creating a RetryValue value, an invalid attribute value was detected. "+ "A RetryValue must use a matching attribute type for the value. "+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ fmt.Sprintf("RetryValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ fmt.Sprintf("RetryValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), ) } } for name := range attributes { _, ok := attributeTypes[name] if !ok { diags.AddError( "Extra RetryValue Attribute Value", "While creating a RetryValue value, an extra attribute value was detected. "+ "A RetryValue must not contain values beyond the expected attribute types. "+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ fmt.Sprintf("Extra RetryValue Attribute Name: %s", name), ) } } if diags.HasError() { return NewRetryValueUnknown(), diags } errorMessageRegexAttribute, ok := attributes["error_message_regex"] if !ok { diags.AddError( "Attribute Missing", `error_message_regex is missing from object`) return NewRetryValueUnknown(), diags } errorMessageRegexVal, ok := errorMessageRegexAttribute.(basetypes.ListValue) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`error_message_regex expected to be basetypes.ListValue, was: %T`, errorMessageRegexAttribute)) } intervalSecondsAttribute, ok := attributes["interval_seconds"] if !ok { diags.AddError( "Attribute Missing", `interval_seconds is missing from object`) return NewRetryValueUnknown(), diags } intervalSecondsVal, ok := intervalSecondsAttribute.(basetypes.Int64Value) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`interval_seconds expected to be basetypes.Int64Value, was: %T`, intervalSecondsAttribute)) } maxIntervalSecondsAttribute, ok := attributes["max_interval_seconds"] if !ok { diags.AddError( "Attribute Missing", `max_interval_seconds is missing from object`) return NewRetryValueUnknown(), diags } maxIntervalSecondsVal, ok := maxIntervalSecondsAttribute.(basetypes.Int64Value) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`max_interval_seconds expected to be basetypes.Int64Value, was: %T`, maxIntervalSecondsAttribute)) } multiplierAttribute, ok := attributes["multiplier"] if !ok { diags.AddError( "Attribute Missing", `multiplier is missing from object`) return NewRetryValueUnknown(), diags } multiplierVal, ok := multiplierAttribute.(basetypes.Float64Value) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`multiplier expected to be basetypes.Float64Value, was: %T`, multiplierAttribute)) } randomizationFactorAttribute, ok := attributes["randomization_factor"] if !ok { diags.AddError( "Attribute Missing", `randomization_factor is missing from object`) return NewRetryValueUnknown(), diags } randomizationFactorVal, ok := randomizationFactorAttribute.(basetypes.Float64Value) if !ok { diags.AddError( "Attribute Wrong Type", fmt.Sprintf(`randomization_factor expected to be basetypes.Float64Value, was: %T`, randomizationFactorAttribute)) } if diags.HasError() { return NewRetryValueUnknown(), diags } return RetryValue{ ErrorMessageRegex: errorMessageRegexVal, IntervalSeconds: intervalSecondsVal, MaxIntervalSeconds: maxIntervalSecondsVal, Multiplier: multiplierVal, RandomizationFactor: randomizationFactorVal, state: attr.ValueStateKnown, }, diags } func NewRetryValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) RetryValue { object, diags := NewRetryValue(attributeTypes, attributes) if diags.HasError() { // This could potentially be added to the diag package. diagsStrings := make([]string, 0, len(diags)) for _, diagnostic := range diags { diagsStrings = append(diagsStrings, fmt.Sprintf( "%s | %s | %s", diagnostic.Severity(), diagnostic.Summary(), diagnostic.Detail())) } panic("NewRetryValueMust received error(s): " + strings.Join(diagsStrings, "\n")) } return object } func (t RetryType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { if in.Type() == nil { return NewRetryValueNull(), nil } if !in.Type().Equal(t.TerraformType(ctx)) { return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) } if !in.IsKnown() { return NewRetryValueUnknown(), nil } if in.IsNull() { return NewRetryValueNull(), nil } attributes := map[string]attr.Value{} val := map[string]tftypes.Value{} err := in.As(&val) if err != nil { return nil, err } for k, v := range val { a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) if err != nil { return nil, err } attributes[k] = a } return NewRetryValueMust(RetryValue{}.AttributeTypes(ctx), attributes), nil } func (t RetryType) ValueType(ctx context.Context) attr.Value { return RetryValue{} } var _ basetypes.ObjectValuable = RetryValue{} type RetryValue struct { ErrorMessageRegex basetypes.ListValue `tfsdk:"error_message_regex"` IntervalSeconds basetypes.Int64Value `tfsdk:"interval_seconds"` MaxIntervalSeconds basetypes.Int64Value `tfsdk:"max_interval_seconds"` Multiplier basetypes.Float64Value `tfsdk:"multiplier"` RandomizationFactor basetypes.Float64Value `tfsdk:"randomization_factor"` state attr.ValueState } func (v RetryValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { attrTypes := make(map[string]tftypes.Type, 5) var val tftypes.Value var err error attrTypes["error_message_regex"] = basetypes.ListType{ ElemType: types.StringType, }.TerraformType(ctx) attrTypes["interval_seconds"] = basetypes.Int64Type{}.TerraformType(ctx) attrTypes["max_interval_seconds"] = basetypes.Int64Type{}.TerraformType(ctx) attrTypes["multiplier"] = basetypes.Float64Type{}.TerraformType(ctx) attrTypes["randomization_factor"] = basetypes.Float64Type{}.TerraformType(ctx) objectType := tftypes.Object{AttributeTypes: attrTypes} switch v.state { case attr.ValueStateKnown: vals := make(map[string]tftypes.Value, 5) val, err = v.ErrorMessageRegex.ToTerraformValue(ctx) if err != nil { return tftypes.NewValue(objectType, tftypes.UnknownValue), err } vals["error_message_regex"] = val val, err = v.IntervalSeconds.ToTerraformValue(ctx) if err != nil { return tftypes.NewValue(objectType, tftypes.UnknownValue), err } vals["interval_seconds"] = val val, err = v.MaxIntervalSeconds.ToTerraformValue(ctx) if err != nil { return tftypes.NewValue(objectType, tftypes.UnknownValue), err } vals["max_interval_seconds"] = val val, err = v.Multiplier.ToTerraformValue(ctx) if err != nil { return tftypes.NewValue(objectType, tftypes.UnknownValue), err } vals["multiplier"] = val val, err = v.RandomizationFactor.ToTerraformValue(ctx) if err != nil { return tftypes.NewValue(objectType, tftypes.UnknownValue), err } vals["randomization_factor"] = val if err := tftypes.ValidateValue(objectType, vals); err != nil { return tftypes.NewValue(objectType, tftypes.UnknownValue), err } return tftypes.NewValue(objectType, vals), nil case attr.ValueStateNull: return tftypes.NewValue(objectType, nil), nil case attr.ValueStateUnknown: return tftypes.NewValue(objectType, tftypes.UnknownValue), nil default: panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) } } func (v RetryValue) IsNull() bool { return v.state == attr.ValueStateNull } func (v RetryValue) IsUnknown() bool { return v.state == attr.ValueStateUnknown } func (v RetryValue) String() string { return "RetryValue" } func (v RetryValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { var diags diag.Diagnostics var errorMessageRegexVal basetypes.ListValue switch { case v.ErrorMessageRegex.IsUnknown(): errorMessageRegexVal = types.ListUnknown(types.StringType) case v.ErrorMessageRegex.IsNull(): errorMessageRegexVal = types.ListNull(types.StringType) default: var d diag.Diagnostics errorMessageRegexVal, d = types.ListValue(types.StringType, v.ErrorMessageRegex.Elements()) diags.Append(d...) } if diags.HasError() { return types.ObjectUnknown(map[string]attr.Type{ "error_message_regex": basetypes.ListType{ ElemType: types.StringType, }, "interval_seconds": basetypes.Int64Type{}, "max_interval_seconds": basetypes.Int64Type{}, "multiplier": basetypes.Float64Type{}, "randomization_factor": basetypes.Float64Type{}, }), diags } attributeTypes := map[string]attr.Type{ "error_message_regex": basetypes.ListType{ ElemType: types.StringType, }, "interval_seconds": basetypes.Int64Type{}, "max_interval_seconds": basetypes.Int64Type{}, "multiplier": basetypes.Float64Type{}, "randomization_factor": basetypes.Float64Type{}, } if v.IsNull() { return types.ObjectNull(attributeTypes), diags } if v.IsUnknown() { return types.ObjectUnknown(attributeTypes), diags } objVal, diags := types.ObjectValue( attributeTypes, map[string]attr.Value{ "error_message_regex": errorMessageRegexVal, "interval_seconds": v.IntervalSeconds, "max_interval_seconds": v.MaxIntervalSeconds, "multiplier": v.Multiplier, "randomization_factor": v.RandomizationFactor, }) return objVal, diags } func (v RetryValue) Equal(o attr.Value) bool { other, ok := o.(RetryValue) if !ok { return false } if v.state != other.state { return false } if v.state != attr.ValueStateKnown { return true } if !v.ErrorMessageRegex.Equal(other.ErrorMessageRegex) { return false } if !v.IntervalSeconds.Equal(other.IntervalSeconds) { return false } if !v.MaxIntervalSeconds.Equal(other.MaxIntervalSeconds) { return false } if !v.Multiplier.Equal(other.Multiplier) { return false } if !v.RandomizationFactor.Equal(other.RandomizationFactor) { return false } return true } func (v RetryValue) Type(ctx context.Context) attr.Type { return RetryType{ basetypes.ObjectType{ AttrTypes: v.AttributeTypes(ctx), }, } } func (v RetryValue) AttributeTypes(ctx context.Context) map[string]attr.Type { return map[string]attr.Type{ "error_message_regex": basetypes.ListType{ ElemType: types.StringType, }, "interval_seconds": basetypes.Int64Type{}, "max_interval_seconds": basetypes.Int64Type{}, "multiplier": basetypes.Float64Type{}, "randomization_factor": basetypes.Float64Type{}, } } func (v RetryValue) GetIntervalSeconds() int { return int(v.IntervalSeconds.ValueInt64()) } func (v RetryValue) GetIntervalSecondsAsDuration() time.Duration { return time.Duration(v.IntervalSeconds.ValueInt64()) * time.Second } func (v RetryValue) GetMaxIntervalSeconds() int { return int(v.MaxIntervalSeconds.ValueInt64()) } func (v RetryValue) GetMaxIntervalSecondsAsDuration() time.Duration { return time.Duration(v.MaxIntervalSeconds.ValueInt64()) * time.Second } func (v RetryValue) GetMultiplier() float64 { return v.Multiplier.ValueFloat64() } func (v RetryValue) GetRandomizationFactor() float64 { return v.RandomizationFactor.ValueFloat64() } func (v RetryValue) GetDefaultRetryableStatusCodes() []int { return defaultRetryableStatusCodes } func (v RetryValue) GetDefaultRetryableReadAfterCreateStatusCodes() []int { return defaultRetryableReadAfterCreateStatusCodes } func (v RetryValue) GetErrorMessages() []string { if v.IsNull() { return nil } if v.IsUnknown() { return nil } res := make([]string, len(v.ErrorMessageRegex.Elements())) for i, elem := range v.ErrorMessageRegex.Elements() { res[i] = elem.(types.String).ValueString() } return res } func (v RetryValue) GetErrorMessagesRegex() []regexp.Regexp { msgs := v.GetErrorMessages() if msgs == nil { return nil } res := make([]regexp.Regexp, len(msgs)) for i, msg := range msgs { res[i] = *regexp.MustCompile(msg) } return res }