| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package moduleref | |
| import ( | |
| "maps" | |
| "strings" | |
| "github.com/hashicorp/go-version" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/modsdir" | |
| ) | |
| // Resolver is the struct responsible for finding all modules references in | |
| // Terraform configuration for a given internal module manifest. | |
| type Resolver struct { | |
| manifest *Manifest | |
| internalManifest modsdir.Manifest | |
| } | |
| // NewResolver creates a new Resolver, storing a copy of the internal manifest | |
| // that is passed. | |
| func NewResolver(internalManifest modsdir.Manifest) *Resolver { | |
| // Since maps are pointers, create a copy of the internal manifest to | |
| // prevent introducing side effects to the original | |
| internalManifestCopy := maps.Clone(internalManifest) | |
| // Remove the root module entry from the internal manifest as it is | |
| // never directly referenced. | |
| delete(internalManifestCopy, "") | |
| return &Resolver{ | |
| internalManifest: internalManifestCopy, | |
| manifest: &Manifest{ | |
| FormatVersion: FormatVersion, | |
| Records: Records{}, | |
| }, | |
| } | |
| } | |
| // Resolve will attempt to find all module references for the passed configuration | |
| // and return a new manifest encapsulating this information. | |
| func (r *Resolver) Resolve(cfg *configs.Config) *Manifest { | |
| // First find all the referenced modules. | |
| r.findAndTrimReferencedEntries(cfg, nil, nil) | |
| return r.manifest | |
| } | |
| // findAndTrimReferencedEntries will traverse a given Terraform configuration | |
| // and attempt find a caller for every entry in the internal module manifest. | |
| // If an entry is found, it will be removed from the internal manifest and | |
| // appended to the manifest that records this new information in a nested heirarchy. | |
| func (r *Resolver) findAndTrimReferencedEntries(cfg *configs.Config, parentRecord *Record, parentKey *string) { | |
| var name string | |
| var versionConstraints version.Constraints | |
| if parentKey != nil { | |
| for key, child := range cfg.Parent.Children { | |
| if key == *parentKey { | |
| name = key | |
| versionConstraints = child.VersionConstraint.Required | |
| break | |
| } | |
| } | |
| } | |
| childRecord := &Record{ | |
| Key: name, | |
| Source: cfg.SourceAddr, | |
| VersionConstraints: versionConstraints, | |
| } | |
| key := strings.Join(cfg.Path, ".") | |
| for entryKey, entry := range r.internalManifest { | |
| if entryKey == key { | |
| // Use resolved version from manifest | |
| childRecord.Version = entry.Version | |
| if parentRecord.Source != nil { | |
| parentRecord.addChild(childRecord) | |
| } else { | |
| r.manifest.addModuleEntry(childRecord) | |
| } | |
| // "Trim" the entry from the internal manifest, saving us cycles | |
| // as we descend into the module tree. | |
| delete(r.internalManifest, entryKey) | |
| break | |
| } | |
| } | |
| // Traverse the child configurations | |
| for childKey, childCfg := range cfg.Children { | |
| r.findAndTrimReferencedEntries(childCfg, childRecord, &childKey) | |
| } | |
| } |