opentofu/internal/configs/static_scope.go
Christian Mesh 0d1e6cd5f0
Handle static variable secret flag (#2045)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
2024-10-03 10:46:58 -04:00

273 lines
9.4 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package configs
import (
"fmt"
"os"
"path/filepath"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/didyoumean"
"github.com/opentofu/opentofu/internal/lang"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// newStaticScope creates a lang.Scope that's backed by the static view of the module represented by the StaticEvaluator
func newStaticScope(eval *StaticEvaluator, stack ...StaticIdentifier) *lang.Scope {
return &lang.Scope{
Data: staticScopeData{eval, stack},
ParseRef: addrs.ParseRef,
BaseDir: ".", // Always current working directory for now. (same as Evaluator.Scope())
PureOnly: false,
ConsoleMode: false,
}
}
// This structure represents the data required to evaluate a specific identifier reference (top of the stack)
// It is used by lang.Scope to link the given StaticEvaluator data to addrs.References in the current scope.
type staticScopeData struct {
eval *StaticEvaluator
stack []StaticIdentifier
}
// staticScopeData must implement lang.Data
var _ lang.Data = (*staticScopeData)(nil)
// Creates a nested scope to evaluate nested references
func (s staticScopeData) scope(ident StaticIdentifier) (*lang.Scope, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
for _, frame := range s.stack {
if frame.String() == ident.String() {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Circular reference",
Detail: fmt.Sprintf("%s is self referential", ident.String()), // TODO use stack in error message
Subject: ident.DeclRange.Ptr(),
})
}
}
return newStaticScope(s.eval, append(s.stack, ident)...), diags
}
// If an error occurs when resolving a dependent value, we need to add additional context to the diagnostics
func (s staticScopeData) enhanceDiagnostics(ident StaticIdentifier, diags tfdiags.Diagnostics) tfdiags.Diagnostics {
if diags.HasErrors() {
top := s.stack[len(s.stack)-1]
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unable to compute static value",
Detail: fmt.Sprintf("%s depends on %s which is not available", top, ident.String()),
Subject: top.DeclRange.Ptr(),
})
}
return diags
}
// Early check to only allow references we expect in a static context
func (s staticScopeData) StaticValidateReferences(refs []*addrs.Reference, _ addrs.Referenceable, _ addrs.Referenceable) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
top := s.stack[len(s.stack)-1]
for _, ref := range refs {
switch subject := ref.Subject.(type) {
case addrs.LocalValue:
continue
case addrs.InputVariable:
continue
case addrs.PathAttr:
continue
case addrs.TerraformAttr:
continue
case addrs.ModuleCallInstanceOutput:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Module output not supported in static context",
Detail: fmt.Sprintf("Unable to use %s in static context, which is required by %s", subject.String(), top.String()),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
case addrs.ProviderFunction:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider function in static context",
Detail: fmt.Sprintf("Unable to use %s in static context, which is required by %s", subject.String(), top.String()),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Dynamic value in static context",
Detail: fmt.Sprintf("Unable to use %s in static context, which is required by %s", subject.String(), top.String()),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
}
return diags
}
func (s staticScopeData) GetCountAttr(addrs.CountAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetForEachAttr(addrs.ForEachAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetResource(addrs.Resource, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetLocalValue(ident addrs.LocalValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
local, ok := s.eval.cfg.Locals[ident.Name]
if !ok {
return cty.DynamicVal, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Undefined local",
Detail: fmt.Sprintf("Undefined local %s", ident.String()),
Subject: rng.ToHCL().Ptr(),
})
}
id := StaticIdentifier{
Module: s.eval.call.addr,
Subject: fmt.Sprintf("local.%s", local.Name),
DeclRange: local.DeclRange,
}
scope, scopeDiags := s.scope(id)
diags = diags.Append(scopeDiags)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
val, valDiags := scope.EvalExpr(local.Expr, cty.DynamicPseudoType)
return val, s.enhanceDiagnostics(id, diags.Append(valDiags))
}
func (s staticScopeData) GetModule(addrs.ModuleCall, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// TODO this is copied and trimmed down from tofu/evaluate.go GetPathAttr. Ideally this should be refactored to a common location.
var diags tfdiags.Diagnostics
switch addr.Name {
case "cwd":
wd, err := os.Getwd()
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Failed to get working directory`,
Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
wd, err = filepath.Abs(wd)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Failed to get working directory`,
Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
return cty.StringVal(filepath.ToSlash(wd)), diags
case "module":
return cty.StringVal(s.eval.cfg.SourceDir), diags
case "root":
return cty.StringVal(s.eval.call.rootPath), diags
default:
suggestion := didyoumean.NameSuggestion(addr.Name, []string{"cwd", "module", "root"})
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "path" attribute`,
Detail: fmt.Sprintf(`The "path" object does not have an attribute named %q.%s`, addr.Name, suggestion),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
}
func (s staticScopeData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// TODO this is copied and trimmed down from tofu/evaluate.go GetTerraformAttr. Ideally this should be refactored to a common location.
var diags tfdiags.Diagnostics
switch addr.Name {
case "workspace":
workspaceName := s.eval.call.workspace
return cty.StringVal(workspaceName), diags
case "env":
// Prior to Terraform 0.12 there was an attribute "env", which was
// an alias name for "workspace". This was deprecated and is now
// removed.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "terraform" attribute`,
Detail: `The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The "state environment" concept was renamed to "workspace" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.`,
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "terraform" attribute`,
Detail: fmt.Sprintf(`The "terraform" object does not have an attribute named %q. The only supported attribute is terraform.workspace, the name of the currently-selected workspace.`, addr.Name),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
}
func (s staticScopeData) GetInputVariable(ident addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
variable, ok := s.eval.cfg.Variables[ident.Name]
if !ok {
return cty.NilVal, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Undefined variable",
Detail: fmt.Sprintf("Undefined variable %s", ident.String()),
Subject: rng.ToHCL().Ptr(),
})
}
id := StaticIdentifier{
Module: s.eval.call.addr,
Subject: fmt.Sprintf("var.%s", variable.Name),
DeclRange: variable.DeclRange,
}
val, valDiags := s.eval.call.vars(variable)
if variable.Sensitive {
val = val.Mark(marks.Sensitive)
}
return val, s.enhanceDiagnostics(id, diags.Append(valDiags))
}
func (s staticScopeData) GetOutput(addrs.OutputValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetCheckBlock(addrs.Check, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}