github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/configschema/internal_validate.go (about)

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