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
234 lines
7.3 KiB
Go
234 lines
7.3 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package stackconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-slug/sourceaddrs"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
hcljson "github.com/hashicorp/hcl/v2/json"
|
|
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
const initialLanguageEdition = "TFStack2023"
|
|
|
|
// File represents the content of a single .tfcomponent.hcl or .tfcomponent.json
|
|
// file before it's been merged with its siblings in the same directory to
|
|
// produce the overall [Stack] object.
|
|
type File struct {
|
|
// SourceAddr is the source location for this particular file, meaning
|
|
// that the "sub-path" portion of the address should always be populated
|
|
// and refer to a particular file rather than to a directory.
|
|
SourceAddr sourceaddrs.FinalSource
|
|
|
|
Declarations
|
|
}
|
|
|
|
// DecodeFileBody takes a body that is assumed to represent the root of a
|
|
// .tfcomponent.hcl or .tfcomponent.json file and decodes the declarations
|
|
// inside.
|
|
//
|
|
// If you have a []byte containing source code then consider using [ParseFile]
|
|
// instead, which parses the source code and then delegates to this function.
|
|
//
|
|
// This is exported for unusual situations where it's useful to analyze just
|
|
// a single file 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 DecodeFileBody(body hcl.Body, fileAddr sourceaddrs.FinalSource) (*File, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
ret := &File{
|
|
SourceAddr: fileAddr,
|
|
Declarations: makeDeclarations(),
|
|
}
|
|
|
|
content, hclDiags := body.Content(rootConfigSchema)
|
|
diags = diags.Append(hclDiags)
|
|
if content == nil {
|
|
return ret, diags
|
|
}
|
|
// Even if there are some errors we'll still try to analyze a partial
|
|
// result, in case it allows us to give the user more context to work
|
|
// with when resolving the errors detected so far.
|
|
|
|
if langAttr, ok := content.Attributes["language"]; ok {
|
|
// For now there is only one edition of the language and so we'll just
|
|
// reject anything other than the current version. If we add other
|
|
// editions later then we'll probably need to move the check for this
|
|
// up into LoadSingleStackConfig so we can make sure that all of the
|
|
// files in a directory agree on a language edition to use.
|
|
editionKW := hcl.ExprAsKeyword(langAttr.Expr)
|
|
if editionKW != initialLanguageEdition {
|
|
var extra string
|
|
if strings.HasPrefix(editionKW, "TFStack") {
|
|
extra = "\n\nThis stack configuration might be intended for a newer version of Terraform."
|
|
}
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid language edition",
|
|
Detail: fmt.Sprintf(
|
|
"If you declare an explicit language edition then it must currently be the keyword %s, because no other editions are supported.%s",
|
|
initialLanguageEdition, extra,
|
|
),
|
|
})
|
|
// We'll halt processing here if it's not for our current edition,
|
|
// because we'll probably encounter language features from whatever
|
|
// later language edition this config was written for.
|
|
return ret, diags
|
|
}
|
|
}
|
|
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
|
|
case "component":
|
|
decl, moreDiags := decodeComponentBlock(block)
|
|
diags = diags.Append(moreDiags)
|
|
diags = diags.Append(
|
|
ret.Declarations.addComponent(decl),
|
|
)
|
|
|
|
case "stack":
|
|
decl, moreDiags := decodeEmbeddedStackBlock(block)
|
|
diags = diags.Append(moreDiags)
|
|
diags = diags.Append(
|
|
ret.Declarations.addEmbeddedStack(decl),
|
|
)
|
|
|
|
case "variable":
|
|
decl, moreDiags := decodeInputVariableBlock(block)
|
|
diags = diags.Append(moreDiags)
|
|
diags = diags.Append(
|
|
ret.Declarations.addInputVariable(decl),
|
|
)
|
|
|
|
case "locals":
|
|
decls, moreDiags := decodeLocalValuesBlock(block)
|
|
diags = diags.Append(moreDiags)
|
|
for _, decl := range decls {
|
|
diags = diags.Append(
|
|
ret.Declarations.addLocalValue(decl),
|
|
)
|
|
}
|
|
|
|
case "output":
|
|
decl, moreDiags := decodeOutputValueBlock(block)
|
|
diags = diags.Append(moreDiags)
|
|
diags = diags.Append(
|
|
ret.Declarations.addOutputValue(decl),
|
|
)
|
|
|
|
case "provider":
|
|
decl, moreDiags := decodeProviderConfigBlock(block)
|
|
diags = diags.Append(moreDiags)
|
|
diags = diags.Append(
|
|
ret.Declarations.addProviderConfig(decl),
|
|
)
|
|
|
|
case "required_providers":
|
|
decl, moreDiags := decodeProviderRequirementsBlock(block)
|
|
diags = diags.Append(moreDiags)
|
|
diags = diags.Append(
|
|
ret.Declarations.addRequiredProviders(decl),
|
|
)
|
|
|
|
case "removed":
|
|
decl, moreDiags := decodeRemovedBlock(block)
|
|
diags = diags.Append(moreDiags)
|
|
diags = diags.Append(
|
|
ret.Declarations.addRemoved(decl),
|
|
)
|
|
|
|
default:
|
|
// Should not get here because the cases above should be exhaustive
|
|
// for everything declared in rootConfigSchema.
|
|
panic(fmt.Sprintf("unhandled block type %q", block.Type))
|
|
}
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
// ParseFileSource parses the given source code as the content of either a
|
|
// .tfcomponent.hcl or .tfcomponent.json file, and then delegates the result to
|
|
// [DecodeFileBody] for analysis, returning that final result.
|
|
//
|
|
// ParseFileSource chooses between native vs. JSON syntax based on the suffix
|
|
// of the filename in the given source address, which must be either
|
|
// ".tfcomponent.hcl" or ".tfcomponent.json".
|
|
func ParseFileSource(src []byte, fileAddr sourceaddrs.FinalSource) (*File, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
filename := sourceaddrs.FinalSourceFilename(fileAddr)
|
|
|
|
var body hcl.Body
|
|
switch validFilenameSuffix(filename) {
|
|
case ".tfcomponent.hcl":
|
|
hclFile, hclDiags := hclsyntax.ParseConfig(src, fileAddr.String(), hcl.InitialPos)
|
|
diags = diags.Append(hclDiags)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
body = hclFile.Body
|
|
case ".tfcomponent.json":
|
|
hclFile, hclDiags := hcljson.Parse(src, fileAddr.String())
|
|
diags = diags.Append(hclDiags)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
body = hclFile.Body
|
|
default:
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Unsupported file type",
|
|
fmt.Sprintf(
|
|
"Cannot load %s as a stack configuration file: filename must have either a .tfcomponent.hcl or .tfcomponent.json suffix.",
|
|
fileAddr,
|
|
),
|
|
))
|
|
return nil, diags
|
|
}
|
|
|
|
ret, moreDiags := DecodeFileBody(body, fileAddr)
|
|
diags = diags.Append(moreDiags)
|
|
return ret, diags
|
|
}
|
|
|
|
// validFilenameSuffix returns ".tfcomponent.hcl" or ".tfcomponent.json" if the
|
|
// given filename ends with that suffix, and otherwise returns an empty
|
|
// string to indicate that the suffix was invalid.
|
|
func validFilenameSuffix(filename string) string {
|
|
const nativeSuffix = ".tfcomponent.hcl"
|
|
const jsonSuffix = ".tfcomponent.json"
|
|
|
|
switch {
|
|
case strings.HasSuffix(filename, nativeSuffix):
|
|
return nativeSuffix
|
|
case strings.HasSuffix(filename, jsonSuffix):
|
|
return jsonSuffix
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
var rootConfigSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: "language"},
|
|
},
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: "stack", LabelNames: []string{"name"}},
|
|
{Type: "component", LabelNames: []string{"name"}},
|
|
{Type: "variable", LabelNames: []string{"name"}},
|
|
{Type: "locals"},
|
|
{Type: "output", LabelNames: []string{"name"}},
|
|
{Type: "provider", LabelNames: []string{"type", "name"}},
|
|
{Type: "required_providers"},
|
|
{Type: "removed"},
|
|
},
|
|
}
|