github.com/opentofu/opentofu@v1.7.1/internal/configs/config_build.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 configs
     7  
     8  import (
     9  	"fmt"
    10  	"path"
    11  	"sort"
    12  	"strings"
    13  
    14  	version "github.com/hashicorp/go-version"
    15  	"github.com/hashicorp/hcl/v2"
    16  
    17  	"github.com/opentofu/opentofu/internal/addrs"
    18  )
    19  
    20  // BuildConfig constructs a Config from a root module by loading all of its
    21  // descendent modules via the given ModuleWalker.
    22  //
    23  // The result is a module tree that has so far only had basic module- and
    24  // file-level invariants validated. If the returned diagnostics contains errors,
    25  // the returned module tree may be incomplete but can still be used carefully
    26  // for static analysis.
    27  func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) {
    28  	var diags hcl.Diagnostics
    29  	cfg := &Config{
    30  		Module: root,
    31  	}
    32  	cfg.Root = cfg // Root module is self-referential.
    33  	cfg.Children, diags = buildChildModules(cfg, walker)
    34  	diags = append(diags, buildTestModules(cfg, walker)...)
    35  
    36  	// Skip provider resolution if there are any errors, since the provider
    37  	// configurations themselves may not be valid.
    38  	if !diags.HasErrors() {
    39  		// Now that the config is built, we can connect the provider names to all
    40  		// the known types for validation.
    41  		providers := cfg.resolveProviderTypes()
    42  		cfg.resolveProviderTypesForTests(providers)
    43  	}
    44  
    45  	diags = append(diags, validateProviderConfigs(nil, cfg, nil)...)
    46  	diags = append(diags, validateProviderConfigsForTests(cfg)...)
    47  
    48  	return cfg, diags
    49  }
    50  
    51  func buildTestModules(root *Config, walker ModuleWalker) hcl.Diagnostics {
    52  	var diags hcl.Diagnostics
    53  
    54  	for name, file := range root.Module.Tests {
    55  		for _, run := range file.Runs {
    56  			if run.Module == nil {
    57  				continue
    58  			}
    59  
    60  			// We want to make sure the path for the testing modules are unique
    61  			// so we create a dedicated path for them.
    62  			//
    63  			// Some examples:
    64  			//    - file: main.tftest.hcl, run: setup - test.main.setup
    65  			//    - file: tests/main.tftest.hcl, run: setup - test.tests.main.setup
    66  
    67  			dir := path.Dir(name)
    68  			base := path.Base(name)
    69  
    70  			path := addrs.Module{}
    71  			path = append(path, "test")
    72  			if dir != "." {
    73  				path = append(path, strings.Split(dir, "/")...)
    74  			}
    75  			path = append(path, strings.TrimSuffix(base, ".tftest.hcl"), run.Name)
    76  
    77  			req := ModuleRequest{
    78  				Name:              run.Name,
    79  				Path:              path,
    80  				SourceAddr:        run.Module.Source,
    81  				SourceAddrRange:   run.Module.SourceDeclRange,
    82  				VersionConstraint: run.Module.Version,
    83  				Parent:            root,
    84  				CallRange:         run.Module.DeclRange,
    85  			}
    86  
    87  			cfg, modDiags := loadModule(root, &req, walker)
    88  			diags = append(diags, modDiags...)
    89  
    90  			if cfg != nil {
    91  				// To get the loader to work, we need to set a bunch of values
    92  				// (like the name, path, and parent) as if the module was being
    93  				// loaded as a child of the root config.
    94  				//
    95  				// In actuality, when this is executed it will be as if the
    96  				// module was the root. So, we'll post-process some things to
    97  				// get it to behave as expected later.
    98  
    99  				// First, update the main module for this test run to behave as
   100  				// if it is the root module.
   101  				cfg.Parent = nil
   102  
   103  				// Then we need to update the paths for this config and all
   104  				// children, so they think they are all relative to the root
   105  				// module we just created.
   106  				rebaseChildModule(cfg, cfg)
   107  
   108  				// Finally, link the new config back into our test run so
   109  				// it can be retrieved later.
   110  				run.ConfigUnderTest = cfg
   111  			}
   112  		}
   113  	}
   114  
   115  	return diags
   116  }
   117  
   118  func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, hcl.Diagnostics) {
   119  	var diags hcl.Diagnostics
   120  	ret := map[string]*Config{}
   121  
   122  	calls := parent.Module.ModuleCalls
   123  
   124  	// We'll sort the calls by their local names so that they'll appear in a
   125  	// predictable order in any logging that's produced during the walk.
   126  	callNames := make([]string, 0, len(calls))
   127  	for k := range calls {
   128  		callNames = append(callNames, k)
   129  	}
   130  	sort.Strings(callNames)
   131  
   132  	for _, callName := range callNames {
   133  		call := calls[callName]
   134  		path := make([]string, len(parent.Path)+1)
   135  		copy(path, parent.Path)
   136  		path[len(path)-1] = call.Name
   137  
   138  		req := ModuleRequest{
   139  			Name:              call.Name,
   140  			Path:              path,
   141  			SourceAddr:        call.SourceAddr,
   142  			SourceAddrRange:   call.SourceAddrRange,
   143  			VersionConstraint: call.Version,
   144  			Parent:            parent,
   145  			CallRange:         call.DeclRange,
   146  		}
   147  		child, modDiags := loadModule(parent.Root, &req, walker)
   148  		diags = append(diags, modDiags...)
   149  		if child == nil {
   150  			// This means an error occurred, there should be diagnostics within
   151  			// modDiags for this.
   152  			continue
   153  		}
   154  
   155  		ret[call.Name] = child
   156  	}
   157  
   158  	return ret, diags
   159  }
   160  
   161  func loadModule(root *Config, req *ModuleRequest, walker ModuleWalker) (*Config, hcl.Diagnostics) {
   162  	var diags hcl.Diagnostics
   163  
   164  	mod, ver, modDiags := walker.LoadModule(req)
   165  	diags = append(diags, modDiags...)
   166  	if mod == nil {
   167  		// nil can be returned if the source address was invalid and so
   168  		// nothing could be loaded whatsoever. LoadModule should've
   169  		// returned at least one error diagnostic in that case.
   170  		return nil, diags
   171  	}
   172  
   173  	cfg := &Config{
   174  		Parent:          req.Parent,
   175  		Root:            root,
   176  		Path:            req.Path,
   177  		Module:          mod,
   178  		CallRange:       req.CallRange,
   179  		SourceAddr:      req.SourceAddr,
   180  		SourceAddrRange: req.SourceAddrRange,
   181  		Version:         ver,
   182  	}
   183  
   184  	cfg.Children, modDiags = buildChildModules(cfg, walker)
   185  	diags = append(diags, modDiags...)
   186  
   187  	if mod.Backend != nil {
   188  		diags = diags.Append(&hcl.Diagnostic{
   189  			Severity: hcl.DiagWarning,
   190  			Summary:  "Backend configuration ignored",
   191  			Detail:   "Any selected backend applies to the entire configuration, so OpenTofu expects provider configurations only in the root module.\n\nThis is a warning rather than an error because it's sometimes convenient to temporarily call a root module as a child module for testing purposes, but this backend configuration block will have no effect.",
   192  			Subject:  mod.Backend.DeclRange.Ptr(),
   193  		})
   194  	}
   195  
   196  	if len(mod.Import) > 0 {
   197  		diags = diags.Append(&hcl.Diagnostic{
   198  			Severity: hcl.DiagError,
   199  			Summary:  "Invalid import configuration",
   200  			Detail:   fmt.Sprintf("An import block was detected in %q. Import blocks are only allowed in the root module.", cfg.Path),
   201  			Subject:  mod.Import[0].DeclRange.Ptr(),
   202  		})
   203  	}
   204  
   205  	return cfg, diags
   206  }
   207  
   208  // rebaseChildModule updates cfg to make it act as if root is the base of the
   209  // module tree.
   210  //
   211  // This is used for modules loaded directly from test files. In order to load
   212  // them properly, and reuse the code for loading modules from normal
   213  // configuration files, we pretend they are children of the main configuration
   214  // object. Later, when it comes time for them to execute they will act as if
   215  // they are the root module directly.
   216  //
   217  // This function updates cfg so that it treats the provided root as the actual
   218  // root of this module tree. It then recurses into all the child modules and
   219  // does the same for them.
   220  func rebaseChildModule(cfg *Config, root *Config) {
   221  	for _, child := range cfg.Children {
   222  		rebaseChildModule(child, root)
   223  	}
   224  
   225  	cfg.Path = cfg.Path[len(root.Path):]
   226  	cfg.Root = root
   227  }
   228  
   229  // A ModuleWalker knows how to find and load a child module given details about
   230  // the module to be loaded and a reference to its partially-loaded parent
   231  // Config.
   232  type ModuleWalker interface {
   233  	// LoadModule finds and loads a requested child module.
   234  	//
   235  	// If errors are detected during loading, implementations should return them
   236  	// in the diagnostics object. If the diagnostics object contains any errors
   237  	// then the caller will tolerate the returned module being nil or incomplete.
   238  	// If no errors are returned, it should be non-nil and complete.
   239  	//
   240  	// Full validation need not have been performed but an implementation should
   241  	// ensure that the basic file- and module-validations performed by the
   242  	// LoadConfigDir function (valid syntax, no namespace collisions, etc) have
   243  	// been performed before returning a module.
   244  	LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics)
   245  }
   246  
   247  // ModuleWalkerFunc is an implementation of ModuleWalker that directly wraps
   248  // a callback function, for more convenient use of that interface.
   249  type ModuleWalkerFunc func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics)
   250  
   251  // LoadModule implements ModuleWalker.
   252  func (f ModuleWalkerFunc) LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
   253  	return f(req)
   254  }
   255  
   256  // ModuleRequest is used with the ModuleWalker interface to describe a child
   257  // module that must be loaded.
   258  type ModuleRequest struct {
   259  	// Name is the "logical name" of the module call within configuration.
   260  	// This is provided in case the name is used as part of a storage key
   261  	// for the module, but implementations must otherwise treat it as an
   262  	// opaque string. It is guaranteed to have already been validated as an
   263  	// HCL identifier and UTF-8 encoded.
   264  	Name string
   265  
   266  	// Path is a list of logical names that traverse from the root module to
   267  	// this module. This can be used, for example, to form a lookup key for
   268  	// each distinct module call in a configuration, allowing for multiple
   269  	// calls with the same name at different points in the tree.
   270  	Path addrs.Module
   271  
   272  	// SourceAddr is the source address string provided by the user in
   273  	// configuration.
   274  	SourceAddr addrs.ModuleSource
   275  
   276  	// SourceAddrRange is the source range for the SourceAddr value as it
   277  	// was provided in configuration. This can and should be used to generate
   278  	// diagnostics about the source address having invalid syntax, referring
   279  	// to a non-existent object, etc.
   280  	SourceAddrRange hcl.Range
   281  
   282  	// VersionConstraint is the version constraint applied to the module in
   283  	// configuration. This data structure includes the source range for
   284  	// the constraint, which can and should be used to generate diagnostics
   285  	// about constraint-related issues, such as constraints that eliminate all
   286  	// available versions of a module whose source is otherwise valid.
   287  	VersionConstraint VersionConstraint
   288  
   289  	// Parent is the partially-constructed module tree node that the loaded
   290  	// module will be added to. Callers may refer to any field of this
   291  	// structure except Children, which is still under construction when
   292  	// ModuleRequest objects are created and thus has undefined content.
   293  	// The main reason this is provided is so that full module paths can
   294  	// be constructed for uniqueness.
   295  	Parent *Config
   296  
   297  	// CallRange is the source range for the header of the "module" block
   298  	// in configuration that prompted this request. This can be used as the
   299  	// subject of an error diagnostic that relates to the module call itself,
   300  	// rather than to either its source address or its version number.
   301  	CallRange hcl.Range
   302  }
   303  
   304  // DisabledModuleWalker is a ModuleWalker that doesn't support
   305  // child modules at all, and so will return an error if asked to load one.
   306  //
   307  // This is provided primarily for testing. There is no good reason to use this
   308  // in the main application.
   309  var DisabledModuleWalker ModuleWalker
   310  
   311  func init() {
   312  	DisabledModuleWalker = ModuleWalkerFunc(func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
   313  		return nil, nil, hcl.Diagnostics{
   314  			{
   315  				Severity: hcl.DiagError,
   316  				Summary:  "Child modules are not supported",
   317  				Detail:   "Child module calls are not allowed in this context.",
   318  				Subject:  &req.CallRange,
   319  			},
   320  		}
   321  	})
   322  }