From 19b5287b8ff2205d1bdf976553992e519ac02f01 Mon Sep 17 00:00:00 2001 From: Oleksandr Levchenkov Date: Mon, 24 Jun 2024 17:18:16 +0300 Subject: [PATCH] allow static evaluations in encryption configuration (#1728) Signed-off-by: ollevche Signed-off-by: Christian Mesh Signed-off-by: Oleksandr Levchenkov Co-authored-by: Christian Mesh --- CHANGELOG.md | 1 + .../encryption-flow/broken.tf.disabled | 13 +++- internal/command/meta_encryption.go | 2 +- internal/configs/module.go | 11 +-- internal/configs/parser_config_dir_test.go | 2 +- internal/configs/static_evaluator.go | 9 +++ internal/encryption/base.go | 15 ++-- internal/encryption/config/config.go | 10 +++ internal/encryption/encryption.go | 11 +-- internal/encryption/enctest/setup.go | 5 +- internal/encryption/example_test.go | 6 +- internal/encryption/keyprovider.go | 65 +++++++++++------ .../keyprovider/static/example_test.go | 5 +- internal/encryption/plan.go | 5 +- internal/encryption/state.go | 5 +- internal/encryption/targets.go | 3 + internal/encryption/targets_test.go | 64 +++++++++++++++-- internal/lang/eval.go | 27 +++++-- internal/lang/eval_test.go | 72 +++++++++++++++++++ .../encryption/fallback_from_unencrypted.tf | 8 ++- .../state/examples/encryption/sample.tf | 8 ++- 21 files changed, 281 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 696e54b08b..5bd164fc1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ENHANCEMENTS: * Make state persistence interval configurable via `TF_STATE_PERSIST_INTERVAL` environment variable ([#1591](https://github.com/opentofu/opentofu/pull/1591)) * Improved performance of writing state files and reduced their size using compact json encoding. ([#1647](https://github.com/opentofu/opentofu/pull/1647)) * Allow to reference variable inside the `variables` block of a test file. ([1488](https://github.com/opentofu/opentofu/pull/1488)) +* Allow variables and other static values to be used in encryption configuration. ([1728](https://github.com/opentofu/opentofu/pull/1728)) BUG FIXES: * Fixed validation for `enforced` flag in encryption configuration. ([#1711](https://github.com/opentofu/opentofu/pull/1711)) diff --git a/internal/command/e2etest/testdata/encryption-flow/broken.tf.disabled b/internal/command/e2etest/testdata/encryption-flow/broken.tf.disabled index 54c3746de9..46176db92d 100644 --- a/internal/command/e2etest/testdata/encryption-flow/broken.tf.disabled +++ b/internal/command/e2etest/testdata/encryption-flow/broken.tf.disabled @@ -1,8 +1,17 @@ +variable "passphrase" { + type = string + default = "aaaaaaaa-83f1-47ec-9b2d-2aebf6417167" +} + +locals { + key_length = 32 +} + terraform { encryption { key_provider "pbkdf2" "basic" { - passphrase = "aaaaaaaa-83f1-47ec-9b2d-2aebf6417167" - key_length = 32 + passphrase = var.passphrase + key_length = local.key_length iterations = 200000 hash_function = "sha512" salt_length = 12 diff --git a/internal/command/meta_encryption.go b/internal/command/meta_encryption.go index 0235433be1..379e43b690 100644 --- a/internal/command/meta_encryption.go +++ b/internal/command/meta_encryption.go @@ -52,7 +52,7 @@ func (m *Meta) EncryptionFromModule(module *configs.Module) (encryption.Encrypti cfg = cfg.Merge(envCfg) } - enc, encDiags := encryption.New(encryption.DefaultRegistry, cfg) + enc, encDiags := encryption.New(encryption.DefaultRegistry, cfg, module.StaticEvaluator) diags = diags.Append(encDiags) return enc, diags diff --git a/internal/configs/module.go b/internal/configs/module.go index d8b8a64b1c..f1de7856b4 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -64,6 +64,9 @@ type Module struct { // IsOverridden indicates if the module is being overridden. It's used in // testing framework to not call the underlying module. IsOverridden bool + + // StaticEvaluator is used to evaluate static expressions in the scope of the Module. + StaticEvaluator *StaticEvaluator } // File describes the contents of a single configuration file. @@ -186,20 +189,20 @@ func NewModule(primaryFiles, overrideFiles []*File, call StaticModuleCall, sourc } // Static evaluation to build a StaticContext now that module has all relevant Locals / Variables - eval := NewStaticEvaluator(mod, call) + mod.StaticEvaluator = NewStaticEvaluator(mod, call) // If we have a backend, it may have fields that require locals/vars if mod.Backend != nil { // We don't know the backend type / loader at this point so we save the context for later use - mod.Backend.Eval = eval + mod.Backend.Eval = mod.StaticEvaluator } if mod.CloudConfig != nil { - mod.CloudConfig.eval = eval + mod.CloudConfig.eval = mod.StaticEvaluator } // Process all module calls now that we have the static context for _, mc := range mod.ModuleCalls { - mDiags := mc.decodeStaticFields(eval) + mDiags := mc.decodeStaticFields(mod.StaticEvaluator) diags = append(diags, mDiags...) } diff --git a/internal/configs/parser_config_dir_test.go b/internal/configs/parser_config_dir_test.go index ce4d422087..d4780ed860 100644 --- a/internal/configs/parser_config_dir_test.go +++ b/internal/configs/parser_config_dir_test.go @@ -272,7 +272,7 @@ func TestParserLoadConfigDirWithTests_TofuFiles(t *testing.T) { parser := NewParser(nil) path := tt.path - mod, diags := parser.LoadConfigDirWithTests(path, "test") + mod, diags := parser.LoadConfigDirWithTests(path, "test", RootModuleCallForTesting()) if len(diags) != 0 { t.Errorf("unexpected diagnostics") for _, diag := range diags { diff --git a/internal/configs/static_evaluator.go b/internal/configs/static_evaluator.go index d36023d81d..8e7b646398 100644 --- a/internal/configs/static_evaluator.go +++ b/internal/configs/static_evaluator.go @@ -117,3 +117,12 @@ func (s StaticEvaluator) DecodeBlock(body hcl.Body, spec hcldec.Spec, ident Stat diags = append(diags, valDiags...) return val, diags } + +func (s StaticEvaluator) EvalContext(ident StaticIdentifier, refs []*addrs.Reference) (*hcl.EvalContext, hcl.Diagnostics) { + return s.EvalContextWithParent(nil, ident, refs) +} + +func (s StaticEvaluator) EvalContextWithParent(parent *hcl.EvalContext, ident StaticIdentifier, refs []*addrs.Reference) (*hcl.EvalContext, hcl.Diagnostics) { + evalCtx, diags := s.scope(ident).EvalContextWithParent(parent, refs) + return evalCtx, diags.ToHCL() +} diff --git a/internal/encryption/base.go b/internal/encryption/base.go index c9bf048e07..69b5e80ad1 100644 --- a/internal/encryption/base.go +++ b/internal/encryption/base.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption/config" "github.com/opentofu/opentofu/internal/encryption/keyprovider" "github.com/opentofu/opentofu/internal/encryption/method" @@ -28,15 +29,17 @@ type baseEncryption struct { name string encMethods []method.Method encMeta map[keyprovider.Addr][]byte + staticEval *configs.StaticEvaluator } -func newBaseEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string) (*baseEncryption, hcl.Diagnostics) { +func newBaseEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string, staticEval *configs.StaticEvaluator) (*baseEncryption, hcl.Diagnostics) { base := &baseEncryption{ - enc: enc, - target: target, - enforced: enforced, - name: name, - encMeta: make(map[keyprovider.Addr][]byte), + enc: enc, + target: target, + enforced: enforced, + name: name, + encMeta: make(map[keyprovider.Addr][]byte), + staticEval: staticEval, } // Setup the encryptor // diff --git a/internal/encryption/config/config.go b/internal/encryption/config/config.go index 3c14f3749a..bc98ff6c56 100644 --- a/internal/encryption/config/config.go +++ b/internal/encryption/config/config.go @@ -31,6 +31,16 @@ func (c *EncryptionConfig) Merge(override *EncryptionConfig) *EncryptionConfig { return MergeConfigs(c, override) } +// GetKeyProvider takes type and name arguments to find a respective KeyProviderConfig in the list. +func (c *EncryptionConfig) GetKeyProvider(kpType, kpName string) (KeyProviderConfig, bool) { + for _, kp := range c.KeyProviderConfigs { + if kp.Type == kpType && kp.Name == kpName { + return kp, true + } + } + return KeyProviderConfig{}, false +} + // KeyProviderConfig describes the terraform.encryption.key_provider.* block you can use to declare a key provider for // encryption. The Body field will contain the remaining undeclared fields the key provider can consume. type KeyProviderConfig struct { diff --git a/internal/encryption/encryption.go b/internal/encryption/encryption.go index 08651296c8..43880dfb9d 100644 --- a/internal/encryption/encryption.go +++ b/internal/encryption/encryption.go @@ -7,6 +7,7 @@ package encryption import ( "github.com/hashicorp/hcl/v2" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption/config" "github.com/opentofu/opentofu/internal/encryption/registry" ) @@ -37,7 +38,7 @@ type encryption struct { } // New creates a new Encryption provider from the given configuration and registry. -func New(reg registry.Registry, cfg *config.EncryptionConfig) (Encryption, hcl.Diagnostics) { +func New(reg registry.Registry, cfg *config.EncryptionConfig, staticEval *configs.StaticEvaluator) (Encryption, hcl.Diagnostics) { if cfg == nil { return Disabled(), nil } @@ -52,21 +53,21 @@ func New(reg registry.Registry, cfg *config.EncryptionConfig) (Encryption, hcl.D var encDiags hcl.Diagnostics if cfg.State != nil { - enc.state, encDiags = newStateEncryption(enc, cfg.State.AsTargetConfig(), cfg.State.Enforced, "state") + enc.state, encDiags = newStateEncryption(enc, cfg.State.AsTargetConfig(), cfg.State.Enforced, "state", staticEval) diags = append(diags, encDiags...) } else { enc.state = StateEncryptionDisabled() } if cfg.Plan != nil { - enc.plan, encDiags = newPlanEncryption(enc, cfg.Plan.AsTargetConfig(), cfg.Plan.Enforced, "plan") + enc.plan, encDiags = newPlanEncryption(enc, cfg.Plan.AsTargetConfig(), cfg.Plan.Enforced, "plan", staticEval) diags = append(diags, encDiags...) } else { enc.plan = PlanEncryptionDisabled() } if cfg.Remote != nil && cfg.Remote.Default != nil { - enc.remoteDefault, encDiags = newStateEncryption(enc, cfg.Remote.Default, false, "remote.default") + enc.remoteDefault, encDiags = newStateEncryption(enc, cfg.Remote.Default, false, "remote.default", staticEval) diags = append(diags, encDiags...) } else { enc.remoteDefault = StateEncryptionDisabled() @@ -76,7 +77,7 @@ func New(reg registry.Registry, cfg *config.EncryptionConfig) (Encryption, hcl.D for _, remoteTarget := range cfg.Remote.Targets { // TODO the addr here should be generated in one place. addr := "remote.remote_state_datasource." + remoteTarget.Name - enc.remotes[remoteTarget.Name], encDiags = newStateEncryption(enc, remoteTarget.AsTargetConfig(), false, addr) + enc.remotes[remoteTarget.Name], encDiags = newStateEncryption(enc, remoteTarget.AsTargetConfig(), false, addr, staticEval) diags = append(diags, encDiags...) } } diff --git a/internal/encryption/enctest/setup.go b/internal/encryption/enctest/setup.go index 22aed5f845..338233347a 100644 --- a/internal/encryption/enctest/setup.go +++ b/internal/encryption/enctest/setup.go @@ -9,6 +9,7 @@ package enctest import ( "github.com/hashicorp/hcl/v2" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/encryption/config" "github.com/opentofu/opentofu/internal/encryption/keyprovider/static" @@ -35,7 +36,9 @@ func EncryptionDirect(configData string) encryption.Encryption { handleDiags(diags) - enc, diags := encryption.New(reg, cfg) + staticEval := configs.NewStaticEvaluator(nil, configs.RootModuleCallForTesting()) + + enc, diags := encryption.New(reg, cfg, staticEval) handleDiags(diags) return enc diff --git a/internal/encryption/example_test.go b/internal/encryption/example_test.go index 4fed215939..fe73ec4118 100644 --- a/internal/encryption/example_test.go +++ b/internal/encryption/example_test.go @@ -9,6 +9,7 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/encryption/config" "github.com/opentofu/opentofu/internal/encryption/keyprovider/static" @@ -57,8 +58,11 @@ func Example() { // Merge the configurations cfg := config.MergeConfigs(cfgA, cfgB) + // Construct static evaluator to pass additional values into encryption configuration. + staticEval := configs.NewStaticEvaluator(nil, configs.RootModuleCallForTesting()) + // Construct the encryption object - enc, diags := encryption.New(reg, cfg) + enc, diags := encryption.New(reg, cfg, staticEval) handleDiags(diags) sfe := enc.State() diff --git a/internal/encryption/keyprovider.go b/internal/encryption/keyprovider.go index 31e06be330..e60cfaf72a 100644 --- a/internal/encryption/keyprovider.go +++ b/internal/encryption/keyprovider.go @@ -10,7 +10,10 @@ import ( "errors" "fmt" + "github.com/opentofu/opentofu/internal/addrs" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption/config" + "github.com/opentofu/opentofu/internal/lang" "github.com/hashicorp/hcl/v2" "github.com/opentofu/opentofu/internal/encryption/keyprovider" @@ -88,17 +91,17 @@ func (e *targetBuilder) setupKeyProvider(cfg config.KeyProviderConfig, stack []c keyProviderDescriptor, err := e.reg.GetKeyProviderDescriptor(id) if err != nil { if errors.Is(err, ®istry.KeyProviderNotFoundError{}) { - return hcl.Diagnostics{&hcl.Diagnostic{ + return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unknown key_provider type", Detail: fmt.Sprintf("Can not find %q", cfg.Type), - }} + }) } - return hcl.Diagnostics{&hcl.Diagnostic{ + return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Error fetching key_provider %q", cfg.Type), Detail: err.Error(), - }} + }) } // Now that we know we have the correct Descriptor, we can decode the configuration @@ -106,21 +109,21 @@ func (e *targetBuilder) setupKeyProvider(cfg config.KeyProviderConfig, stack []c keyProviderConfig := keyProviderDescriptor.ConfigStruct() // Locate all the dependencies - deps, diags := gohcl.VariablesInBody(cfg.Body, keyProviderConfig) + deps, varDiags := gohcl.VariablesInBody(cfg.Body, keyProviderConfig) + diags = append(diags, varDiags...) if diags.HasErrors() { return diags } - // Required Dependencies + // lang.References is going to fail parsing key_provider deps + // so we filter them out in nonKeyProviderDeps. + var nonKeyProviderDeps []hcl.Traversal + + // Setting up key providers from deps. for _, dep := range deps { // Key Provider references should be in the form key_provider.type.name if len(dep) != 3 { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid key_provider reference", - Detail: "Expected reference in form key_provider.type.name", - Subject: dep.SourceRange().Ptr(), - }) + nonKeyProviderDeps = append(nonKeyProviderDeps, dep) continue } @@ -130,23 +133,23 @@ func (e *targetBuilder) setupKeyProvider(cfg config.KeyProviderConfig, stack []c depName := (dep[2].(hcl.TraverseAttr)).Name if depRoot != "key_provider" { + nonKeyProviderDeps = append(nonKeyProviderDeps, dep) + continue + } + + kpc, ok := e.cfg.GetKeyProvider(depType, depName) + if !ok { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Invalid key_provider reference", - Detail: "Expected reference in form key_provider.type.name", + Summary: "Undefined Key Provider", + Detail: fmt.Sprintf("Key provider %s.%s is missing from the encryption configuration.", depType, depName), Subject: dep.SourceRange().Ptr(), }) continue } - for _, kpc := range e.cfg.KeyProviderConfigs { - // Find the key provider in the config - if kpc.Type == depType && kpc.Name == depName { - depDiags := e.setupKeyProvider(kpc, stack) - diags = append(diags, depDiags...) - break - } - } + depDiags := e.setupKeyProvider(kpc, stack) + diags = append(diags, depDiags...) } if diags.HasErrors() { // We should not continue now if we have any diagnostics that are errors @@ -156,8 +159,24 @@ func (e *targetBuilder) setupKeyProvider(cfg config.KeyProviderConfig, stack []c return diags } + refs, refDiags := lang.References(addrs.ParseRef, nonKeyProviderDeps) + diags = append(diags, refDiags.ToHCL()...) + if diags.HasErrors() { + return diags + } + + evalCtx, evalDiags := e.staticEval.EvalContextWithParent(e.ctx, configs.StaticIdentifier{ + Module: addrs.RootModule, + Subject: fmt.Sprintf("encryption.key_provider.%s.%s", cfg.Type, cfg.Name), + DeclRange: e.cfg.DeclRange, + }, refs) + diags = append(diags, evalDiags...) + if diags.HasErrors() { + return diags + } + // Initialize the Key Provider - decodeDiags := gohcl.DecodeBody(cfg.Body, e.ctx, keyProviderConfig) + decodeDiags := gohcl.DecodeBody(cfg.Body, evalCtx, keyProviderConfig) diags = append(diags, decodeDiags...) if diags.HasErrors() { return diags diff --git a/internal/encryption/keyprovider/static/example_test.go b/internal/encryption/keyprovider/static/example_test.go index b2cb384d0e..b50c5ae7e9 100644 --- a/internal/encryption/keyprovider/static/example_test.go +++ b/internal/encryption/keyprovider/static/example_test.go @@ -9,6 +9,7 @@ import ( "fmt" "strings" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/encryption/config" "github.com/opentofu/opentofu/internal/encryption/keyprovider/static" @@ -44,7 +45,9 @@ func Example() { panic(diags) } - enc, diags := encryption.New(registry, cfg) + staticEvaluator := configs.NewStaticEvaluator(nil, configs.RootModuleCallForTesting()) + + enc, diags := encryption.New(registry, cfg, staticEvaluator) if diags.HasErrors() { panic(diags) } diff --git a/internal/encryption/plan.go b/internal/encryption/plan.go index a1bb5366cf..7b89655233 100644 --- a/internal/encryption/plan.go +++ b/internal/encryption/plan.go @@ -9,6 +9,7 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption/config" ) @@ -49,8 +50,8 @@ type planEncryption struct { base *baseEncryption } -func newPlanEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string) (PlanEncryption, hcl.Diagnostics) { - base, diags := newBaseEncryption(enc, target, enforced, name) +func newPlanEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string, staticEval *configs.StaticEvaluator) (PlanEncryption, hcl.Diagnostics) { + base, diags := newBaseEncryption(enc, target, enforced, name, staticEval) return &planEncryption{base}, diags } diff --git a/internal/encryption/state.go b/internal/encryption/state.go index 91b1c69232..cd273b4de6 100644 --- a/internal/encryption/state.go +++ b/internal/encryption/state.go @@ -10,6 +10,7 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption/config" ) @@ -57,8 +58,8 @@ type stateEncryption struct { base *baseEncryption } -func newStateEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string) (StateEncryption, hcl.Diagnostics) { - base, diags := newBaseEncryption(enc, target, enforced, name) +func newStateEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string, staticEval *configs.StaticEvaluator) (StateEncryption, hcl.Diagnostics) { + base, diags := newBaseEncryption(enc, target, enforced, name, staticEval) return &stateEncryption{base}, diags } diff --git a/internal/encryption/targets.go b/internal/encryption/targets.go index 2194396415..f6ab03333d 100644 --- a/internal/encryption/targets.go +++ b/internal/encryption/targets.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption/config" "github.com/opentofu/opentofu/internal/encryption/keyprovider" "github.com/opentofu/opentofu/internal/encryption/method" @@ -31,6 +32,7 @@ type targetBuilder struct { keyValues map[string]map[string]cty.Value methodValues map[string]map[string]cty.Value methods map[method.Addr]method.Method + staticEval *configs.StaticEvaluator } func (base *baseEncryption) buildTargetMethods(meta map[keyprovider.Addr][]byte) ([]method.Method, hcl.Diagnostics) { @@ -40,6 +42,7 @@ func (base *baseEncryption) buildTargetMethods(meta map[keyprovider.Addr][]byte) cfg: base.enc.cfg, reg: base.enc.reg, + staticEval: base.staticEval, ctx: &hcl.EvalContext{ Variables: map[string]cty.Value{}, }, diff --git a/internal/encryption/targets_test.go b/internal/encryption/targets_test.go index 5bde5d43ec..f48050cc16 100644 --- a/internal/encryption/targets_test.go +++ b/internal/encryption/targets_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/hashicorp/hcl/v2" + "github.com/opentofu/opentofu/internal/addrs" + "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/encryption/config" "github.com/opentofu/opentofu/internal/encryption/keyprovider" "github.com/opentofu/opentofu/internal/encryption/keyprovider/static" @@ -18,6 +20,7 @@ import ( "github.com/opentofu/opentofu/internal/encryption/method/unencrypted" "github.com/opentofu/opentofu/internal/encryption/registry" "github.com/opentofu/opentofu/internal/encryption/registry/lockingencryptionregistry" + "github.com/zclconf/go-cty/cty" ) func TestBaseEncryption_buildTargetMethods(t *testing.T) { @@ -112,6 +115,36 @@ func TestBaseEncryption_buildTargetMethods(t *testing.T) { `, wantErr: ": Unencrypted method is forbidden; Unable to use `unencrypted` method since the `enforced` flag is used.", }, + "key-from-vars": { + rawConfig: ` + key_provider "static" "basic" { + key = var.key + } + method "aes_gcm" "example" { + keys = key_provider.static.basic + } + state { + method = method.aes_gcm.example + } + `, + wantMethods: []func(method.Method) bool{ + aesgcm.Is, + }, + }, + "undefined-key-from-vars": { + rawConfig: ` + key_provider "static" "basic" { + key = var.undefinedkey + } + method "aes_gcm" "example" { + keys = key_provider.static.basic + } + state { + method = method.aes_gcm.example + } + `, + wantErr: "Test Config Source:3,12-28: Undefined variable; Undefined variable var.undefinedkey", + }, } reg := lockingencryptionregistry.New() @@ -125,8 +158,26 @@ func TestBaseEncryption_buildTargetMethods(t *testing.T) { panic(err) } + mod := &configs.Module{ + Variables: map[string]*configs.Variable{ + "key": { + Name: "key", + Default: cty.StringVal("6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169"), + Type: cty.String, + }, + }, + } + + getVars := func(v *configs.Variable) (cty.Value, hcl.Diagnostics) { + return v.Default, nil + } + + modCall := configs.NewStaticModuleCall(addrs.RootModule, getVars, "", "") + + staticEval := configs.NewStaticEvaluator(mod, modCall) + for name, test := range tests { - t.Run(name, test.newTestRun(reg)) + t.Run(name, test.newTestRun(reg, staticEval)) } } @@ -136,7 +187,7 @@ type btmTestCase struct { wantErr string } -func (testCase btmTestCase) newTestRun(reg registry.Registry) func(t *testing.T) { +func (testCase btmTestCase) newTestRun(reg registry.Registry, staticEval *configs.StaticEvaluator) func(t *testing.T) { return func(t *testing.T) { t.Parallel() @@ -150,10 +201,11 @@ func (testCase btmTestCase) newTestRun(reg registry.Registry) func(t *testing.T) cfg: cfg, reg: reg, }, - target: cfg.State.AsTargetConfig(), - enforced: cfg.State.Enforced, - name: "test", - encMeta: make(map[keyprovider.Addr][]byte), + target: cfg.State.AsTargetConfig(), + enforced: cfg.State.Enforced, + name: "test", + encMeta: make(map[keyprovider.Addr][]byte), + staticEval: staticEval, } methods, diags := base.buildTargetMethods(base.encMeta) diff --git a/internal/lang/eval.go b/internal/lang/eval.go index bfe8b3258e..61f2e94216 100644 --- a/internal/lang/eval.go +++ b/internal/lang/eval.go @@ -260,7 +260,7 @@ func (s *Scope) EvalReference(ref *addrs.Reference, wantType cty.Type) (cty.Valu // We cheat a bit here and just build an EvalContext for our requested // reference with the "self" address overridden, and then pull the "self" // result out of it to return. - ctx, ctxDiags := s.evalContext([]*addrs.Reference{ref}, ref.Subject) + ctx, ctxDiags := s.evalContext(nil, []*addrs.Reference{ref}, ref.Subject) diags = diags.Append(ctxDiags) val := ctx.Variables["self"] if val == cty.NilVal { @@ -289,21 +289,34 @@ func (s *Scope) EvalReference(ref *addrs.Reference, wantType cty.Type) (cty.Valu // this type offers, but this is here for less common situations where the // caller will handle the evaluation calls itself. func (s *Scope) EvalContext(refs []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) { - return s.evalContext(refs, s.SelfAddr) + return s.evalContext(nil, refs, s.SelfAddr) } -func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceable) (*hcl.EvalContext, tfdiags.Diagnostics) { +// EvalContextWithParent is exactly the same as EvalContext except the resulting hcl.EvalContext +// will be derived from the given parental hcl.EvalContext. It will enable different hcl mechanisms +// to iteratively lookup target functions and variables in EvalContext's parent. +// See Traversal.TraverseAbs (hcl) or FunctionCallExpr.Value (hcl/hclsyntax) for more details. +func (s *Scope) EvalContextWithParent(p *hcl.EvalContext, refs []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) { + return s.evalContext(p, refs, s.SelfAddr) +} + +//nolint:funlen,gocyclo,cyclop // TODO: refactor this function to match linting requirements +func (s *Scope) evalContext(parent *hcl.EvalContext, refs []*addrs.Reference, selfAddr addrs.Referenceable) (*hcl.EvalContext, tfdiags.Diagnostics) { if s == nil { panic("attempt to construct EvalContext for nil Scope") } var diags tfdiags.Diagnostics + vals := make(map[string]cty.Value) funcs := make(map[string]function.Function) - ctx := &hcl.EvalContext{ - Variables: vals, - Functions: funcs, - } + + // Calling NewChild() on a nil parent will + // produce an EvalContext with no parent. + ctx := parent.NewChild() + ctx.Variables = vals + ctx.Functions = funcs + for name, fn := range s.Functions() { funcs[name] = fn } diff --git a/internal/lang/eval_test.go b/internal/lang/eval_test.go index 3b8ef7fca4..4b3409dd5b 100644 --- a/internal/lang/eval_test.go +++ b/internal/lang/eval_test.go @@ -8,6 +8,7 @@ package lang import ( "bytes" "encoding/json" + "reflect" "testing" "github.com/opentofu/opentofu/internal/addrs" @@ -19,6 +20,7 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" ctyjson "github.com/zclconf/go-cty/cty/json" ) @@ -421,6 +423,76 @@ func TestScopeEvalContext(t *testing.T) { } } +// TestScopeEvalContextWithParent tests if the resulting EvalCtx has correct parent. +func TestScopeEvalContextWithParent(t *testing.T) { + t.Run("with-parent", func(t *testing.T) { + barStr, barFunc := cty.StringVal("bar"), function.New(&function.Spec{ + Impl: func(_ []cty.Value, _ cty.Type) (cty.Value, error) { + return cty.NilVal, nil + }, + }) + + scope, parent := &Scope{}, &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "foo": barStr, + }, + Functions: map[string]function.Function{ + "foo": barFunc, + }, + } + + child, diags := scope.EvalContextWithParent(parent, nil) + if len(diags) != 0 { + t.Errorf("Unexpected diagnostics:") + for _, diag := range diags { + t.Errorf("- %s", diag) + } + return + } + + if child.Parent() == nil { + t.Fatalf("Child EvalCtx has no parent") + } + + if child.Parent() != parent { + t.Fatalf("Child EvalCtx has different parent:\n GOT:%v\nWANT:%v", child.Parent(), parent) + } + + if ln := len(child.Parent().Variables); ln != 1 { + t.Fatalf("EvalContextWithParent modified parent's variables: incorrent length: %d", ln) + } + + if v := child.Parent().Variables["foo"]; !v.RawEquals(barStr) { + t.Fatalf("EvalContextWithParent modified parent's variables:\n GOT:%v\nWANT:%v", v, barStr) + } + + if ln := len(child.Parent().Functions); ln != 1 { + t.Fatalf("EvalContextWithParent modified parent's functions: incorrent length: %d", ln) + } + + if v := child.Parent().Functions["foo"]; !reflect.DeepEqual(v, barFunc) { + t.Fatalf("EvalContextWithParent modified parent's functions:\n GOT:%v\nWANT:%v", v, barFunc) + } + }) + + t.Run("zero-parent", func(t *testing.T) { + scope := &Scope{} + + root, diags := scope.EvalContextWithParent(nil, nil) + if len(diags) != 0 { + t.Errorf("Unexpected diagnostics:") + for _, diag := range diags { + t.Errorf("- %s", diag) + } + return + } + + if root.Parent() != nil { + t.Fatalf("Resulting EvalCtx has unexpected parent: %v", root.Parent()) + } + }) +} + func TestScopeExpandEvalBlock(t *testing.T) { nestedObjTy := cty.Object(map[string]cty.Type{ "boop": cty.String, diff --git a/website/docs/language/state/examples/encryption/fallback_from_unencrypted.tf b/website/docs/language/state/examples/encryption/fallback_from_unencrypted.tf index 5910ec73bf..3f6e9e3661 100644 --- a/website/docs/language/state/examples/encryption/fallback_from_unencrypted.tf +++ b/website/docs/language/state/examples/encryption/fallback_from_unencrypted.tf @@ -1,3 +1,8 @@ +variable "passphrase" { + # Change passphrase to be at least 16 characters long: + default = "changeme!" +} + terraform { encryption { ## Step 1: Add the unencrypted method: @@ -5,8 +10,7 @@ terraform { ## Step 2: Add the desired key provider: key_provider "pbkdf2" "mykey" { - # Change this to be at least 16 characters long: - passphrase = "changeme!" + passphrase = var.passphrase } ## Step 3: Add the desired encryption method: diff --git a/website/docs/language/state/examples/encryption/sample.tf b/website/docs/language/state/examples/encryption/sample.tf index 3214577520..690879e4fa 100644 --- a/website/docs/language/state/examples/encryption/sample.tf +++ b/website/docs/language/state/examples/encryption/sample.tf @@ -1,9 +1,13 @@ +variable "passphrase" { + # Change passphrase to be at least 16 characters long: + default = "changeme!" +} + terraform { encryption { ## Step 1: Add the desired key provider: key_provider "pbkdf2" "mykey" { - # Change this to be at least 16 characters long: - passphrase = "changeme!" + passphrase = var.passphrase } ## Step 2: Set up your encryption method: method "aes_gcm" "new_method" {