Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
807cec4
Add `tfstringvalidator.AlsoRequiresWhenEquals`.
ewbankkit Jul 9, 2026
ab54f96
Internal AlsoRequiresWhenEqualsValidator.
ewbankkit Jul 9, 2026
85ac7bf
tfstringvalidator.AlsoRequiresWhenEquals: Use internal AlsoRequiresWh…
ewbankkit Jul 9, 2026
a4ec442
Simplify.
ewbankkit Jul 9, 2026
893cebc
boolvalidator.AlsoRequiresWhenTrue: Additional test cases.
ewbankkit Jul 9, 2026
5da563a
boolvalidator.AlsoRequiresWhenTrue: Use internal AlsoRequiresWhenEqua…
ewbankkit Jul 9, 2026
091a03d
Reduce visibility.
ewbankkit Jul 10, 2026
5ac9fad
Fix golangci-lint 'whitespace'.
ewbankkit Jul 10, 2026
f810112
r/aws_bedrockagentcore_registry: Use 'tfstringvalidator.AlsoRequiresW…
ewbankkit Jul 10, 2026
afce48c
Tweak error message.
ewbankkit Jul 10, 2026
8630cac
Use 'tfobjectvalidator.ExactlyOneOfChildren'.
ewbankkit Jul 10, 2026
0c8bedd
r/aws_bedrockagentcore_gateway: Use 'tfstringvalidator.AlsoRequiresWh…
ewbankkit Jul 10, 2026
04a3254
Add 'tfstringvalidator.ConflictsWithWhenEquals'.
ewbankkit Jul 10, 2026
dcda959
Simplify.
ewbankkit Jul 10, 2026
1e2e7c1
'WhenEquals' -> 'When' with a condition.
ewbankkit Jul 10, 2026
7730aa8
Add 'internal/framework/planmodifiers/listplanmodifier'.
ewbankkit Jul 10, 2026
d770802
Add 'tfstringvalidator.ConflictsWithWhenNotEquals'.
ewbankkit Jul 10, 2026
10b1fb1
r/aws_bedrockagentcore_memory_strategy: Use 'tfstringvalidator.*'.
ewbankkit Jul 10, 2026
9a3a748
r/aws_bedrockagentcore_memory_strategy: Use 'tflistplanmodifier.Error…
ewbankkit Jul 10, 2026
d4f9581
Add unit tests.
ewbankkit Jul 10, 2026
5b4abee
r/aws_bedrockagentcore_memory_strategy: Fixup acceptance tests.
ewbankkit Jul 10, 2026
f7c5d53
Fix Copilot review comment.
ewbankkit Jul 10, 2026
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: MPL-2.0

package planmodifiers
package listplanmodifier

import (
"context"
Expand All @@ -11,27 +11,27 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)

// ListDefaultValueFromPath returns a plan modifier that sets a list's default value
// DefaultValueFromPath returns a plan modifier that sets a list's default value
// from the planned value at another path.
func ListDefaultValueFromPath[T basetypes.ListValuable](path path.Path) planmodifier.List {
return listDefaultValueFromPath[T]{
func DefaultValueFromPath[T basetypes.ListValuable](path path.Path) planmodifier.List {
return defaultValueFromPath[T]{
path: path,
}
}

type listDefaultValueFromPath[T basetypes.ListValuable] struct {
type defaultValueFromPath[T basetypes.ListValuable] struct {
path path.Path
}

func (m listDefaultValueFromPath[T]) Description(ctx context.Context) string {
func (m defaultValueFromPath[T]) Description(ctx context.Context) string {
return m.MarkdownDescription(ctx)
}

func (m listDefaultValueFromPath[T]) MarkdownDescription(context.Context) string {
func (m defaultValueFromPath[T]) MarkdownDescription(context.Context) string {
return "The default value of this attribute is another attribute's value."
}

func (m listDefaultValueFromPath[T]) PlanModifyList(ctx context.Context, request planmodifier.ListRequest, response *planmodifier.ListResponse) {
func (m defaultValueFromPath[T]) PlanModifyList(ctx context.Context, request planmodifier.ListRequest, response *planmodifier.ListResponse) {
// Do nothing if there is a known planned value.
if !request.PlanValue.IsUnknown() {
return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: MPL-2.0

package planmodifiers_test
package listplanmodifier_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
Expand All @@ -15,10 +14,10 @@ import (
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
fwplanmodifiers "github.com/hashicorp/terraform-provider-aws/internal/framework/planmodifiers"
tflistplanmodifier "github.com/hashicorp/terraform-provider-aws/internal/framework/planmodifiers/listplanmodifier"
)

func TestListDefaultValueFromPath(t *testing.T) {
func TestDefaultValueFromPath(t *testing.T) {
t.Parallel()

type testCase struct {
Expand All @@ -39,18 +38,18 @@ func TestListDefaultValueFromPath(t *testing.T) {
nullState := tfsdk.State{
Schema: testSchema,
Raw: tftypes.NewValue(
testSchema.Type().TerraformType(context.Background()),
testSchema.Type().TerraformType(t.Context()),
nil,
),
}
testPlan := func(src, dst types.List) tfsdk.Plan {
tfSrc, err := src.ToTerraformValue(context.Background())
tfSrc, err := src.ToTerraformValue(t.Context())

if err != nil {
panic("ToTerraformValue error: " + err.Error())
}

tfDst, err := dst.ToTerraformValue(context.Background())
tfDst, err := dst.ToTerraformValue(t.Context())

if err != nil {
panic("ToTerraformValue error: " + err.Error())
Expand All @@ -59,7 +58,7 @@ func TestListDefaultValueFromPath(t *testing.T) {
return tfsdk.Plan{
Schema: testSchema,
Raw: tftypes.NewValue(
testSchema.Type().TerraformType(context.Background()),
testSchema.Type().TerraformType(t.Context()),
map[string]tftypes.Value{
"src": tfSrc,
"dst": tfDst,
Expand All @@ -68,13 +67,13 @@ func TestListDefaultValueFromPath(t *testing.T) {
}
}
testState := func(src, dst types.List) tfsdk.State {
tfSrc, err := src.ToTerraformValue(context.Background())
tfSrc, err := src.ToTerraformValue(t.Context())

if err != nil {
panic("ToTerraformValue error: " + err.Error())
}

tfDst, err := dst.ToTerraformValue(context.Background())
tfDst, err := dst.ToTerraformValue(t.Context())

if err != nil {
panic("ToTerraformValue error: " + err.Error())
Expand All @@ -83,7 +82,7 @@ func TestListDefaultValueFromPath(t *testing.T) {
return tfsdk.State{
Schema: testSchema,
Raw: tftypes.NewValue(
testSchema.Type().TerraformType(context.Background()),
testSchema.Type().TerraformType(t.Context()),
map[string]tftypes.Value{
"src": tfSrc,
"dst": tfDst,
Expand Down Expand Up @@ -171,7 +170,7 @@ func TestListDefaultValueFromPath(t *testing.T) {
response := planmodifier.ListResponse{
PlanValue: test.request.PlanValue,
}
fwplanmodifiers.ListDefaultValueFromPath[types.List](path.Root("src")).PlanModifyList(context.Background(), test.request, &response)
tflistplanmodifier.DefaultValueFromPath[types.List](path.Root("src")).PlanModifyList(t.Context(), test.request, &response)

if diff := cmp.Diff(test.expected, response); diff != "" {
t.Errorf("unexpected diff (+wanted, -got): %s", diff)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ package boolvalidator

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-aws/internal/framework/validators/internal"
)

// AlsoRequiresWhenTrue checks that each path.Expression has a non-null
Expand All @@ -26,69 +26,10 @@ import (
// Relative path.Expressions are resolved using the attribute being
// validated.
func AlsoRequiresWhenTrue(expressions ...path.Expression) validator.Bool {
return alsoRequiresWhenTrueValidator{
pathExpressions: expressions,
}
}

type alsoRequiresWhenTrueValidator struct {
pathExpressions path.Expressions
}

func (v alsoRequiresWhenTrueValidator) Description(ctx context.Context) string {
return v.MarkdownDescription(ctx)
}

func (v alsoRequiresWhenTrueValidator) MarkdownDescription(_ context.Context) string {
return fmt.Sprintf("Ensure that when this attribute is true, the following are also configured: %q", v.pathExpressions)
}

func (v alsoRequiresWhenTrueValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) {
// If the gating attribute is not configured, the condition cannot be
// "true," so there is nothing to enforce. If it is unknown, defer
// validation until it becomes known.
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}

// Only fire when the gating attribute is true.
if !req.ConfigValue.ValueBool() {
return
}

expressions := req.PathExpression.MergeExpressions(v.pathExpressions...)

for _, expression := range expressions {
matchedPaths, diags := req.Config.PathMatches(ctx, expression)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
continue
}

for _, mp := range matchedPaths {
// Skip self.
if mp.Equal(req.Path) {
continue
}

var mpVal attr.Value
diags := req.Config.GetAttribute(ctx, mp, &mpVal)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
continue
}

// Defer if any target is unknown; we cannot decide yet.
if mpVal.IsUnknown() {
return
}

if mpVal.IsNull() {
resp.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
req.Path,
fmt.Sprintf("Attribute %q must be specified when %q is true", mp, req.Path),
))
}
}
return internal.AlsoRequiresWhenValidator{
When: func(_ context.Context, v attr.Value) bool {
return v.Equal(types.BoolValue(true))
},
PathExpressions: expressions,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,48 @@ func TestAlsoRequiresWhenTrue(t *testing.T) {
expressions: []path.Expression{path.MatchRoot("a"), path.MatchRoot("b")},
expErrors: 1,
},
"self-true-multiple-targets-other-null": {
configValue: types.BoolValue(true),
config: func(t *testing.T) tfsdk.Config { return testConfig(t, true, nil, "value") },
expressions: []path.Expression{path.MatchRoot("a"), path.MatchRoot("b")},
expErrors: 1,
},
"self-true-multiple-targets-all-null": {
configValue: types.BoolValue(true),
config: func(t *testing.T) tfsdk.Config { return testConfig(t, true, nil, nil) },
expressions: []path.Expression{path.MatchRoot("a"), path.MatchRoot("b")},
expErrors: 2,
},
"self-true-multiple-targets-all-set": {
configValue: types.BoolValue(true),
config: func(t *testing.T) tfsdk.Config { return testConfig(t, true, "valueA", "valueB") },
expressions: []path.Expression{path.MatchRoot("a"), path.MatchRoot("b")},
expErrors: 0,
},
"self-true-multiple-targets-one-unknown-other-null": {
configValue: types.BoolValue(true),
config: func(t *testing.T) tfsdk.Config {
return testConfig(t, true, tftypes.UnknownValue, nil)
},
expressions: []path.Expression{path.MatchRoot("a"), path.MatchRoot("b")},
expErrors: 0,
},
"self-true-multiple-targets-one-null-other-unknown": {
configValue: types.BoolValue(true),
config: func(t *testing.T) tfsdk.Config {
return testConfig(t, true, nil, tftypes.UnknownValue)
},
expressions: []path.Expression{path.MatchRoot("a"), path.MatchRoot("b")},
expErrors: 0,
},
"self-true-multiple-targets-all-unknown": {
configValue: types.BoolValue(true),
config: func(t *testing.T) tfsdk.Config {
return testConfig(t, true, tftypes.UnknownValue, tftypes.UnknownValue)
},
expressions: []path.Expression{path.MatchRoot("a"), path.MatchRoot("b")},
expErrors: 0,
},
"self-true-self-reference-ignored": {
// pointing at self is a no-op (does not crash or flag)
configValue: types.BoolValue(true),
Expand Down
109 changes: 109 additions & 0 deletions internal/framework/validators/internal/also_requires_when.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: MPL-2.0

package internal

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
"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/schema/validator"
)

var (
_ validator.Bool = (*AlsoRequiresWhenValidator)(nil)
_ validator.String = (*AlsoRequiresWhenValidator)(nil)
)

type AlsoRequiresWhenValidator struct {
When func(context.Context, attr.Value) bool
PathExpressions path.Expressions
}

func (v AlsoRequiresWhenValidator) Description(ctx context.Context) string {
return v.MarkdownDescription(ctx)
}

func (v AlsoRequiresWhenValidator) MarkdownDescription(ctx context.Context) string {
return fmt.Sprintf("Ensure that when this attribute value matches the condition, the following are also configured: %[1]q", v.PathExpressions)
}

func (v AlsoRequiresWhenValidator) ValidateBool(ctx context.Context, request validator.BoolRequest, response *validator.BoolResponse) {
validateRequest := ValidatorRequest{
Config: request.Config,
ConfigValue: request.ConfigValue,
Path: request.Path,
PathExpression: request.PathExpression,
}
var validateResponse ValidatorResponse

v.validate(ctx, validateRequest, &validateResponse)

response.Diagnostics.Append(validateResponse.Diagnostics...)
}

func (v AlsoRequiresWhenValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) {
validateRequest := ValidatorRequest{
Config: request.Config,
ConfigValue: request.ConfigValue,
Path: request.Path,
PathExpression: request.PathExpression,
}
var validateResponse ValidatorResponse

v.validate(ctx, validateRequest, &validateResponse)

response.Diagnostics.Append(validateResponse.Diagnostics...)
}

func (v AlsoRequiresWhenValidator) validate(ctx context.Context, request ValidatorRequest, response *ValidatorResponse) {
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
return
}

if !v.When(ctx, request.ConfigValue) {
return
}

var responseDiags diag.Diagnostics

for _, expression := range request.PathExpression.MergeExpressions(v.PathExpressions...) {
matchedPaths, diags := request.Config.PathMatches(ctx, expression)
response.Diagnostics.Append(diags...)
if diags.HasError() {
continue
}

for _, mp := range matchedPaths {
// Skip self.
if mp.Equal(request.Path) {
continue
}

var mpVal attr.Value
response.Diagnostics.Append(request.Config.GetAttribute(ctx, mp, &mpVal)...)
if response.Diagnostics.HasError() {
return
}

// Defer if any target is unknown; we cannot decide yet.
if mpVal.IsUnknown() {
return
}

if mpVal.IsNull() {
// Collect all errors.
responseDiags.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
request.Path,
fmt.Sprintf("Attribute %[1]q must be configured", mp),
))
}
}
}

response.Diagnostics.Append(responseDiags...)
}
Loading