github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/configs/module.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 8 "github.com/iaas-resource-provision/iaas-rpc/internal/addrs" 9 "github.com/iaas-resource-provision/iaas-rpc/internal/experiments" 10 ) 11 12 // Module is a container for a set of configuration constructs that are 13 // evaluated within a common namespace. 14 type Module struct { 15 // SourceDir is the filesystem directory that the module was loaded from. 16 // 17 // This is populated automatically only for configurations loaded with 18 // LoadConfigDir. If the parser is using a virtual filesystem then the 19 // path here will be in terms of that virtual filesystem. 20 21 // Any other caller that constructs a module directly with NewModule may 22 // assign a suitable value to this attribute before using it for other 23 // purposes. It should be treated as immutable by all consumers of Module 24 // values. 25 SourceDir string 26 27 CoreVersionConstraints []VersionConstraint 28 29 ActiveExperiments experiments.Set 30 31 Backend *Backend 32 ProviderConfigs map[string]*Provider 33 ProviderRequirements *RequiredProviders 34 ProviderLocalNames map[addrs.Provider]string 35 ProviderMetas map[addrs.Provider]*ProviderMeta 36 37 Variables map[string]*Variable 38 Locals map[string]*Local 39 Outputs map[string]*Output 40 41 ModuleCalls map[string]*ModuleCall 42 43 ManagedResources map[string]*Resource 44 DataResources map[string]*Resource 45 } 46 47 // File describes the contents of a single configuration file. 48 // 49 // Individual files are not usually used alone, but rather combined together 50 // with other files (conventionally, those in the same directory) to produce 51 // a *Module, using NewModule. 52 // 53 // At the level of an individual file we represent directly the structural 54 // elements present in the file, without any attempt to detect conflicting 55 // declarations. A File object can therefore be used for some basic static 56 // analysis of individual elements, but must be built into a Module to detect 57 // duplicate declarations. 58 type File struct { 59 CoreVersionConstraints []VersionConstraint 60 61 ActiveExperiments experiments.Set 62 63 Backends []*Backend 64 ProviderConfigs []*Provider 65 ProviderMetas []*ProviderMeta 66 RequiredProviders []*RequiredProviders 67 68 Variables []*Variable 69 Locals []*Local 70 Outputs []*Output 71 72 ModuleCalls []*ModuleCall 73 74 ManagedResources []*Resource 75 DataResources []*Resource 76 } 77 78 // NewModule takes a list of primary files and a list of override files and 79 // produces a *Module by combining the files together. 80 // 81 // If there are any conflicting declarations in the given files -- for example, 82 // if the same variable name is defined twice -- then the resulting module 83 // will be incomplete and error diagnostics will be returned. Careful static 84 // analysis of the returned Module is still possible in this case, but the 85 // module will probably not be semantically valid. 86 func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) { 87 var diags hcl.Diagnostics 88 mod := &Module{ 89 ProviderConfigs: map[string]*Provider{}, 90 ProviderLocalNames: map[addrs.Provider]string{}, 91 Variables: map[string]*Variable{}, 92 Locals: map[string]*Local{}, 93 Outputs: map[string]*Output{}, 94 ModuleCalls: map[string]*ModuleCall{}, 95 ManagedResources: map[string]*Resource{}, 96 DataResources: map[string]*Resource{}, 97 ProviderMetas: map[addrs.Provider]*ProviderMeta{}, 98 } 99 100 // Process the required_providers blocks first, to ensure that all 101 // resources have access to the correct provider FQNs 102 for _, file := range primaryFiles { 103 for _, r := range file.RequiredProviders { 104 if mod.ProviderRequirements != nil { 105 diags = append(diags, &hcl.Diagnostic{ 106 Severity: hcl.DiagError, 107 Summary: "Duplicate required providers configuration", 108 Detail: fmt.Sprintf("A module may have only one required providers configuration. The required providers were previously configured at %s.", mod.ProviderRequirements.DeclRange), 109 Subject: &r.DeclRange, 110 }) 111 continue 112 } 113 mod.ProviderRequirements = r 114 } 115 } 116 117 // If no required_providers block is configured, create a useful empty 118 // state to reduce nil checks elsewhere 119 if mod.ProviderRequirements == nil { 120 mod.ProviderRequirements = &RequiredProviders{ 121 RequiredProviders: make(map[string]*RequiredProvider), 122 } 123 } 124 125 // Any required_providers blocks in override files replace the entire 126 // block for each provider 127 for _, file := range overrideFiles { 128 for _, override := range file.RequiredProviders { 129 for name, rp := range override.RequiredProviders { 130 mod.ProviderRequirements.RequiredProviders[name] = rp 131 } 132 } 133 } 134 135 for _, file := range primaryFiles { 136 fileDiags := mod.appendFile(file) 137 diags = append(diags, fileDiags...) 138 } 139 140 for _, file := range overrideFiles { 141 fileDiags := mod.mergeFile(file) 142 diags = append(diags, fileDiags...) 143 } 144 145 diags = append(diags, checkModuleExperiments(mod)...) 146 147 // Generate the FQN -> LocalProviderName map 148 mod.gatherProviderLocalNames() 149 150 return mod, diags 151 } 152 153 // ResourceByAddr returns the configuration for the resource with the given 154 // address, or nil if there is no such resource. 155 func (m *Module) ResourceByAddr(addr addrs.Resource) *Resource { 156 key := addr.String() 157 switch addr.Mode { 158 case addrs.ManagedResourceMode: 159 return m.ManagedResources[key] 160 case addrs.DataResourceMode: 161 return m.DataResources[key] 162 default: 163 return nil 164 } 165 } 166 167 func (m *Module) appendFile(file *File) hcl.Diagnostics { 168 var diags hcl.Diagnostics 169 170 // If there are any conflicting requirements then we'll catch them 171 // when we actually check these constraints. 172 m.CoreVersionConstraints = append(m.CoreVersionConstraints, file.CoreVersionConstraints...) 173 174 m.ActiveExperiments = experiments.SetUnion(m.ActiveExperiments, file.ActiveExperiments) 175 176 for _, b := range file.Backends { 177 if m.Backend != nil { 178 diags = append(diags, &hcl.Diagnostic{ 179 Severity: hcl.DiagError, 180 Summary: "Duplicate backend configuration", 181 Detail: fmt.Sprintf("A module may have only one backend configuration. The backend was previously configured at %s.", m.Backend.DeclRange), 182 Subject: &b.DeclRange, 183 }) 184 continue 185 } 186 m.Backend = b 187 } 188 189 for _, pc := range file.ProviderConfigs { 190 key := pc.moduleUniqueKey() 191 if existing, exists := m.ProviderConfigs[key]; exists { 192 if existing.Alias == "" { 193 diags = append(diags, &hcl.Diagnostic{ 194 Severity: hcl.DiagError, 195 Summary: "Duplicate provider configuration", 196 Detail: fmt.Sprintf("A default (non-aliased) provider configuration for %q was already given at %s. If multiple configurations are required, set the \"alias\" argument for alternative configurations.", existing.Name, existing.DeclRange), 197 Subject: &pc.DeclRange, 198 }) 199 } else { 200 diags = append(diags, &hcl.Diagnostic{ 201 Severity: hcl.DiagError, 202 Summary: "Duplicate provider configuration", 203 Detail: fmt.Sprintf("A provider configuration for %q with alias %q was already given at %s. Each configuration for the same provider must have a distinct alias.", existing.Name, existing.Alias, existing.DeclRange), 204 Subject: &pc.DeclRange, 205 }) 206 } 207 continue 208 } 209 m.ProviderConfigs[key] = pc 210 } 211 212 for _, pm := range file.ProviderMetas { 213 provider := m.ProviderForLocalConfig(addrs.LocalProviderConfig{LocalName: pm.Provider}) 214 if existing, exists := m.ProviderMetas[provider]; exists { 215 diags = append(diags, &hcl.Diagnostic{ 216 Severity: hcl.DiagError, 217 Summary: "Duplicate provider_meta block", 218 Detail: fmt.Sprintf("A provider_meta block for provider %q was already declared at %s. Providers may only have one provider_meta block per module.", existing.Provider, existing.DeclRange), 219 Subject: &pm.DeclRange, 220 }) 221 } 222 m.ProviderMetas[provider] = pm 223 } 224 225 for _, v := range file.Variables { 226 if existing, exists := m.Variables[v.Name]; exists { 227 diags = append(diags, &hcl.Diagnostic{ 228 Severity: hcl.DiagError, 229 Summary: "Duplicate variable declaration", 230 Detail: fmt.Sprintf("A variable named %q was already declared at %s. Variable names must be unique within a module.", existing.Name, existing.DeclRange), 231 Subject: &v.DeclRange, 232 }) 233 } 234 m.Variables[v.Name] = v 235 } 236 237 for _, l := range file.Locals { 238 if existing, exists := m.Locals[l.Name]; exists { 239 diags = append(diags, &hcl.Diagnostic{ 240 Severity: hcl.DiagError, 241 Summary: "Duplicate local value definition", 242 Detail: fmt.Sprintf("A local value named %q was already defined at %s. Local value names must be unique within a module.", existing.Name, existing.DeclRange), 243 Subject: &l.DeclRange, 244 }) 245 } 246 m.Locals[l.Name] = l 247 } 248 249 for _, o := range file.Outputs { 250 if existing, exists := m.Outputs[o.Name]; exists { 251 diags = append(diags, &hcl.Diagnostic{ 252 Severity: hcl.DiagError, 253 Summary: "Duplicate output definition", 254 Detail: fmt.Sprintf("An output named %q was already defined at %s. Output names must be unique within a module.", existing.Name, existing.DeclRange), 255 Subject: &o.DeclRange, 256 }) 257 } 258 m.Outputs[o.Name] = o 259 } 260 261 for _, mc := range file.ModuleCalls { 262 if existing, exists := m.ModuleCalls[mc.Name]; exists { 263 diags = append(diags, &hcl.Diagnostic{ 264 Severity: hcl.DiagError, 265 Summary: "Duplicate module call", 266 Detail: fmt.Sprintf("A module call named %q was already defined at %s. Module calls must have unique names within a module.", existing.Name, existing.DeclRange), 267 Subject: &mc.DeclRange, 268 }) 269 } 270 m.ModuleCalls[mc.Name] = mc 271 } 272 273 for _, r := range file.ManagedResources { 274 key := r.moduleUniqueKey() 275 if existing, exists := m.ManagedResources[key]; exists { 276 diags = append(diags, &hcl.Diagnostic{ 277 Severity: hcl.DiagError, 278 Summary: fmt.Sprintf("Duplicate resource %q configuration", existing.Type), 279 Detail: fmt.Sprintf("A %s resource named %q was already declared at %s. Resource names must be unique per type in each module.", existing.Type, existing.Name, existing.DeclRange), 280 Subject: &r.DeclRange, 281 }) 282 continue 283 } 284 m.ManagedResources[key] = r 285 286 // set the provider FQN for the resource 287 if r.ProviderConfigRef != nil { 288 r.Provider = m.ProviderForLocalConfig(r.ProviderConfigAddr()) 289 } else { 290 // an invalid resource name (for e.g. "null resource" instead of 291 // "null_resource") can cause a panic down the line in addrs: 292 // https://github.com/iaas-resource-provision/iaas-rpc/issues/25560 293 implied, err := addrs.ParseProviderPart(r.Addr().ImpliedProvider()) 294 if err == nil { 295 r.Provider = m.ImpliedProviderForUnqualifiedType(implied) 296 } 297 // We don't return a diagnostic because the invalid resource name 298 // will already have been caught. 299 } 300 } 301 302 for _, r := range file.DataResources { 303 key := r.moduleUniqueKey() 304 if existing, exists := m.DataResources[key]; exists { 305 diags = append(diags, &hcl.Diagnostic{ 306 Severity: hcl.DiagError, 307 Summary: fmt.Sprintf("Duplicate data %q configuration", existing.Type), 308 Detail: fmt.Sprintf("A %s data resource named %q was already declared at %s. Resource names must be unique per type in each module.", existing.Type, existing.Name, existing.DeclRange), 309 Subject: &r.DeclRange, 310 }) 311 continue 312 } 313 m.DataResources[key] = r 314 315 // set the provider FQN for the resource 316 if r.ProviderConfigRef != nil { 317 r.Provider = m.ProviderForLocalConfig(r.ProviderConfigAddr()) 318 } else { 319 // an invalid data source name (for e.g. "null resource" instead of 320 // "null_resource") can cause a panic down the line in addrs: 321 // https://github.com/iaas-resource-provision/iaas-rpc/issues/25560 322 implied, err := addrs.ParseProviderPart(r.Addr().ImpliedProvider()) 323 if err == nil { 324 r.Provider = m.ImpliedProviderForUnqualifiedType(implied) 325 } 326 // We don't return a diagnostic because the invalid resource name 327 // will already have been caught. 328 } 329 } 330 331 return diags 332 } 333 334 func (m *Module) mergeFile(file *File) hcl.Diagnostics { 335 var diags hcl.Diagnostics 336 337 if len(file.CoreVersionConstraints) != 0 { 338 // This is a bit of a strange case for overriding since we normally 339 // would union together across multiple files anyway, but we'll 340 // allow it and have each override file clobber any existing list. 341 m.CoreVersionConstraints = nil 342 m.CoreVersionConstraints = append(m.CoreVersionConstraints, file.CoreVersionConstraints...) 343 } 344 345 if len(file.Backends) != 0 { 346 switch len(file.Backends) { 347 case 1: 348 m.Backend = file.Backends[0] 349 default: 350 // An override file with multiple backends is still invalid, even 351 // though it can override backends from _other_ files. 352 diags = append(diags, &hcl.Diagnostic{ 353 Severity: hcl.DiagError, 354 Summary: "Duplicate backend configuration", 355 Detail: fmt.Sprintf("Each override file may have only one backend configuration. A backend was previously configured at %s.", file.Backends[0].DeclRange), 356 Subject: &file.Backends[1].DeclRange, 357 }) 358 } 359 } 360 361 for _, pc := range file.ProviderConfigs { 362 key := pc.moduleUniqueKey() 363 existing, exists := m.ProviderConfigs[key] 364 if pc.Alias == "" { 365 // We allow overriding a non-existing _default_ provider configuration 366 // because the user model is that an absent provider configuration 367 // implies an empty provider configuration, which is what the user 368 // is therefore overriding here. 369 if exists { 370 mergeDiags := existing.merge(pc) 371 diags = append(diags, mergeDiags...) 372 } else { 373 m.ProviderConfigs[key] = pc 374 } 375 } else { 376 // For aliased providers, there must be a base configuration to 377 // override. This allows us to detect and report alias typos 378 // that might otherwise cause the override to not apply. 379 if !exists { 380 diags = append(diags, &hcl.Diagnostic{ 381 Severity: hcl.DiagError, 382 Summary: "Missing base provider configuration for override", 383 Detail: fmt.Sprintf("There is no %s provider configuration with the alias %q. An override file can only override an aliased provider configuration that was already defined in a primary configuration file.", pc.Name, pc.Alias), 384 Subject: &pc.DeclRange, 385 }) 386 continue 387 } 388 mergeDiags := existing.merge(pc) 389 diags = append(diags, mergeDiags...) 390 } 391 } 392 393 for _, v := range file.Variables { 394 existing, exists := m.Variables[v.Name] 395 if !exists { 396 diags = append(diags, &hcl.Diagnostic{ 397 Severity: hcl.DiagError, 398 Summary: "Missing base variable declaration to override", 399 Detail: fmt.Sprintf("There is no variable named %q. An override file can only override a variable that was already declared in a primary configuration file.", v.Name), 400 Subject: &v.DeclRange, 401 }) 402 continue 403 } 404 mergeDiags := existing.merge(v) 405 diags = append(diags, mergeDiags...) 406 } 407 408 for _, l := range file.Locals { 409 existing, exists := m.Locals[l.Name] 410 if !exists { 411 diags = append(diags, &hcl.Diagnostic{ 412 Severity: hcl.DiagError, 413 Summary: "Missing base local value definition to override", 414 Detail: fmt.Sprintf("There is no local value named %q. An override file can only override a local value that was already defined in a primary configuration file.", l.Name), 415 Subject: &l.DeclRange, 416 }) 417 continue 418 } 419 mergeDiags := existing.merge(l) 420 diags = append(diags, mergeDiags...) 421 } 422 423 for _, o := range file.Outputs { 424 existing, exists := m.Outputs[o.Name] 425 if !exists { 426 diags = append(diags, &hcl.Diagnostic{ 427 Severity: hcl.DiagError, 428 Summary: "Missing base output definition to override", 429 Detail: fmt.Sprintf("There is no output named %q. An override file can only override an output that was already defined in a primary configuration file.", o.Name), 430 Subject: &o.DeclRange, 431 }) 432 continue 433 } 434 mergeDiags := existing.merge(o) 435 diags = append(diags, mergeDiags...) 436 } 437 438 for _, mc := range file.ModuleCalls { 439 existing, exists := m.ModuleCalls[mc.Name] 440 if !exists { 441 diags = append(diags, &hcl.Diagnostic{ 442 Severity: hcl.DiagError, 443 Summary: "Missing module call to override", 444 Detail: fmt.Sprintf("There is no module call named %q. An override file can only override a module call that was defined in a primary configuration file.", mc.Name), 445 Subject: &mc.DeclRange, 446 }) 447 continue 448 } 449 mergeDiags := existing.merge(mc) 450 diags = append(diags, mergeDiags...) 451 } 452 453 for _, r := range file.ManagedResources { 454 key := r.moduleUniqueKey() 455 existing, exists := m.ManagedResources[key] 456 if !exists { 457 diags = append(diags, &hcl.Diagnostic{ 458 Severity: hcl.DiagError, 459 Summary: "Missing resource to override", 460 Detail: fmt.Sprintf("There is no %s resource named %q. An override file can only override a resource block defined in a primary configuration file.", r.Type, r.Name), 461 Subject: &r.DeclRange, 462 }) 463 continue 464 } 465 mergeDiags := existing.merge(r, m.ProviderRequirements.RequiredProviders) 466 diags = append(diags, mergeDiags...) 467 } 468 469 for _, r := range file.DataResources { 470 key := r.moduleUniqueKey() 471 existing, exists := m.DataResources[key] 472 if !exists { 473 diags = append(diags, &hcl.Diagnostic{ 474 Severity: hcl.DiagError, 475 Summary: "Missing data resource to override", 476 Detail: fmt.Sprintf("There is no %s data resource named %q. An override file can only override a data block defined in a primary configuration file.", r.Type, r.Name), 477 Subject: &r.DeclRange, 478 }) 479 continue 480 } 481 mergeDiags := existing.merge(r, m.ProviderRequirements.RequiredProviders) 482 diags = append(diags, mergeDiags...) 483 } 484 485 return diags 486 } 487 488 // gatherProviderLocalNames is a helper function that populatesA a map of 489 // provider FQNs -> provider local names. This information is useful for 490 // user-facing output, which should include both the FQN and LocalName. It must 491 // only be populated after the module has been parsed. 492 func (m *Module) gatherProviderLocalNames() { 493 providers := make(map[addrs.Provider]string) 494 for k, v := range m.ProviderRequirements.RequiredProviders { 495 providers[v.Type] = k 496 } 497 m.ProviderLocalNames = providers 498 } 499 500 // LocalNameForProvider returns the module-specific user-supplied local name for 501 // a given provider FQN, or the default local name if none was supplied. 502 func (m *Module) LocalNameForProvider(p addrs.Provider) string { 503 if existing, exists := m.ProviderLocalNames[p]; exists { 504 return existing 505 } else { 506 // If there isn't a map entry, fall back to the default: 507 // Type = LocalName 508 return p.Type 509 } 510 } 511 512 // ProviderForLocalConfig returns the provider FQN for a given 513 // LocalProviderConfig, based on its local name. 514 func (m *Module) ProviderForLocalConfig(pc addrs.LocalProviderConfig) addrs.Provider { 515 return m.ImpliedProviderForUnqualifiedType(pc.LocalName) 516 } 517 518 // ImpliedProviderForUnqualifiedType returns the provider FQN for a given type, 519 // first by looking up the type in the provider requirements map, and falling 520 // back to an implied default provider. 521 // 522 // The intended behaviour is that configuring a provider with local name "foo" 523 // in a required_providers block will result in resources with type "foo" using 524 // that provider. 525 func (m *Module) ImpliedProviderForUnqualifiedType(pType string) addrs.Provider { 526 if provider, exists := m.ProviderRequirements.RequiredProviders[pType]; exists { 527 return provider.Type 528 } 529 return addrs.ImpliedProviderForUnqualifiedType(pType) 530 }