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 }