github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/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 "github.com/eliastor/durgaform/internal/addrs" 10 "github.com/eliastor/durgaform/internal/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 haveVersionArg := false 63 if attr, exists := content.Attributes["version"]; exists { 64 var versionDiags hcl.Diagnostics 65 mc.Version, versionDiags = decodeVersionConstraint(attr) 66 diags = append(diags, versionDiags...) 67 haveVersionArg = true 68 } 69 70 if attr, exists := content.Attributes["source"]; exists { 71 mc.SourceSet = true 72 mc.SourceAddrRange = attr.Expr.Range() 73 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &mc.SourceAddrRaw) 74 diags = append(diags, valDiags...) 75 if !valDiags.HasErrors() { 76 var addr addrs.ModuleSource 77 var err error 78 if haveVersionArg { 79 addr, err = addrs.ParseModuleSourceRegistry(mc.SourceAddrRaw) 80 } else { 81 addr, err = addrs.ParseModuleSource(mc.SourceAddrRaw) 82 } 83 mc.SourceAddr = addr 84 if err != nil { 85 // NOTE: We leave mc.SourceAddr as nil for any situation where the 86 // source attribute is invalid, so any code which tries to carefully 87 // use the partial result of a failed config decode must be 88 // resilient to that. 89 mc.SourceAddr = nil 90 91 // NOTE: In practice it's actually very unlikely to end up here, 92 // because our source address parser can turn just about any string 93 // into some sort of remote package address, and so for most errors 94 // we'll detect them only during module installation. There are 95 // still a _few_ purely-syntax errors we can catch at parsing time, 96 // though, mostly related to remote package sub-paths and local 97 // paths. 98 switch err := err.(type) { 99 case *getmodules.MaybeRelativePathErr: 100 diags = append(diags, &hcl.Diagnostic{ 101 Severity: hcl.DiagError, 102 Summary: "Invalid module source address", 103 Detail: fmt.Sprintf( 104 "Durgaform 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.", 105 err.Addr, err.Addr, 106 ), 107 Subject: mc.SourceAddrRange.Ptr(), 108 }) 109 default: 110 if haveVersionArg { 111 // In this case we'll include some extra context that 112 // we assumed a registry source address due to the 113 // version argument. 114 diags = append(diags, &hcl.Diagnostic{ 115 Severity: hcl.DiagError, 116 Summary: "Invalid registry module source address", 117 Detail: fmt.Sprintf("Failed to parse module registry address: %s.\n\nDurgaform assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err), 118 Subject: mc.SourceAddrRange.Ptr(), 119 }) 120 } else { 121 diags = append(diags, &hcl.Diagnostic{ 122 Severity: hcl.DiagError, 123 Summary: "Invalid module source address", 124 Detail: fmt.Sprintf("Failed to parse module source address: %s.", err), 125 Subject: mc.SourceAddrRange.Ptr(), 126 }) 127 } 128 } 129 } 130 } 131 } 132 133 if attr, exists := content.Attributes["count"]; exists { 134 mc.Count = attr.Expr 135 } 136 137 if attr, exists := content.Attributes["for_each"]; exists { 138 if mc.Count != nil { 139 diags = append(diags, &hcl.Diagnostic{ 140 Severity: hcl.DiagError, 141 Summary: `Invalid combination of "count" and "for_each"`, 142 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.`, 143 Subject: &attr.NameRange, 144 }) 145 } 146 147 mc.ForEach = attr.Expr 148 } 149 150 if attr, exists := content.Attributes["depends_on"]; exists { 151 deps, depsDiags := decodeDependsOn(attr) 152 diags = append(diags, depsDiags...) 153 mc.DependsOn = append(mc.DependsOn, deps...) 154 } 155 156 if attr, exists := content.Attributes["providers"]; exists { 157 seen := make(map[string]hcl.Range) 158 pairs, pDiags := hcl.ExprMap(attr.Expr) 159 diags = append(diags, pDiags...) 160 for _, pair := range pairs { 161 key, keyDiags := decodeProviderConfigRef(pair.Key, "providers") 162 diags = append(diags, keyDiags...) 163 value, valueDiags := decodeProviderConfigRef(pair.Value, "providers") 164 diags = append(diags, valueDiags...) 165 if keyDiags.HasErrors() || valueDiags.HasErrors() { 166 continue 167 } 168 169 matchKey := key.String() 170 if prev, exists := seen[matchKey]; exists { 171 diags = append(diags, &hcl.Diagnostic{ 172 Severity: hcl.DiagError, 173 Summary: "Duplicate provider address", 174 Detail: fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev), 175 Subject: pair.Value.Range().Ptr(), 176 }) 177 continue 178 } 179 180 rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range()) 181 seen[matchKey] = rng 182 mc.Providers = append(mc.Providers, PassedProviderConfig{ 183 InChild: key, 184 InParent: value, 185 }) 186 } 187 } 188 189 var seenEscapeBlock *hcl.Block 190 for _, block := range content.Blocks { 191 switch block.Type { 192 case "_": 193 if seenEscapeBlock != nil { 194 diags = append(diags, &hcl.Diagnostic{ 195 Severity: hcl.DiagError, 196 Summary: "Duplicate escaping block", 197 Detail: fmt.Sprintf( 198 "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.", 199 seenEscapeBlock.DefRange, 200 ), 201 Subject: &block.DefRange, 202 }) 203 continue 204 } 205 seenEscapeBlock = block 206 207 // When there's an escaping block its content merges with the 208 // existing config we extracted earlier, so later decoding 209 // will see a blend of both. 210 mc.Config = hcl.MergeBodies([]hcl.Body{mc.Config, block.Body}) 211 212 default: 213 // All of the other block types in our schema are reserved. 214 diags = append(diags, &hcl.Diagnostic{ 215 Severity: hcl.DiagError, 216 Summary: "Reserved block type name in module block", 217 Detail: fmt.Sprintf("The block type name %q is reserved for use by Durgaform in a future version.", block.Type), 218 Subject: &block.TypeRange, 219 }) 220 } 221 } 222 223 return mc, diags 224 } 225 226 // EntersNewPackage returns true if this call is to an external module, either 227 // directly via a remote source address or indirectly via a registry source 228 // address. 229 // 230 // Other behaviors in Durgaform may treat package crossings as a special 231 // situation, because that indicates that the caller and callee can change 232 // independently of one another and thus we should disallow using any features 233 // where the caller assumes anything about the callee other than its input 234 // variables, required provider configurations, and output values. 235 func (mc *ModuleCall) EntersNewPackage() bool { 236 return moduleSourceAddrEntersNewPackage(mc.SourceAddr) 237 } 238 239 // PassedProviderConfig represents a provider config explicitly passed down to 240 // a child module, possibly giving it a new local address in the process. 241 type PassedProviderConfig struct { 242 InChild *ProviderConfigRef 243 InParent *ProviderConfigRef 244 } 245 246 var moduleBlockSchema = &hcl.BodySchema{ 247 Attributes: []hcl.AttributeSchema{ 248 { 249 Name: "source", 250 Required: true, 251 }, 252 { 253 Name: "version", 254 }, 255 { 256 Name: "count", 257 }, 258 { 259 Name: "for_each", 260 }, 261 { 262 Name: "depends_on", 263 }, 264 { 265 Name: "providers", 266 }, 267 }, 268 Blocks: []hcl.BlockHeaderSchema{ 269 {Type: "_"}, // meta-argument escaping block 270 271 // These are all reserved for future use. 272 {Type: "lifecycle"}, 273 {Type: "locals"}, 274 {Type: "provider", LabelNames: []string{"type"}}, 275 }, 276 } 277 278 func moduleSourceAddrEntersNewPackage(addr addrs.ModuleSource) bool { 279 switch addr.(type) { 280 case nil: 281 // There are only two situations where we should get here: 282 // - We've been asked about the source address of the root module, 283 // which is always nil. 284 // - We've been asked about a ModuleCall that is part of the partial 285 // result of a failed decode. 286 // The root module exists outside of all module packages, so we'll 287 // just return false for that case. For the error case it doesn't 288 // really matter what we return as long as we don't panic, because 289 // we only make a best-effort to allow careful inspection of objects 290 // representing invalid configuration. 291 return false 292 case addrs.ModuleSourceLocal: 293 // Local source addresses are the only address type that remains within 294 // the same package. 295 return false 296 default: 297 // All other address types enter a new package. 298 return true 299 } 300 }