github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/config_build.go (about)

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