| package configload |
| |
| import ( |
| "fmt" |
| "path/filepath" |
| |
| "github.com/hashicorp/terraform-svchost/disco" |
| "github.com/hashicorp/terraform/internal/configs" |
| "github.com/hashicorp/terraform/internal/registry" |
| "github.com/spf13/afero" |
| ) |
| |
| // A Loader instance is the main entry-point for loading configurations via |
| // this package. |
| // |
| // It extends the general config-loading functionality in the parent package |
| // "configs" to support installation of modules from remote sources and |
| // loading full configurations using modules that were previously installed. |
| type Loader struct { |
| // parser is used to read configuration |
| parser *configs.Parser |
| |
| // modules is used to install and locate descendent modules that are |
| // referenced (directly or indirectly) from the root module. |
| modules moduleMgr |
| } |
| |
| // Config is used with NewLoader to specify configuration arguments for the |
| // loader. |
| type Config struct { |
| // ModulesDir is a path to a directory where descendent modules are |
| // (or should be) installed. (This is usually the |
| // .terraform/modules directory, in the common case where this package |
| // is being loaded from the main Terraform CLI package.) |
| ModulesDir string |
| |
| // Services is the service discovery client to use when locating remote |
| // module registry endpoints. If this is nil then registry sources are |
| // not supported, which should be true only in specialized circumstances |
| // such as in tests. |
| Services *disco.Disco |
| } |
| |
| // NewLoader creates and returns a loader that reads configuration from the |
| // real OS filesystem. |
| // |
| // The loader has some internal state about the modules that are currently |
| // installed, which is read from disk as part of this function. If that |
| // manifest cannot be read then an error will be returned. |
| func NewLoader(config *Config) (*Loader, error) { |
| fs := afero.NewOsFs() |
| parser := configs.NewParser(fs) |
| reg := registry.NewClient(config.Services, nil) |
| |
| ret := &Loader{ |
| parser: parser, |
| modules: moduleMgr{ |
| FS: afero.Afero{Fs: fs}, |
| CanInstall: true, |
| Dir: config.ModulesDir, |
| Services: config.Services, |
| Registry: reg, |
| }, |
| } |
| |
| err := ret.modules.readModuleManifestSnapshot() |
| if err != nil { |
| return nil, fmt.Errorf("failed to read module manifest: %s", err) |
| } |
| |
| return ret, nil |
| } |
| |
| // ModulesDir returns the path to the directory where the loader will look for |
| // the local cache of remote module packages. |
| func (l *Loader) ModulesDir() string { |
| return l.modules.Dir |
| } |
| |
| // RefreshModules updates the in-memory cache of the module manifest from the |
| // module manifest file on disk. This is not necessary in normal use because |
| // module installation and configuration loading are separate steps, but it |
| // can be useful in tests where module installation is done as a part of |
| // configuration loading by a helper function. |
| // |
| // Call this function after any module installation where an existing loader |
| // is already alive and may be used again later. |
| // |
| // An error is returned if the manifest file cannot be read. |
| func (l *Loader) RefreshModules() error { |
| if l == nil { |
| // Nothing to do, then. |
| return nil |
| } |
| return l.modules.readModuleManifestSnapshot() |
| } |
| |
| // Parser returns the underlying parser for this loader. |
| // |
| // This is useful for loading other sorts of files than the module directories |
| // that a loader deals with, since then they will share the source code cache |
| // for this loader and can thus be shown as snippets in diagnostic messages. |
| func (l *Loader) Parser() *configs.Parser { |
| return l.parser |
| } |
| |
| // Sources returns the source code cache for the underlying parser of this |
| // loader. This is a shorthand for l.Parser().Sources(). |
| func (l *Loader) Sources() map[string][]byte { |
| return l.parser.Sources() |
| } |
| |
| // IsConfigDir returns true if and only if the given directory contains at |
| // least one Terraform configuration file. This is a wrapper around calling |
| // the same method name on the loader's parser. |
| func (l *Loader) IsConfigDir(path string) bool { |
| return l.parser.IsConfigDir(path) |
| } |
| |
| // ImportSources writes into the receiver's source code the given source |
| // code buffers. |
| // |
| // This is useful in the situation where an ancillary loader is created for |
| // some reason (e.g. loading config from a plan file) but the cached source |
| // code from that loader must be imported into the "main" loader in order |
| // to return source code snapshots in diagnostic messages. |
| // |
| // loader.ImportSources(otherLoader.Sources()) |
| func (l *Loader) ImportSources(sources map[string][]byte) { |
| p := l.Parser() |
| for name, src := range sources { |
| p.ForceFileSource(name, src) |
| } |
| } |
| |
| // ImportSourcesFromSnapshot writes into the receiver's source code the |
| // source files from the given snapshot. |
| // |
| // This is similar to ImportSources but knows how to unpack and flatten a |
| // snapshot data structure to get the corresponding flat source file map. |
| func (l *Loader) ImportSourcesFromSnapshot(snap *Snapshot) { |
| p := l.Parser() |
| for _, m := range snap.Modules { |
| baseDir := m.Dir |
| for fn, src := range m.Files { |
| fullPath := filepath.Join(baseDir, fn) |
| p.ForceFileSource(fullPath, src) |
| } |
| } |
| } |