kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/configs/module_call.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/hashicorp/hcl/v2/gohcl" 8 "github.com/hashicorp/hcl/v2/hclsyntax" 9 "kubeform.dev/terraform-backend-sdk/addrs" 10 "kubeform.dev/terraform-backend-sdk/getmodules" 11 ) 12 13 // ModuleCall represents a "module" block in a module or file. 14 type ModuleCall struct { 15 Name string 16 17 SourceAddr addrs.ModuleSource 18 SourceAddrRaw string 19 SourceAddrRange hcl.Range 20 SourceSet bool 21 22 Config hcl.Body 23 24 Version VersionConstraint 25 26 Count hcl.Expression 27 ForEach hcl.Expression 28 29 Providers []PassedProviderConfig 30 31 DependsOn []hcl.Traversal 32 33 DeclRange hcl.Range 34 } 35 36 func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagnostics) { 37 var diags hcl.Diagnostics 38 39 mc := &ModuleCall{ 40 Name: block.Labels[0], 41 DeclRange: block.DefRange, 42 } 43 44 schema := moduleBlockSchema 45 if override { 46 schema = schemaForOverrides(schema) 47 } 48 49 content, remain, moreDiags := block.Body.PartialContent(schema) 50 diags = append(diags, moreDiags...) 51 mc.Config = remain 52 53 if !hclsyntax.ValidIdentifier(mc.Name) { 54 diags = append(diags, &hcl.Diagnostic{ 55 Severity: hcl.DiagError, 56 Summary: "Invalid module instance name", 57 Detail: badIdentifierDetail, 58 Subject: &block.LabelRanges[0], 59 }) 60 } 61 62 if attr, exists := content.Attributes["source"]; exists { 63 mc.SourceSet = true 64 mc.SourceAddrRange = attr.Expr.Range() 65 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &mc.SourceAddrRaw) 66 diags = append(diags, valDiags...) 67 if !valDiags.HasErrors() { 68 addr, err := addrs.ParseModuleSource(mc.SourceAddrRaw) 69 mc.SourceAddr = addr 70 if err != nil { 71 // NOTE: In practice it's actually very unlikely to end up here, 72 // because our source address parser can turn just about any string 73 // into some sort of remote package address, and so for most errors 74 // we'll detect them only during module installation. There are 75 // still a _few_ purely-syntax errors we can catch at parsing time, 76 // though, mostly related to remote package sub-paths and local 77 // paths. 78 switch err := err.(type) { 79 case *getmodules.MaybeRelativePathErr: 80 diags = append(diags, &hcl.Diagnostic{ 81 Severity: hcl.DiagError, 82 Summary: "Invalid module source address", 83 Detail: fmt.Sprintf( 84 "Terraform failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.", 85 err.Addr, err.Addr, 86 ), 87 Subject: mc.SourceAddrRange.Ptr(), 88 }) 89 default: 90 diags = append(diags, &hcl.Diagnostic{ 91 Severity: hcl.DiagError, 92 Summary: "Invalid module source address", 93 Detail: fmt.Sprintf("Failed to parse module source address: %s.", err), 94 Subject: mc.SourceAddrRange.Ptr(), 95 }) 96 } 97 } 98 } 99 // NOTE: We leave mc.SourceAddr as nil for any situation where the 100 // source attribute is invalid, so any code which tries to carefully 101 // use the partial result of a failed config decode must be 102 // resilient to that. 103 } 104 105 if attr, exists := content.Attributes["version"]; exists { 106 var versionDiags hcl.Diagnostics 107 mc.Version, versionDiags = decodeVersionConstraint(attr) 108 diags = append(diags, versionDiags...) 109 } 110 111 if attr, exists := content.Attributes["count"]; exists { 112 mc.Count = attr.Expr 113 } 114 115 if attr, exists := content.Attributes["for_each"]; exists { 116 if mc.Count != nil { 117 diags = append(diags, &hcl.Diagnostic{ 118 Severity: hcl.DiagError, 119 Summary: `Invalid combination of "count" and "for_each"`, 120 Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`, 121 Subject: &attr.NameRange, 122 }) 123 } 124 125 mc.ForEach = attr.Expr 126 } 127 128 if attr, exists := content.Attributes["depends_on"]; exists { 129 deps, depsDiags := decodeDependsOn(attr) 130 diags = append(diags, depsDiags...) 131 mc.DependsOn = append(mc.DependsOn, deps...) 132 } 133 134 if attr, exists := content.Attributes["providers"]; exists { 135 seen := make(map[string]hcl.Range) 136 pairs, pDiags := hcl.ExprMap(attr.Expr) 137 diags = append(diags, pDiags...) 138 for _, pair := range pairs { 139 key, keyDiags := decodeProviderConfigRef(pair.Key, "providers") 140 diags = append(diags, keyDiags...) 141 value, valueDiags := decodeProviderConfigRef(pair.Value, "providers") 142 diags = append(diags, valueDiags...) 143 if keyDiags.HasErrors() || valueDiags.HasErrors() { 144 continue 145 } 146 147 matchKey := key.String() 148 if prev, exists := seen[matchKey]; exists { 149 diags = append(diags, &hcl.Diagnostic{ 150 Severity: hcl.DiagError, 151 Summary: "Duplicate provider address", 152 Detail: fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev), 153 Subject: pair.Value.Range().Ptr(), 154 }) 155 continue 156 } 157 158 rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range()) 159 seen[matchKey] = rng 160 mc.Providers = append(mc.Providers, PassedProviderConfig{ 161 InChild: key, 162 InParent: value, 163 }) 164 } 165 } 166 167 var seenEscapeBlock *hcl.Block 168 for _, block := range content.Blocks { 169 switch block.Type { 170 case "_": 171 if seenEscapeBlock != nil { 172 diags = append(diags, &hcl.Diagnostic{ 173 Severity: hcl.DiagError, 174 Summary: "Duplicate escaping block", 175 Detail: fmt.Sprintf( 176 "The special block type \"_\" can be used to force particular arguments to be interpreted as module input variables rather than as meta-arguments, but each module block can have only one such block. The first escaping block was at %s.", 177 seenEscapeBlock.DefRange, 178 ), 179 Subject: &block.DefRange, 180 }) 181 continue 182 } 183 seenEscapeBlock = block 184 185 // When there's an escaping block its content merges with the 186 // existing config we extracted earlier, so later decoding 187 // will see a blend of both. 188 mc.Config = hcl.MergeBodies([]hcl.Body{mc.Config, block.Body}) 189 190 default: 191 // All of the other block types in our schema are reserved. 192 diags = append(diags, &hcl.Diagnostic{ 193 Severity: hcl.DiagError, 194 Summary: "Reserved block type name in module block", 195 Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), 196 Subject: &block.TypeRange, 197 }) 198 } 199 } 200 201 return mc, diags 202 } 203 204 // EntersNewPackage returns true if this call is to an external module, either 205 // directly via a remote source address or indirectly via a registry source 206 // address. 207 // 208 // Other behaviors in Terraform may treat package crossings as a special 209 // situation, because that indicates that the caller and callee can change 210 // independently of one another and thus we should disallow using any features 211 // where the caller assumes anything about the callee other than its input 212 // variables, required provider configurations, and output values. 213 func (mc *ModuleCall) EntersNewPackage() bool { 214 return moduleSourceAddrEntersNewPackage(mc.SourceAddr) 215 } 216 217 // PassedProviderConfig represents a provider config explicitly passed down to 218 // a child module, possibly giving it a new local address in the process. 219 type PassedProviderConfig struct { 220 InChild *ProviderConfigRef 221 InParent *ProviderConfigRef 222 } 223 224 var moduleBlockSchema = &hcl.BodySchema{ 225 Attributes: []hcl.AttributeSchema{ 226 { 227 Name: "source", 228 Required: true, 229 }, 230 { 231 Name: "version", 232 }, 233 { 234 Name: "count", 235 }, 236 { 237 Name: "for_each", 238 }, 239 { 240 Name: "depends_on", 241 }, 242 { 243 Name: "providers", 244 }, 245 }, 246 Blocks: []hcl.BlockHeaderSchema{ 247 {Type: "_"}, // meta-argument escaping block 248 249 // These are all reserved for future use. 250 {Type: "lifecycle"}, 251 {Type: "locals"}, 252 {Type: "provider", LabelNames: []string{"type"}}, 253 }, 254 } 255 256 func moduleSourceAddrEntersNewPackage(addr addrs.ModuleSource) bool { 257 switch addr.(type) { 258 case nil: 259 // There are only two situations where we should get here: 260 // - We've been asked about the source address of the root module, 261 // which is always nil. 262 // - We've been asked about a ModuleCall that is part of the partial 263 // result of a failed decode. 264 // The root module exists outside of all module packages, so we'll 265 // just return false for that case. For the error case it doesn't 266 // really matter what we return as long as we don't panic, because 267 // we only make a best-effort to allow careful inspection of objects 268 // representing invalid configuration. 269 return false 270 case addrs.ModuleSourceLocal: 271 // Local source addresses are the only address type that remains within 272 // the same package. 273 return false 274 default: 275 // All other address types enter a new package. 276 return true 277 } 278 }