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