// Copyright (c) HashiCorp, Inc. // 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 }