mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-19 02:39:17 -05:00
* stacks: remove support for deprecated .tfstack extension * also remove from comments and readme
167 lines
5.8 KiB
Go
167 lines
5.8 KiB
Go
// 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
|
|
}
|