github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/configs/config_build.go (about)

     1  package configs
     2  
     3  import (
     4  	"sort"
     5  
     6  	version "github.com/hashicorp/go-version"
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/eliastor/durgaform/internal/addrs"
     9  )
    10  
    11  // BuildConfig constructs a Config from a root module by loading all of its
    12  // descendent modules via the given ModuleWalker.
    13  //
    14  // The result is a module tree that has so far only had basic module- and
    15  // file-level invariants validated. If the returned diagnostics contains errors,
    16  // the returned module tree may be incomplete but can still be used carefully
    17  // for static analysis.
    18  func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) {
    19  	var diags hcl.Diagnostics
    20  	cfg := &Config{
    21  		Module: root,
    22  	}
    23  	cfg.Root = cfg // Root module is self-referential.
    24  	cfg.Children, diags = buildChildModules(cfg, walker)
    25  
    26  	// Skip provider resolution if there are any errors, since the provider
    27  	// configurations themselves may not be valid.
    28  	if !diags.HasErrors() {
    29  		// Now that the config is built, we can connect the provider names to all
    30  		// the known types for validation.
    31  		cfg.resolveProviderTypes()
    32  	}
    33  
    34  	diags = append(diags, validateProviderConfigs(nil, cfg, nil)...)
    35  
    36  	return cfg, diags
    37  }
    38  
    39  func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, hcl.Diagnostics) {
    40  	var diags hcl.Diagnostics
    41  	ret := map[string]*Config{}
    42  
    43  	calls := parent.Module.ModuleCalls
    44  
    45  	// We'll sort the calls by their local names so that they'll appear in a
    46  	// predictable order in any logging that's produced during the walk.
    47  	callNames := make([]string, 0, len(calls))
    48  	for k := range calls {
    49  		callNames = append(callNames, k)
    50  	}
    51  	sort.Strings(callNames)
    52  
    53  	for _, callName := range callNames {
    54  		call := calls[callName]
    55  		path := make([]string, len(parent.Path)+1)
    56  		copy(path, parent.Path)
    57  		path[len(path)-1] = call.Name
    58  
    59  		req := ModuleRequest{
    60  			Name:              call.Name,
    61  			Path:              path,
    62  			SourceAddr:        call.SourceAddr,
    63  			SourceAddrRange:   call.SourceAddrRange,
    64  			VersionConstraint: call.Version,
    65  			Parent:            parent,
    66  			CallRange:         call.DeclRange,
    67  		}
    68  
    69  		mod, ver, modDiags := walker.LoadModule(&req)
    70  		diags = append(diags, modDiags...)
    71  		if mod == nil {
    72  			// nil can be returned if the source address was invalid and so
    73  			// nothing could be loaded whatsoever. LoadModule should've
    74  			// returned at least one error diagnostic in that case.
    75  			continue
    76  		}
    77  
    78  		child := &Config{
    79  			Parent:          parent,
    80  			Root:            parent.Root,
    81  			Path:            path,
    82  			Module:          mod,
    83  			CallRange:       call.DeclRange,
    84  			SourceAddr:      call.SourceAddr,
    85  			SourceAddrRange: call.SourceAddrRange,
    86  			Version:         ver,
    87  		}
    88  
    89  		child.Children, modDiags = buildChildModules(child, walker)
    90  		diags = append(diags, modDiags...)
    91  
    92  		if mod.Backend != nil {
    93  			diags = diags.Append(&hcl.Diagnostic{
    94  				Severity: hcl.DiagWarning,
    95  				Summary:  "Backend configuration ignored",
    96  				Detail:   "Any selected backend applies to the entire configuration, so Durgaform 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.",
    97  				Subject:  mod.Backend.DeclRange.Ptr(),
    98  			})
    99  		}
   100  
   101  		ret[call.Name] = child
   102  	}
   103  
   104  	return ret, diags
   105  }
   106  
   107  // A ModuleWalker knows how to find and load a child module given details about
   108  // the module to be loaded and a reference to its partially-loaded parent
   109  // Config.
   110  type ModuleWalker interface {
   111  	// LoadModule finds and loads a requested child module.
   112  	//
   113  	// If errors are detected during loading, implementations should return them
   114  	// in the diagnostics object. If the diagnostics object contains any errors
   115  	// then the caller will tolerate the returned module being nil or incomplete.
   116  	// If no errors are returned, it should be non-nil and complete.
   117  	//
   118  	// Full validation need not have been performed but an implementation should
   119  	// ensure that the basic file- and module-validations performed by the
   120  	// LoadConfigDir function (valid syntax, no namespace collisions, etc) have
   121  	// been performed before returning a module.
   122  	LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics)
   123  }
   124  
   125  // ModuleWalkerFunc is an implementation of ModuleWalker that directly wraps
   126  // a callback function, for more convenient use of that interface.
   127  type ModuleWalkerFunc func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics)
   128  
   129  // LoadModule implements ModuleWalker.
   130  func (f ModuleWalkerFunc) LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
   131  	return f(req)
   132  }
   133  
   134  // ModuleRequest is used with the ModuleWalker interface to describe a child
   135  // module that must be loaded.
   136  type ModuleRequest struct {
   137  	// Name is the "logical name" of the module call within configuration.
   138  	// This is provided in case the name is used as part of a storage key
   139  	// for the module, but implementations must otherwise treat it as an
   140  	// opaque string. It is guaranteed to have already been validated as an
   141  	// HCL identifier and UTF-8 encoded.
   142  	Name string
   143  
   144  	// Path is a list of logical names that traverse from the root module to
   145  	// this module. This can be used, for example, to form a lookup key for
   146  	// each distinct module call in a configuration, allowing for multiple
   147  	// calls with the same name at different points in the tree.
   148  	Path addrs.Module
   149  
   150  	// SourceAddr is the source address string provided by the user in
   151  	// configuration.
   152  	SourceAddr addrs.ModuleSource
   153  
   154  	// SourceAddrRange is the source range for the SourceAddr value as it
   155  	// was provided in configuration. This can and should be used to generate
   156  	// diagnostics about the source address having invalid syntax, referring
   157  	// to a non-existent object, etc.
   158  	SourceAddrRange hcl.Range
   159  
   160  	// VersionConstraint is the version constraint applied to the module in
   161  	// configuration. This data structure includes the source range for
   162  	// the constraint, which can and should be used to generate diagnostics
   163  	// about constraint-related issues, such as constraints that eliminate all
   164  	// available versions of a module whose source is otherwise valid.
   165  	VersionConstraint VersionConstraint
   166  
   167  	// Parent is the partially-constructed module tree node that the loaded
   168  	// module will be added to. Callers may refer to any field of this
   169  	// structure except Children, which is still under construction when
   170  	// ModuleRequest objects are created and thus has undefined content.
   171  	// The main reason this is provided is so that full module paths can
   172  	// be constructed for uniqueness.
   173  	Parent *Config
   174  
   175  	// CallRange is the source range for the header of the "module" block
   176  	// in configuration that prompted this request. This can be used as the
   177  	// subject of an error diagnostic that relates to the module call itself,
   178  	// rather than to either its source address or its version number.
   179  	CallRange hcl.Range
   180  }
   181  
   182  // DisabledModuleWalker is a ModuleWalker that doesn't support
   183  // child modules at all, and so will return an error if asked to load one.
   184  //
   185  // This is provided primarily for testing. There is no good reason to use this
   186  // in the main application.
   187  var DisabledModuleWalker ModuleWalker
   188  
   189  func init() {
   190  	DisabledModuleWalker = ModuleWalkerFunc(func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
   191  		return nil, nil, hcl.Diagnostics{
   192  			{
   193  				Severity: hcl.DiagError,
   194  				Summary:  "Child modules are not supported",
   195  				Detail:   "Child module calls are not allowed in this context.",
   196  				Subject:  &req.CallRange,
   197  			},
   198  		}
   199  	})
   200  }