| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackconfig | |
| import ( | |
| "fmt" | |
| "os" | |
| "path/filepath" | |
| "github.com/hashicorp/go-slug/sourceaddrs" | |
| "github.com/hashicorp/go-slug/sourcebundle" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // Stack represents a single stack, which can potentially call other | |
| // "embedded stacks" in a similar manner to how Terraform modules can call | |
| // other modules. | |
| type Stack struct { | |
| SourceAddr sourceaddrs.FinalSource | |
| // ConfigFiles describes the individual .tfcomponent.hcl or | |
| // .tfcomponent.json files that this stack configuration object was built | |
| // from. Most callers should ignore the detail of which file each | |
| // declaration originated in, but we retain this in case it's useful for | |
| // generating better error messages, etc. | |
| // | |
| // The keys of this map are the string representations of each file's | |
| // source address, which also matches how we populate the "Filename" | |
| // field of source ranges referring to the files and so callers can | |
| // attempt to look up files by the diagnostic range filename, but must | |
| // be resilient to cases where nothing matches because not all diagnostics | |
| // will refer to stack configuration files. | |
| ConfigFiles map[string]*File | |
| Declarations | |
| } | |
| // LoadSingleStackConfig loads the configuration for only a single stack from | |
| // the given source address. | |
| // | |
| // If the given address is a local source then it's interpreted relative to | |
| // the process's current working directory. Otherwise it will be loaded from | |
| // the provided source bundle. | |
| // | |
| // This is exported for unusual situations where it's useful to analyze just | |
| // a single stack configuration directory in isolation, without considering | |
| // its context in a configuration tree. Some fields of the objects representing | |
| // declarations in the configuration will be unpopulated when loading through | |
| // this entry point. Prefer [LoadConfigDir] in most cases. | |
| func LoadSingleStackConfig(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bundle) (*Stack, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| localDir, err := sources.LocalPathForSource(sourceAddr) | |
| if err != nil { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Cannot find configuration source code", | |
| fmt.Sprintf("Failed to load %s from the pre-installed source packages: %s.", sourceAddr, err), | |
| )) | |
| return nil, diags | |
| } | |
| allEntries, err := os.ReadDir(localDir) | |
| if err != nil { | |
| if os.IsNotExist(err) { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Missing stack configuration", | |
| fmt.Sprintf("There is no stack configuration directory at %s.", sourceAddr), | |
| )) | |
| } else { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Cannot read stack configuration", | |
| // In this case the error message from the Go standard library | |
| // is likely to disclose the real local directory name | |
| // from the source bundle, but that's okay because it may | |
| // sometimes help with debugging. | |
| fmt.Sprintf("Error while reading the cached snapshot of %s: %s.", sourceAddr, err), | |
| )) | |
| } | |
| return nil, diags | |
| } | |
| ret := &Stack{ | |
| SourceAddr: sourceAddr, | |
| ConfigFiles: make(map[string]*File), | |
| Declarations: makeDeclarations(), | |
| } | |
| for _, entry := range allEntries { | |
| if suffix := validFilenameSuffix(entry.Name()); suffix == "" { | |
| // not a file we're interested in, then | |
| continue | |
| } | |
| asLocalSourcePath := "./" + filepath.Base(entry.Name()) | |
| relSource, err := sourceaddrs.ParseLocalSource(asLocalSourcePath) | |
| if err != nil { | |
| // If we get here then it's a bug in how we constructed the | |
| // path above, not invalid user input. | |
| panic(fmt.Sprintf("constructed invalid relative source path: %s", err)) | |
| } | |
| fileSourceAddr, err := sourceaddrs.ResolveRelativeFinalSource(sourceAddr, relSource) | |
| if err != nil { | |
| // If we get here then it's a bug in how we constructed the | |
| // path above, not invalid user input. | |
| panic(fmt.Sprintf("constructed invalid relative source path: %s", err)) | |
| } | |
| if entry.IsDir() { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Invalid stack configuration directory", | |
| fmt.Sprintf("The entry %s is a directory. All entries with the stack configuration name suffixes must be files.", fileSourceAddr), | |
| )) | |
| } | |
| src, err := os.ReadFile(filepath.Join(localDir, entry.Name())) | |
| if err != nil { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Cannot read stack configuration", | |
| // In this case the error message from the Go standard library | |
| // is likely to disclose the real local directory name | |
| // from the source bundle, but that's okay because it may | |
| // sometimes help with debugging. | |
| fmt.Sprintf("Error while reading the cached snapshot of %s: %s.", fileSourceAddr, err), | |
| )) | |
| } | |
| file, moreDiags := ParseFileSource(src, fileSourceAddr) | |
| diags = diags.Append(moreDiags) | |
| if moreDiags.HasErrors() { | |
| // We'll still try to analyze other files, so we can gather up | |
| // as many diagnostics as possible to return all together in | |
| // case there's some pattern between them that the user can | |
| // fix systematically across all instances. | |
| continue | |
| } | |
| // Incorporate this file's declarations into the overall stack | |
| // configuration. | |
| diags = diags.Append(ret.Declarations.merge(&file.Declarations)) | |
| ret.ConfigFiles[file.SourceAddr.String()] = file | |
| } | |
| for _, pc := range ret.ProviderConfigs { | |
| localName := pc.LocalAddr.LocalName | |
| providerAddr, ok := ret.RequiredProviders.ProviderForLocalName(localName) | |
| if !ok { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Undeclared provider local name", | |
| Detail: fmt.Sprintf( | |
| "This configuration's required_providers block does not include a definition for the local name %q.", | |
| localName, | |
| ), | |
| }) | |
| continue | |
| } | |
| pc.ProviderAddr = providerAddr | |
| } | |
| return ret, diags | |
| } |