github.com/opentofu/opentofu@v1.7.1/internal/configs/configschema/internal_validate.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package configschema
     7  
     8  import (
     9  	"fmt"
    10  	"regexp"
    11  
    12  	"github.com/zclconf/go-cty/cty"
    13  
    14  	multierror "github.com/hashicorp/go-multierror"
    15  )
    16  
    17  var validName = regexp.MustCompile(`^[a-z0-9_]+$`)
    18  
    19  // InternalValidate returns an error if the receiving block and its child schema
    20  // definitions have any inconsistencies with the documented rules for valid
    21  // schema.
    22  //
    23  // This can be used within unit tests to detect when a given schema is invalid,
    24  // and is run when tofu loads provider schemas during NewContext.
    25  func (b *Block) InternalValidate() error {
    26  	if b == nil {
    27  		return fmt.Errorf("top-level block schema is nil")
    28  	}
    29  	return b.internalValidate("")
    30  }
    31  
    32  func (b *Block) internalValidate(prefix string) error {
    33  	var multiErr *multierror.Error
    34  
    35  	for name, attrS := range b.Attributes {
    36  		if attrS == nil {
    37  			multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: attribute schema is nil", prefix, name))
    38  			continue
    39  		}
    40  		multiErr = multierror.Append(multiErr, attrS.internalValidate(name, prefix))
    41  	}
    42  
    43  	for name, blockS := range b.BlockTypes {
    44  		if blockS == nil {
    45  			multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: block schema is nil", prefix, name))
    46  			continue
    47  		}
    48  
    49  		if _, isAttr := b.Attributes[name]; isAttr {
    50  			multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: name defined as both attribute and child block type", prefix, name))
    51  		} else if !validName.MatchString(name) {
    52  			multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name))
    53  		}
    54  
    55  		if blockS.MinItems < 0 || blockS.MaxItems < 0 {
    56  			multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must both be greater than zero", prefix, name))
    57  		}
    58  
    59  		switch blockS.Nesting {
    60  		case NestingSingle:
    61  			switch {
    62  			case blockS.MinItems != blockS.MaxItems:
    63  				multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must match in NestingSingle mode", prefix, name))
    64  			case blockS.MinItems < 0 || blockS.MinItems > 1:
    65  				multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must be set to either 0 or 1 in NestingSingle mode", prefix, name))
    66  			}
    67  		case NestingGroup:
    68  			if blockS.MinItems != 0 || blockS.MaxItems != 0 {
    69  				multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems cannot be used in NestingGroup mode", prefix, name))
    70  			}
    71  		case NestingList, NestingSet:
    72  			if blockS.MinItems > blockS.MaxItems && blockS.MaxItems != 0 {
    73  				multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems must be less than or equal to MaxItems in %s mode", prefix, name, blockS.Nesting))
    74  			}
    75  			if blockS.Nesting == NestingSet {
    76  				ety := blockS.Block.ImpliedType()
    77  				if ety.HasDynamicTypes() {
    78  					// This is not permitted because the HCL (cty) set implementation
    79  					// needs to know the exact type of set elements in order to
    80  					// properly hash them, and so can't support mixed types.
    81  					multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: NestingSet blocks may not contain attributes of cty.DynamicPseudoType", prefix, name))
    82  				}
    83  			}
    84  		case NestingMap:
    85  			if blockS.MinItems != 0 || blockS.MaxItems != 0 {
    86  				multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must both be 0 in NestingMap mode", prefix, name))
    87  			}
    88  		default:
    89  			multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, blockS.Nesting))
    90  		}
    91  
    92  		subPrefix := prefix + name + "."
    93  		multiErr = multierror.Append(multiErr, blockS.Block.internalValidate(subPrefix))
    94  	}
    95  
    96  	return multiErr.ErrorOrNil()
    97  }
    98  
    99  // InternalValidate returns an error if the receiving attribute and its child
   100  // schema definitions have any inconsistencies with the documented rules for
   101  // valid schema.
   102  func (a *Attribute) InternalValidate(name string) error {
   103  	if a == nil {
   104  		return fmt.Errorf("attribute schema is nil")
   105  	}
   106  	return a.internalValidate(name, "")
   107  }
   108  
   109  func (a *Attribute) internalValidate(name, prefix string) error {
   110  	var err *multierror.Error
   111  
   112  	/* FIXME: this validation breaks certain existing providers and cannot be enforced without coordination.
   113  	if !validName.MatchString(name) {
   114  		err = multierror.Append(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name))
   115  	}
   116  	*/
   117  	if !a.Optional && !a.Required && !a.Computed {
   118  		err = multierror.Append(err, fmt.Errorf("%s%s: must set Optional, Required or Computed", prefix, name))
   119  	}
   120  	if a.Optional && a.Required {
   121  		err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Optional and Required", prefix, name))
   122  	}
   123  	if a.Computed && a.Required {
   124  		err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Computed and Required", prefix, name))
   125  	}
   126  
   127  	if a.Type == cty.NilType && a.NestedType == nil {
   128  		err = multierror.Append(err, fmt.Errorf("%s%s: either Type or NestedType must be defined", prefix, name))
   129  	}
   130  
   131  	if a.Type != cty.NilType {
   132  		if a.NestedType != nil {
   133  			err = multierror.Append(err, fmt.Errorf("%s: Type and NestedType cannot both be set", name))
   134  		}
   135  	}
   136  
   137  	if a.NestedType != nil {
   138  		switch a.NestedType.Nesting {
   139  		case NestingSingle, NestingMap:
   140  			// no validations to perform
   141  		case NestingList, NestingSet:
   142  			if a.NestedType.Nesting == NestingSet {
   143  				ety := a.ImpliedType()
   144  				if ety.HasDynamicTypes() {
   145  					// This is not permitted because the HCL (cty) set implementation
   146  					// needs to know the exact type of set elements in order to
   147  					// properly hash them, and so can't support mixed types.
   148  					err = multierror.Append(err, fmt.Errorf("%s%s: NestingSet blocks may not contain attributes of cty.DynamicPseudoType", prefix, name))
   149  				}
   150  			}
   151  		default:
   152  			err = multierror.Append(err, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, a.NestedType.Nesting))
   153  		}
   154  		for name, attrS := range a.NestedType.Attributes {
   155  			if attrS == nil {
   156  				err = multierror.Append(err, fmt.Errorf("%s%s: attribute schema is nil", prefix, name))
   157  				continue
   158  			}
   159  			err = multierror.Append(err, attrS.internalValidate(name, prefix))
   160  		}
   161  	}
   162  
   163  	return err.ErrorOrNil()
   164  }