| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package configload | |
| import ( | |
| "fmt" | |
| "path/filepath" | |
| "github.com/hashicorp/terraform-svchost/disco" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/modsdir" | |
| "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 descendant modules that are | |
| // referenced (directly or indirectly) from the root module. | |
| modules moduleMgr | |
| parserOpts []configs.Option | |
| } | |
| // Config is used with NewLoader to specify configuration arguments for the | |
| // loader. | |
| type Config struct { | |
| // ModulesDir is a path to a directory where descendant 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 | |
| // IncludeQueryFiles is set to true if query files should be parsed | |
| // when running query commands. | |
| IncludeQueryFiles bool | |
| // IncludeStateMigrateFiles is set to true if state migration files should | |
| // be parsed when running state migrate commands. | |
| IncludeStateMigrateFiles bool | |
| // OverrideFS is used to override the filesystem that the loader | |
| // reads configuration and module files from. | |
| OverrideFS afero.Fs | |
| } | |
| // 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() | |
| if config.OverrideFS != nil { | |
| fs = config.OverrideFS | |
| } | |
| 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, | |
| }, | |
| parserOpts: make([]configs.Option, 0), | |
| } | |
| err := ret.modules.readModuleManifestSnapshot() | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to read module manifest: %s", err) | |
| } | |
| if config.IncludeQueryFiles { | |
| ret.parserOpts = append(ret.parserOpts, configs.MatchQueryFiles()) | |
| } | |
| if config.IncludeStateMigrateFiles { | |
| ret.parserOpts = append(ret.parserOpts, configs.MatchStateMigrateFiles()) | |
| } | |
| 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 | |
| } | |
| func (l *Loader) ModuleManifest() modsdir.Manifest { | |
| return l.modules.manifest | |
| } | |
| // 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, l.parserOpts...) | |
| } | |
| // ImportSources writes into the receiver's source code map 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) | |
| } | |
| } | |
| } | |
| // AllowLanguageExperiments specifies whether subsequent LoadConfig (and | |
| // similar) calls will allow opting in to experimental language features. | |
| // | |
| // If this method is never called for a particular loader, the default behavior | |
| // is to disallow language experiments. | |
| // | |
| // Main code should set this only for alpha or development builds. Test code | |
| // is responsible for deciding for itself whether and how to call this | |
| // method. | |
| func (l *Loader) AllowLanguageExperiments(allowed bool) { | |
| l.parser.AllowLanguageExperiments(allowed) | |
| } | |
| // AllowsLanguageExperiments returns the value most recently passed to | |
| // [Loader.AllowLanguageExperiments], or false if that method has not been | |
| // called on this object. | |
| func (l *Loader) AllowsLanguageExperiments() bool { | |
| return l.parser.AllowsLanguageExperiments() | |
| } | |
| // ModuleWalker returns a walker suitable for loading already-installed modules. | |
| func (l *Loader) ModuleWalker() configs.ModuleWalker { | |
| return configs.ModuleWalkerFunc(l.moduleWalkerLoad) | |
| } |