223 lines
7.6 KiB
Go
223 lines
7.6 KiB
Go
package validators
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
|
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
|
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
|
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
|
)
|
|
|
|
var (
|
|
_ datasource.ConfigValidator = &RequiredTogetherIfValidator{}
|
|
_ provider.ConfigValidator = &RequiredTogetherIfValidator{}
|
|
_ resource.ConfigValidator = &RequiredTogetherIfValidator{}
|
|
_ validator.Object = &RequiredTogetherIfValidator{}
|
|
_ validator.String = &RequiredTogetherIfValidator{}
|
|
_ validator.Bool = &RequiredTogetherIfValidator{}
|
|
)
|
|
|
|
type RequiredTogetherIfValidator struct {
|
|
ConditionPath path.Expression
|
|
ConditionValue attr.Value
|
|
TargetExpressions path.Expressions
|
|
CheckOnlyIfSet bool // When true, only checks if the condition value is set (not null), not its actual value
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) {
|
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) Description(ctx context.Context) string {
|
|
return v.MarkdownDescription(ctx)
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) MarkdownDescription(_ context.Context) string {
|
|
if v.CheckOnlyIfSet {
|
|
return fmt.Sprintf("If %s is set, these attributes must be configured together: %s", v.ConditionPath, v.TargetExpressions)
|
|
}
|
|
return fmt.Sprintf("If %s equals %s, these attributes must be configured together: %s", v.ConditionPath, v.ConditionValue, v.TargetExpressions)
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
|
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) {
|
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
|
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
|
|
resp.Diagnostics.Append(v.Validate(ctx, req.Config)...)
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) shouldValidate(ctx context.Context, config tfsdk.Config) bool {
|
|
// First check the condition attribute's value
|
|
matchedPaths, matchedPathsDiags := config.PathMatches(ctx, v.ConditionPath)
|
|
if matchedPathsDiags.HasError() || len(matchedPaths) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Get the value of the condition attribute
|
|
var conditionValue attr.Value
|
|
getConditionDiags := config.GetAttribute(ctx, matchedPaths[0], &conditionValue)
|
|
if getConditionDiags.HasError() {
|
|
return false
|
|
}
|
|
|
|
// If the condition attribute is null or unknown, skip validation
|
|
if conditionValue.IsNull() || conditionValue.IsUnknown() {
|
|
return false
|
|
}
|
|
|
|
// Check if the condition matches
|
|
if v.CheckOnlyIfSet {
|
|
return !conditionValue.IsNull()
|
|
}
|
|
return conditionValueMatches(ctx, conditionValue, v.ConditionValue)
|
|
}
|
|
|
|
func (v RequiredTogetherIfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics {
|
|
diags := diag.Diagnostics{}
|
|
if !v.shouldValidate(ctx, config) {
|
|
return diags
|
|
}
|
|
|
|
// Condition matched, now apply the RequiredTogether validation
|
|
configuredPaths := path.Paths{}
|
|
foundPaths := path.Paths{}
|
|
unknownPaths := path.Paths{}
|
|
|
|
// Check that all target attributes are present
|
|
for _, expression := range v.TargetExpressions {
|
|
matchedPaths, matchedPathsDiags := config.PathMatches(ctx, expression)
|
|
diags.Append(matchedPathsDiags...)
|
|
|
|
// Collect all errors
|
|
if matchedPathsDiags.HasError() {
|
|
continue
|
|
}
|
|
|
|
// Capture all matched paths
|
|
foundPaths.Append(matchedPaths...)
|
|
|
|
for _, matchedPath := range matchedPaths {
|
|
var value attr.Value
|
|
getAttributeDiags := config.GetAttribute(ctx, matchedPath, &value)
|
|
diags.Append(getAttributeDiags...)
|
|
|
|
// Collect all errors
|
|
if getAttributeDiags.HasError() {
|
|
continue
|
|
}
|
|
|
|
// If value is unknown, collect the path to skip validation later
|
|
if value.IsUnknown() {
|
|
unknownPaths.Append(matchedPath)
|
|
continue
|
|
}
|
|
|
|
// If value is null, move onto the next one
|
|
if value.IsNull() {
|
|
continue
|
|
}
|
|
|
|
// Value is known and not null, it is configured
|
|
configuredPaths.Append(matchedPath)
|
|
}
|
|
}
|
|
|
|
// Return early if all paths were null
|
|
//if len(configuredPaths) == 0 {
|
|
// return diags
|
|
//}
|
|
|
|
// If there are unknown values, we cannot know if the validator should
|
|
// succeed or not
|
|
if len(unknownPaths) > 0 {
|
|
return diags
|
|
}
|
|
|
|
// If configured paths does not equal all matched paths, then something
|
|
// was missing
|
|
if len(configuredPaths) != len(foundPaths) {
|
|
diags.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
|
|
foundPaths[0],
|
|
v.Description(ctx),
|
|
))
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
// ValidateString method to implement the validator.String interface
|
|
func (v RequiredTogetherIfValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
|
resp.Diagnostics.Append(v.Validate(ctx, req.Config)...)
|
|
}
|
|
|
|
// RequiredTogetherIf creates a validator for string type attributes that ensures
|
|
// a set of target attributes are configured together if a condition attribute equals a specific value.
|
|
func RequiredTogetherIf(conditionPath path.Expression, conditionValue attr.Value, targetExpressions ...path.Expression) RequiredTogetherIfValidator {
|
|
return RequiredTogetherIfValidator{
|
|
ConditionPath: conditionPath,
|
|
ConditionValue: conditionValue,
|
|
TargetExpressions: targetExpressions,
|
|
CheckOnlyIfSet: false,
|
|
}
|
|
}
|
|
|
|
func mapPathToExpression(p string) path.Expression {
|
|
parts := strings.Split(p, ".")
|
|
root := parts[0]
|
|
exp := path.MatchRoot(root)
|
|
for _, part := range parts[1:] {
|
|
exp = exp.AtName(part)
|
|
}
|
|
return exp
|
|
}
|
|
|
|
func mapPathsToExpressions(paths ...string) []path.Expression {
|
|
var expressions []path.Expression
|
|
for _, p := range paths {
|
|
expressions = append(expressions, mapPathToExpression(p))
|
|
}
|
|
return expressions
|
|
}
|
|
|
|
func RequiredSimpleTogetherIf(conditionPath string, conditionValue attr.Value, targetExpressions ...string) RequiredTogetherIfValidator {
|
|
return RequiredTogetherIfValidator{
|
|
ConditionPath: mapPathToExpression(conditionPath),
|
|
ConditionValue: conditionValue,
|
|
TargetExpressions: mapPathsToExpressions(targetExpressions...),
|
|
CheckOnlyIfSet: false,
|
|
}
|
|
}
|
|
|
|
// RequiredTogetherIfSet creates a validator that ensures a set of target attributes
|
|
// are configured together if a condition attribute is set (not null), regardless of its value.
|
|
func RequiredTogetherIfSet(conditionPath path.Expression, targetExpressions ...path.Expression) RequiredTogetherIfValidator {
|
|
return RequiredTogetherIfValidator{
|
|
ConditionPath: conditionPath,
|
|
ConditionValue: nil, // Not used for this validator
|
|
TargetExpressions: targetExpressions,
|
|
CheckOnlyIfSet: true,
|
|
}
|
|
}
|