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 }