github.com/opentofu/opentofu@v1.7.1/internal/configs/configload/loader.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 configload
     7  
     8  import (
     9  	"fmt"
    10  	"path/filepath"
    11  
    12  	"github.com/hashicorp/terraform-svchost/disco"
    13  	"github.com/opentofu/opentofu/internal/configs"
    14  	"github.com/opentofu/opentofu/internal/registry"
    15  	"github.com/spf13/afero"
    16  )
    17  
    18  // A Loader instance is the main entry-point for loading configurations via
    19  // this package.
    20  //
    21  // It extends the general config-loading functionality in the parent package
    22  // "configs" to support installation of modules from remote sources and
    23  // loading full configurations using modules that were previously installed.
    24  type Loader struct {
    25  	// parser is used to read configuration
    26  	parser *configs.Parser
    27  
    28  	// modules is used to install and locate descendent modules that are
    29  	// referenced (directly or indirectly) from the root module.
    30  	modules moduleMgr
    31  }
    32  
    33  // Config is used with NewLoader to specify configuration arguments for the
    34  // loader.
    35  type Config struct {
    36  	// ModulesDir is a path to a directory where descendent modules are
    37  	// (or should be) installed. (This is usually the
    38  	// .terraform/modules directory, in the common case where this package
    39  	// is being loaded from the main OpenTofu CLI package.)
    40  	ModulesDir string
    41  
    42  	// Services is the service discovery client to use when locating remote
    43  	// module registry endpoints. If this is nil then registry sources are
    44  	// not supported, which should be true only in specialized circumstances
    45  	// such as in tests.
    46  	Services *disco.Disco
    47  }
    48  
    49  // NewLoader creates and returns a loader that reads configuration from the
    50  // real OS filesystem.
    51  //
    52  // The loader has some internal state about the modules that are currently
    53  // installed, which is read from disk as part of this function. If that
    54  // manifest cannot be read then an error will be returned.
    55  func NewLoader(config *Config) (*Loader, error) {
    56  	fs := afero.NewOsFs()
    57  	parser := configs.NewParser(fs)
    58  	reg := registry.NewClient(config.Services, nil)
    59  
    60  	ret := &Loader{
    61  		parser: parser,
    62  		modules: moduleMgr{
    63  			FS:         afero.Afero{Fs: fs},
    64  			CanInstall: true,
    65  			Dir:        config.ModulesDir,
    66  			Services:   config.Services,
    67  			Registry:   reg,
    68  		},
    69  	}
    70  
    71  	err := ret.modules.readModuleManifestSnapshot()
    72  	if err != nil {
    73  		return nil, fmt.Errorf("failed to read module manifest: %w", err)
    74  	}
    75  
    76  	return ret, nil
    77  }
    78  
    79  // ModulesDir returns the path to the directory where the loader will look for
    80  // the local cache of remote module packages.
    81  func (l *Loader) ModulesDir() string {
    82  	return l.modules.Dir
    83  }
    84  
    85  // RefreshModules updates the in-memory cache of the module manifest from the
    86  // module manifest file on disk. This is not necessary in normal use because
    87  // module installation and configuration loading are separate steps, but it
    88  // can be useful in tests where module installation is done as a part of
    89  // configuration loading by a helper function.
    90  //
    91  // Call this function after any module installation where an existing loader
    92  // is already alive and may be used again later.
    93  //
    94  // An error is returned if the manifest file cannot be read.
    95  func (l *Loader) RefreshModules() error {
    96  	if l == nil {
    97  		// Nothing to do, then.
    98  		return nil
    99  	}
   100  	return l.modules.readModuleManifestSnapshot()
   101  }
   102  
   103  // Parser returns the underlying parser for this loader.
   104  //
   105  // This is useful for loading other sorts of files than the module directories
   106  // that a loader deals with, since then they will share the source code cache
   107  // for this loader and can thus be shown as snippets in diagnostic messages.
   108  func (l *Loader) Parser() *configs.Parser {
   109  	return l.parser
   110  }
   111  
   112  // Sources returns the source code cache for the underlying parser of this
   113  // loader. This is a shorthand for l.Parser().Sources().
   114  func (l *Loader) Sources() map[string][]byte {
   115  	return l.parser.Sources()
   116  }
   117  
   118  // IsConfigDir returns true if and only if the given directory contains at
   119  // least one OpenTofu configuration file. This is a wrapper around calling
   120  // the same method name on the loader's parser.
   121  func (l *Loader) IsConfigDir(path string) bool {
   122  	return l.parser.IsConfigDir(path)
   123  }
   124  
   125  // ImportSources writes into the receiver's source code map the given source
   126  // code buffers.
   127  //
   128  // This is useful in the situation where an ancillary loader is created for
   129  // some reason (e.g. loading config from a plan file) but the cached source
   130  // code from that loader must be imported into the "main" loader in order
   131  // to return source code snapshots in diagnostic messages.
   132  //
   133  //	loader.ImportSources(otherLoader.Sources())
   134  func (l *Loader) ImportSources(sources map[string][]byte) {
   135  	p := l.Parser()
   136  	for name, src := range sources {
   137  		p.ForceFileSource(name, src)
   138  	}
   139  }
   140  
   141  // ImportSourcesFromSnapshot writes into the receiver's source code the
   142  // source files from the given snapshot.
   143  //
   144  // This is similar to ImportSources but knows how to unpack and flatten a
   145  // snapshot data structure to get the corresponding flat source file map.
   146  func (l *Loader) ImportSourcesFromSnapshot(snap *Snapshot) {
   147  	p := l.Parser()
   148  	for _, m := range snap.Modules {
   149  		baseDir := m.Dir
   150  		for fn, src := range m.Files {
   151  			fullPath := filepath.Join(baseDir, fn)
   152  			p.ForceFileSource(fullPath, src)
   153  		}
   154  	}
   155  }
   156  
   157  // AllowLanguageExperiments specifies whether subsequent LoadConfig (and
   158  // similar) calls will allow opting in to experimental language features.
   159  //
   160  // If this method is never called for a particular loader, the default behavior
   161  // is to disallow language experiments.
   162  //
   163  // Main code should set this only for alpha or development builds. Test code
   164  // is responsible for deciding for itself whether and how to call this
   165  // method.
   166  func (l *Loader) AllowLanguageExperiments(allowed bool) {
   167  	l.parser.AllowLanguageExperiments(allowed)
   168  }