| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package parser | |
| import ( | |
| "fmt" | |
| "github.com/apparentlymart/go-versions/versions" | |
| "github.com/hashicorp/go-slug/sourceaddrs" | |
| "github.com/hashicorp/go-slug/sourcebundle" | |
| "github.com/hashicorp/go-version" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // SourceBundleModuleWalker is an implementation of [configs.ModuleWalker] | |
| // that loads all modules from a single source bundle. | |
| type SourceBundleModuleWalker struct { | |
| absoluteSourceAddrs map[string]sourceaddrs.FinalSource | |
| sources *sourcebundle.Bundle | |
| parser *configs.SourceBundleParser | |
| } | |
| func NewSourceBundleModuleWalker(rootModuleSource sourceaddrs.FinalSource, sources *sourcebundle.Bundle, parser *configs.SourceBundleParser) *SourceBundleModuleWalker { | |
| absoluteSourceAddrs := make(map[string]sourceaddrs.FinalSource, 1) | |
| absoluteSourceAddrs[addrs.RootModule.String()] = rootModuleSource | |
| return &SourceBundleModuleWalker{ | |
| absoluteSourceAddrs: absoluteSourceAddrs, | |
| sources: sources, | |
| parser: parser, | |
| } | |
| } | |
| // LoadModule implements configs.ModuleWalker. | |
| func (w *SourceBundleModuleWalker) LoadModule(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) { | |
| var diags hcl.Diagnostics | |
| // First we need to assemble the "final source address" for the module | |
| // by asking the source bundle to match the given source address and | |
| // version against what's in the bundle manifest. This should cause | |
| // us to make the same decision that the source bundler made about | |
| // which real package to use. | |
| finalSourceAddr, err := w.finalSourceForModule(req.SourceAddr, &req.VersionConstraint.Required) | |
| if err != nil { | |
| // We should not typically get here because we're translating | |
| // Terraform's own source address representations to the same | |
| // representations the source bundle builder would've used, but | |
| // we'll be robust about it nonetheless. | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Can't load module for component", | |
| Detail: fmt.Sprintf("Invalid source address: %s.", err), | |
| Subject: req.SourceAddrRange.Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| absoluteSourceAddr, err := w.absoluteSourceAddr(finalSourceAddr, req.Parent) | |
| if err != nil { | |
| // Again, this should not happen, but let's ensure we can debug if it | |
| // does. | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Can't load module for component", | |
| Detail: fmt.Sprintf("Unable to determine absolute source address: %s.", err), | |
| Subject: req.SourceAddrRange.Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| // We store the absolute source address for this module so that any in-repo | |
| // child modules can use it to construct their absolute source addresses | |
| // too. | |
| w.absoluteSourceAddrs[req.Path.String()] = absoluteSourceAddr | |
| _, err = w.sources.LocalPathForSource(absoluteSourceAddr) | |
| if err != nil { | |
| // We should not get here if the source bundle was constructed | |
| // correctly. | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Can't load module for component", | |
| Detail: fmt.Sprintf("Failed to load this component's module %s: %s.", req.Path.String(), tfdiags.FormatError(err)), | |
| Subject: req.SourceAddrRange.Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| mod, moreDiags := w.parser.LoadConfigDir(absoluteSourceAddr) | |
| diags = append(diags, moreDiags...) | |
| // Annoyingly we now need to translate our version selection back into | |
| // the legacy type again, so we can return it through the ModuleWalker API. | |
| var legacyV *version.Version | |
| if modSrc, ok := finalSourceAddr.(sourceaddrs.RegistrySourceFinal); ok { | |
| legacyV, err = w.legacyVersionForVersion(modSrc.SelectedVersion()) | |
| if err != nil { | |
| // It would be very strange to get in here because by now we've | |
| // already round-tripped between the legacy and modern version | |
| // constraint representations once, so we should have a version | |
| // number that's compatible with both. | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Can't load module for component", | |
| Detail: fmt.Sprintf("Invalid version string %q: %s.", modSrc.SelectedVersion(), err), | |
| Subject: req.SourceAddrRange.Ptr(), | |
| }) | |
| } | |
| } | |
| return mod, legacyV, diags | |
| } | |
| func (w *SourceBundleModuleWalker) finalSourceForModule(tfSourceAddr addrs.ModuleSource, versionConstraints *version.Constraints) (sourceaddrs.FinalSource, error) { | |
| // Unfortunately the configs package still uses our old model of version | |
| // constraints and Terraform's own form of source addresses, so we need | |
| // to adapt to what the sourcebundle API is expecting. | |
| sourceAddr, err := w.bundleSourceAddrForTerraformSourceAddr(tfSourceAddr) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var allowedVersions versions.Set | |
| if versionConstraints != nil { | |
| allowedVersions, err = w.versionSetForLegacyVersionConstraints(versionConstraints) | |
| if err != nil { | |
| return nil, fmt.Errorf("invalid version constraints: %w", err) | |
| } | |
| } else { | |
| allowedVersions = versions.Released | |
| } | |
| switch sourceAddr := sourceAddr.(type) { | |
| case sourceaddrs.FinalSource: | |
| // Most source address types are already final source addresses. | |
| return sourceAddr, nil | |
| case sourceaddrs.RegistrySource: | |
| // Registry sources are trickier because we need to figure out which | |
| // exact version we're using. | |
| vs := w.sources.RegistryPackageVersions(sourceAddr.Package()) | |
| v := vs.NewestInSet(allowedVersions) | |
| return sourceAddr.Versioned(v), nil | |
| default: | |
| // Should not get here because the above should be exhaustive for all | |
| // possible address types. | |
| return nil, fmt.Errorf("unsupported source address type %T", tfSourceAddr) | |
| } | |
| } | |
| func (w *SourceBundleModuleWalker) bundleSourceAddrForTerraformSourceAddr(tfSourceAddr addrs.ModuleSource) (sourceaddrs.Source, error) { | |
| // In practice this should always succeed because the source bundle builder | |
| // would've parsed the same source addresses using these same parsers | |
| // and so source bundle building would've failed if the given address were | |
| // outside the subset supported for source bundles. | |
| switch tfSourceAddr := tfSourceAddr.(type) { | |
| case addrs.ModuleSourceLocal: | |
| return sourceaddrs.ParseLocalSource(tfSourceAddr.String()) | |
| case addrs.ModuleSourceRemote: | |
| return sourceaddrs.ParseRemoteSource(tfSourceAddr.String()) | |
| case addrs.ModuleSourceRegistry: | |
| return sourceaddrs.ParseRegistrySource(tfSourceAddr.String()) | |
| default: | |
| // Should not get here because the above should be exhaustive for all | |
| // possible address types. | |
| return nil, fmt.Errorf("unsupported source address type %T", tfSourceAddr) | |
| } | |
| } | |
| func (w *SourceBundleModuleWalker) absoluteSourceAddr(sourceAddr sourceaddrs.FinalSource, parent *configs.Config) (sourceaddrs.FinalSource, error) { | |
| switch source := sourceAddr.(type) { | |
| case sourceaddrs.LocalSource: | |
| parentPath := addrs.RootModule | |
| if parent != nil { | |
| parentPath = parent.Path | |
| } | |
| absoluteParentSourceAddr, ok := w.absoluteSourceAddrs[parentPath.String()] | |
| if !ok { | |
| return nil, fmt.Errorf("unexpected missing source address for module parent %q", parentPath) | |
| } | |
| return sourceaddrs.ResolveRelativeFinalSource(absoluteParentSourceAddr, source) | |
| default: | |
| return sourceAddr, nil | |
| } | |
| } | |
| func (w *SourceBundleModuleWalker) versionSetForLegacyVersionConstraints(versionConstraints *version.Constraints) (versions.Set, error) { | |
| // In practice this should always succeed because the source bundle builder | |
| // would've parsed the same version constraints using this same parser | |
| // and so source bundle building would've failed if the given address were | |
| // outside the subset supported for source bundles. | |
| return versions.MeetingConstraintsStringRuby(versionConstraints.String()) | |
| } | |
| func (w *SourceBundleModuleWalker) legacyVersionForVersion(v versions.Version) (*version.Version, error) { | |
| return version.NewVersion(v.String()) | |
| } |