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  }