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 }