You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/stacks/stackconfig/stack.go

189 lines
6.7 KiB

// 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(),
}
var priorSuffix string
for _, entry := range allEntries {
suffix := validFilenameSuffix(entry.Name())
if suffix == "" {
// not a file we're interested in, then
continue
}
if priorSuffix == "" {
priorSuffix = suffix
} else if priorSuffix != suffix {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid stack configuration directory",
"This stack configuration has mixed the deprecated .tfstack.hcl or .tfstack.json file extensions with the latest .tfcomponent.hcl or .tfcomponent.json file extensions. Update all files to the latest .tfcomponent file extension.",
))
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, priorSuffix, 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
}
if priorSuffix == "tfstack" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Deprecated filename usage",
Detail: "This configuration is using the deprecated .tfstack.hcl or .tfstack.json file extensions. This will not be supported in a future version of Terraform, please update your files to use the latest .tfcomponent.hcl or .tfcomponent.json file extensions.",
})
}
return ret, diags
}