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

all: Add support for unknown value refinement data to all types #1062

Draft
wants to merge 41 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
64d9274
quick fix on equals for later
austinvalle Oct 21, 2024
8a8f3fc
test fixes
austinvalle Oct 24, 2024
0d0f4fc
not null and string prefix implementations
austinvalle Oct 24, 2024
15121c4
add number bounds
austinvalle Oct 25, 2024
ec7cc42
Merge branch 'main' into av/refinements
austinvalle Nov 5, 2024
dfdef53
switch back to local - go mod tidy
austinvalle Nov 5, 2024
dcd19d9
switched up interface
austinvalle Nov 21, 2024
d67bd4a
Merge branch 'main' into av/refinements
austinvalle Nov 26, 2024
31d81ad
bump versions
austinvalle Nov 26, 2024
f297cce
update existing docs + equal/string methods
austinvalle Nov 26, 2024
4de17f2
new refinements
austinvalle Nov 26, 2024
0b35094
var name
austinvalle Nov 26, 2024
53802ef
update string type and value
austinvalle Nov 26, 2024
4d923dc
string tests
austinvalle Nov 26, 2024
e242cac
clean up int64 value and type, add tests
austinvalle Nov 26, 2024
12d8bb8
int32 refinements
austinvalle Nov 27, 2024
9f965c4
float64 refinements
austinvalle Nov 27, 2024
c65976c
float 32 refinements
austinvalle Nov 27, 2024
ab222bc
variable change
austinvalle Nov 27, 2024
82ad8e7
number refinements
austinvalle Nov 27, 2024
cfa53db
bool refinements
austinvalle Nov 27, 2024
cdaf3d9
object refinements
austinvalle Nov 27, 2024
cd8d8ed
tuple refinements
austinvalle Nov 27, 2024
54b5cfb
list refinements
austinvalle Nov 27, 2024
b4eee34
set refinements
austinvalle Dec 2, 2024
093adf2
fix testtype
austinvalle Dec 2, 2024
3fce8d5
map refinements
austinvalle Dec 2, 2024
cc81de3
order comment
austinvalle Dec 2, 2024
c6333be
string plan modifiers
austinvalle Dec 2, 2024
18d08f9
int64 plan modifiers
austinvalle Dec 2, 2024
6063b92
int32 plan modifiers
austinvalle Dec 2, 2024
7fd8967
float64 plan modifiers
austinvalle Dec 2, 2024
b52b2c8
float32 plan modifiers
austinvalle Dec 2, 2024
b45f8af
number plan modifiers
austinvalle Dec 2, 2024
2a7b3dd
object and bool plan modifiers
austinvalle Dec 2, 2024
411df8d
list plan modifiers
austinvalle Dec 2, 2024
972929c
map and set plan modifiers
austinvalle Dec 2, 2024
b59f093
add tests for not null refinement
austinvalle Dec 2, 2024
96d9ccf
spelling
austinvalle Dec 3, 2024
a2d4066
switch to commit hash
austinvalle Dec 3, 2024
a85b9d5
fix custom type implementations
austinvalle Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions attr/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package attr
import (
"context"

"github.com/hashicorp/terraform-plugin-framework/types/refinement"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

Expand Down Expand Up @@ -69,3 +70,17 @@ type Value interface {
// compatibility guarantees within the framework.
String() string
}

// ValueWithNotNullRefinement defines an interface describing a Value that can contain
// a refinement that indicates the Value is unknown, but will not be null once it becomes known.
//
// This interface is implemented by all base value types except for DynamicValue, as dynamic types
// in Terraform don't support value refinements.
type ValueWithNotNullRefinement interface {
Value

// NotNullRefinement returns value refinement data and a boolean indicating if a NotNull refinement
// exists on the given Value. If a Value contains a NotNull refinement, this indicates that the value
// is unknown, but the eventual known value will not be null.
NotNullRefinement() (*refinement.NotNull, bool)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ toolchain go1.22.7

require (
github.com/google/go-cmp v0.6.0
github.com/hashicorp/terraform-plugin-go v0.25.0
github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241126200214-bd716fcfe407
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: update with released version

github.com/hashicorp/terraform-plugin-log v0.9.0
)

Expand All @@ -30,5 +30,5 @@ require (
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
google.golang.org/protobuf v1.35.2 // indirect
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8Ei
github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks=
github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw=
github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241126200214-bd716fcfe407 h1:oLzKb+YiJIEq0EY3qGgQTxCLW2CaXN1rJp3yg1H11qI=
github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241126200214-bd716fcfe407/go.mod h1:f8P2pHGkZrtdKLpCI2qIvrewUY+c4nTvtayqjJR9IcY=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
Expand Down Expand Up @@ -64,8 +64,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
67 changes: 41 additions & 26 deletions internal/fwserver/attribute_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)

// ValidateAttributeRequest repesents a request for attribute validation.
// ValidateAttributeRequest represents a request for attribute validation.
type ValidateAttributeRequest struct {
// AttributePath contains the path of the attribute. Use this path for any
// response diagnostics.
Expand Down Expand Up @@ -137,33 +137,48 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt

AttributeValidateNestedAttributes(ctx, a, req, resp)

// Show deprecation warnings only for known values.
if a.GetDeprecationMessage() != "" && !attributeConfig.IsNull() && !attributeConfig.IsUnknown() {
// Dynamic values need to perform more logic to check the config value for null/unknown-ness
dynamicValuable, ok := attributeConfig.(basetypes.DynamicValuable)
if !ok {
resp.Diagnostics.AddAttributeWarning(
req.AttributePath,
"Attribute Deprecated",
a.GetDeprecationMessage(),
)
return
}
// Show deprecation warnings only for known values or unknown values with a "not null" refinement.
if a.GetDeprecationMessage() != "" {
if attributeConfig.IsUnknown() {
// If the unknown value will eventually be not null, we return the deprecation message for the practitioner.
val, ok := attributeConfig.(attr.ValueWithNotNullRefinement)
if ok {
if _, notNull := val.NotNullRefinement(); notNull {
resp.Diagnostics.AddAttributeWarning(
req.AttributePath,
"Attribute Deprecated",
a.GetDeprecationMessage(),
)
return
}
}
} else if !attributeConfig.IsNull() && !attributeConfig.IsUnknown() {
// Dynamic values need to perform more logic to check the config value for null/unknown-ness
dynamicValuable, ok := attributeConfig.(basetypes.DynamicValuable)
if !ok {
resp.Diagnostics.AddAttributeWarning(
req.AttributePath,
"Attribute Deprecated",
a.GetDeprecationMessage(),
)
return
}

dynamicConfigVal, diags := dynamicValuable.ToDynamicValue(ctx)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
dynamicConfigVal, diags := dynamicValuable.ToDynamicValue(ctx)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}

// For dynamic values, it's possible to be known when only the type is known.
// The underlying value can still be null or unknown, so check for that here
if !dynamicConfigVal.IsUnderlyingValueNull() && !dynamicConfigVal.IsUnderlyingValueUnknown() {
resp.Diagnostics.AddAttributeWarning(
req.AttributePath,
"Attribute Deprecated",
a.GetDeprecationMessage(),
)
// For dynamic values, it's possible to be known when only the type is known.
// The underlying value can still be null or unknown, so check for that here
if !dynamicConfigVal.IsUnderlyingValueNull() && !dynamicConfigVal.IsUnderlyingValueUnknown() {
resp.Diagnostics.AddAttributeWarning(
req.AttributePath,
"Attribute Deprecated",
a.GetDeprecationMessage(),
)
}
}
}
}
Expand Down
35 changes: 35 additions & 0 deletions internal/fwserver/attribute_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-go/tftypes"
tfrefinement "github.com/hashicorp/terraform-plugin-go/tftypes/refinement"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
Expand Down Expand Up @@ -490,6 +491,40 @@ func TestAttributeValidate(t *testing.T) {
},
resp: ValidateAttributeResponse{},
},
"deprecation-message-unknown-with-not-null-refinement": {
req: ValidateAttributeRequest{
AttributePath: path.Root("test"),
Config: tfsdk.Config{
Raw: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test": tftypes.String,
},
}, map[string]tftypes.Value{
"test": tftypes.NewValue(tftypes.String, tftypes.UnknownValue).Refine(tfrefinement.Refinements{
tfrefinement.KeyNullness: tfrefinement.NewNullness(false),
}),
}),
Schema: testschema.Schema{
Attributes: map[string]fwschema.Attribute{
"test": testschema.Attribute{
Type: types.StringType,
Optional: true,
DeprecationMessage: "Use something else instead.",
},
},
},
},
},
resp: ValidateAttributeResponse{
Diagnostics: diag.Diagnostics{
diag.NewAttributeWarningDiagnostic(
path.Root("test"),
"Attribute Deprecated",
"Use something else instead.",
),
},
},
},
"deprecation-message-dynamic-underlying-value-unknown": {
req: ValidateAttributeRequest{
AttributePath: path.Root("test"),
Expand Down
6 changes: 3 additions & 3 deletions internal/testing/testtypes/numberwithvalidateattribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (v NumberValueWithValidateAttributeError) Equal(value attr.Value) bool {
return false
}

return v == other
return v.Equal(other)
}

func (v NumberValueWithValidateAttributeError) IsNull() bool {
Expand Down Expand Up @@ -92,7 +92,7 @@ func (t NumberTypeWithValidateAttributeWarning) Equal(o attr.Type) bool {
if !ok {
return false
}
return t == other
return t.NumberType.Equal(other.NumberType)
}

func (t NumberTypeWithValidateAttributeWarning) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
Expand Down Expand Up @@ -134,7 +134,7 @@ func (v NumberValueWithValidateAttributeWarning) Equal(value attr.Value) bool {
return false
}

return v.InternalNumber.Number.Equal(other.InternalNumber.Number)
return v.InternalNumber.Equal(other.InternalNumber)
}

func (v NumberValueWithValidateAttributeWarning) IsNull() bool {
Expand Down
4 changes: 2 additions & 2 deletions internal/testing/testtypes/stringwithvalidateattribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (v StringValueWithValidateAttributeError) Equal(value attr.Value) bool {
return false
}

return v == other
return v.InternalString.Equal(other.InternalString)
}

func (v StringValueWithValidateAttributeError) IsNull() bool {
Expand Down Expand Up @@ -134,7 +134,7 @@ func (v StringValueWithValidateAttributeWarning) Equal(value attr.Value) bool {
return false
}

return v == other
return v.InternalString.Equal(other.InternalString)
}

func (v StringValueWithValidateAttributeWarning) IsNull() bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (v StringValueWithValidateParameterError) Equal(value attr.Value) bool {
return false
}

return v == other
return v.InternalString.Equal(other.InternalString)
}

func (v StringValueWithValidateParameterError) IsNull() bool {
Expand Down
50 changes: 50 additions & 0 deletions resource/schema/boolplanmodifier/will_not_be_null.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package boolplanmodifier

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

// WillNotBeNull returns a plan modifier that will add a refinement to an unknown planned value
// which promises that the final value will not be null.
//
// This unknown value refinement allows Terraform to validate more of the configuration during plan
// and evaluate conditional logic in meta-arguments such as "count":
//
// resource "examplecloud_thing" "b" {
// // Will successfully evaluate during plan with a "not null" refinement on "bool_attribute"
// count = examplecloud_thing.a.bool_attribute != null ? 1 : 0
//
// // .. resource config
// }
func WillNotBeNull() planmodifier.Bool {
return willNotBeNullModifier{}
}

type willNotBeNullModifier struct{}

func (m willNotBeNullModifier) Description(_ context.Context) string {
return "Promises the value of this attribute will not be null once it becomes known"
}

func (m willNotBeNullModifier) MarkdownDescription(_ context.Context) string {
return "Promises the value of this attribute will not be null once it becomes known"
}

func (m willNotBeNullModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
// Do nothing if there is a known planned value.
if !req.PlanValue.IsUnknown() {
return
}

// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsUnknown() {
return
}

resp.PlanValue = req.PlanValue.RefineAsNotNull()
}
88 changes: 88 additions & 0 deletions resource/schema/boolplanmodifier/will_not_be_null_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package boolplanmodifier_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func TestWillNotBeNullModifierPlanModifyBool(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
request planmodifier.BoolRequest
expected *planmodifier.BoolResponse
}{
"known-plan": {
request: planmodifier.BoolRequest{
StateValue: types.BoolValue(false),
PlanValue: types.BoolValue(true),
ConfigValue: types.BoolNull(),
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolValue(true),
},
},
"unknown-config": {
// this is the situation in which a user is
// interpolating into a field. We want that to still
// show up as unknown (with no refinement), otherwise they'll
// get apply-time errors for changing the value even though
// we knew it was legitimately possible for it to change and the
// provider can't prevent this from happening
request: planmodifier.BoolRequest{
StateValue: types.BoolValue(true),
PlanValue: types.BoolUnknown(),
ConfigValue: types.BoolUnknown(),
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolUnknown(),
},
},
"unknown-plan-null-state": {
request: planmodifier.BoolRequest{
StateValue: types.BoolNull(),
PlanValue: types.BoolUnknown(),
ConfigValue: types.BoolNull(),
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolUnknown().RefineAsNotNull(),
},
},
"unknown-plan-non-null-state": {
request: planmodifier.BoolRequest{
StateValue: types.BoolValue(true),
PlanValue: types.BoolUnknown(),
ConfigValue: types.BoolNull(),
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolUnknown().RefineAsNotNull(),
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

resp := &planmodifier.BoolResponse{
PlanValue: testCase.request.PlanValue,
}

boolplanmodifier.WillNotBeNull().PlanModifyBool(context.Background(), testCase.request, resp)

if diff := cmp.Diff(testCase.expected, resp); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
Loading
Loading