Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package opentofu for openSUSE:Factory checked in at 2026-01-27 16:11:15 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/opentofu (Old) and /work/SRC/openSUSE:Factory/.opentofu.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "opentofu" Tue Jan 27 16:11:15 2026 rev:43 rq:1329325 version:1.11.4 Changes: -------- --- /work/SRC/openSUSE:Factory/opentofu/opentofu.changes 2026-01-14 16:23:45.494609473 +0100 +++ /work/SRC/openSUSE:Factory/.opentofu.new.1928/opentofu.changes 2026-01-27 16:11:57.729169153 +0100 @@ -1,0 +2,33 @@ +Thu Jan 22 06:29:13 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.11.4: + * SECURITY ADVISORIES: + - Previous releases in the v1.11 series could potentially take + an excessive amount of time processing a maliciously-crafted + .zip archive during either provider or module installation + during tofu init. (#3689) + * BREAKING CHANGES: + - Modules containing local provider configurations now also + reject the enabled argument, matching existing behavior for + count, for_each, and depends_on. (#3680) + - This was an oversight in the original design of the enabled + feature and was missed during the review process. Although + our goal is to not introduce breaking changes in patch + releases, in some cases it may be warranted. Anyone who has + used the enabled feature in this particular way will have + unintentionally introduced a foot-gun into their + infrastructure and should remedy it post-haste. + * BUG FIXES: + - In JSON syntax, the state encryption method configuration now + allows specifying keys using both normal expression syntax + and using template interpolation syntax. Previously only the + template interpolation syntax was allowed, which was + inconsistent with other parts of the encryption + configuration. (#3654) + - Providers are not configured anymore with DeferralAllowed + capability of OpenTofu since having that created unwanted + behaviour from some providers. (#3676) + - Resources containing write-only attributes now are rendered + consistently during planning. (#3667) + +------------------------------------------------------------------- Old: ---- opentofu-1.11.3.obscpio New: ---- opentofu-1.11.4.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ opentofu.spec ++++++ --- /var/tmp/diff_new_pack.QTVh3R/_old 2026-01-27 16:11:58.877217062 +0100 +++ /var/tmp/diff_new_pack.QTVh3R/_new 2026-01-27 16:11:58.881217229 +0100 @@ -19,7 +19,7 @@ %define executable_name tofu Name: opentofu -Version: 1.11.3 +Version: 1.11.4 Release: 0 Summary: Declaratively manage your cloud infrastructure License: MPL-2.0 @@ -29,7 +29,7 @@ Source1: vendor.tar.gz Source99: opentofu-rpmlintrc BuildRequires: bash-completion -BuildRequires: go1.25 >= 1.25.5 +BuildRequires: go1.25 >= 1.25.6 BuildRequires: golang-packaging # See: https://github.com/hashicorp/opentofu/issues/22807 ExcludeArch: %{ix86} %{arm} ++++++ _service ++++++ --- /var/tmp/diff_new_pack.QTVh3R/_old 2026-01-27 16:11:58.937219566 +0100 +++ /var/tmp/diff_new_pack.QTVh3R/_new 2026-01-27 16:11:58.953220234 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/opentofu/opentofu/</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v1.11.3</param> + <param name="revision">v1.11.4</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.QTVh3R/_old 2026-01-27 16:11:58.981221402 +0100 +++ /var/tmp/diff_new_pack.QTVh3R/_new 2026-01-27 16:11:58.985221569 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/opentofu/opentofu/</param> - <param name="changesrevision">f53d9e3a6f6e04a4f5f22b7c32c1ddc1272ba547</param></service></servicedata> + <param name="changesrevision">2a9b7ac61409f7f22bde65c192c5814eb4b75cdf</param></service></servicedata> (No newline at EOF) ++++++ opentofu-1.11.3.obscpio -> opentofu-1.11.4.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/.go-version new/opentofu-1.11.4/.go-version --- old/opentofu-1.11.3/.go-version 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/.go-version 2026-01-21 16:57:28.000000000 +0100 @@ -1 +1 @@ -1.25.5 +1.25.6 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/CHANGELOG.md new/opentofu-1.11.4/CHANGELOG.md --- old/opentofu-1.11.3/CHANGELOG.md 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/CHANGELOG.md 2026-01-21 16:57:28.000000000 +0100 @@ -1,6 +1,24 @@ The v1.11.x release series is supported until **August 1 2026**. -## 1.11.4 (Unreleased) +## 1.11.5 (Unreleased) + +## 1.11.4 + +SECURITY ADVISORIES: + +* Previous releases in the v1.11 series could potentially take an excessive amount of time processing a maliciously-crafted `.zip` archive during either provider or module installation during `tofu init`. ([#3689](https://github.com/opentofu/opentofu/pull/3689)) + +BREAKING CHANGES: + +* Modules containing local provider configurations now also reject the `enabled` argument, matching existing behavior for `count`, `for_each`, and `depends_on`. ([#3680](https://github.com/opentofu/opentofu/pull/3680)) + + This was an oversight in the original design of the enabled feature and was missed during the review process. Although our goal is to not introduce breaking changes in patch releases, in some cases it may be warranted. Anyone who has used the enabled feature in this particular way will have unintentionally introduced a foot-gun into their infrastructure and should remedy it post-haste. + +BUG FIXES: + +* In JSON syntax, the state encryption method configuration now allows specifying keys using both normal expression syntax and using template interpolation syntax. Previously only the template interpolation syntax was allowed, which was inconsistent with other parts of the encryption configuration. ([#3654](https://github.com/opentofu/opentofu/issues/3654)) +* Providers are not configured anymore with `DeferralAllowed` capability of OpenTofu since having that created unwanted behaviour from some providers. ([#3676](https://github.com/opentofu/opentofu/pull/3676)) +* Resources containing write-only attributes now are rendered consistently during planning. ([#3667](https://github.com/opentofu/opentofu/pull/3667)) ## 1.11.3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/go.mod new/opentofu-1.11.4/go.mod --- old/opentofu-1.11.3/go.mod 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/go.mod 2026-01-21 16:57:28.000000000 +0100 @@ -1,6 +1,6 @@ module github.com/opentofu/opentofu -go 1.25.5 +go 1.25.6 // At the time of adding this configuration, the new Go feature introduced here https://github.com/golang/go/issues/67061, // was having a good amount of issues linked to, affecting AWS Firewall, GCP various services and a lot more. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/command/jsonformat/differ/block.go new/opentofu-1.11.4/internal/command/jsonformat/differ/block.go --- old/opentofu-1.11.3/internal/command/jsonformat/differ/block.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/command/jsonformat/differ/block.go 2026-01-21 16:57:28.000000000 +0100 @@ -28,44 +28,40 @@ blockValue := change.AsMap() attributes := make(map[string]computed.Diff) - for key, attr := range block.Attributes { - childValue := blockValue.GetChild(key) - if !childValue.RelevantAttributes.MatchesPartial() { - // Mark non-relevant attributes as unchanged. - childValue = childValue.AsNoOp() + // In the first iteration we generate the diffs for all non write-only attributes + // and only collect the write-only attributes for a second run. + // This is necessary because [block.Attributes] is a map and since the order is not + // guarantee in a map we cannot reliably include all the write-only attributes + // in the rendered diff. Therefore, we generate the diffs for all non write-only attributes, + // which will generate the actual action of the resource, action that will decide if write-only + // attributes will be included in the rendered output or not. + var writeOnlyAttributes []string + for key, attr := range block.Attributes { + if attr.WriteOnly { + writeOnlyAttributes = append(writeOnlyAttributes, key) + continue } - - // Empty strings in blocks should be considered null for legacy reasons. - // The SDK doesn't support null strings yet, so we work around this now. - if before, ok := childValue.Before.(string); ok && len(before) == 0 { - childValue.Before = nil - } - if after, ok := childValue.After.(string); ok && len(after) == 0 { - childValue.After = nil - } - - // Always treat changes to blocks as implicit. - childValue.BeforeExplicit = false - childValue.AfterExplicit = false - - // Because we want to print also the write-only attributes, we need to pass in the parent block - // action instead of the child one. - // This is because the child action will always result in NoOp since for write-only attributes, the - // values returned will be null. - childChange := ComputeDiffForAttribute(childValue, attr, current) - if childChange.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil { - // This validation is specifically added for `tofu show`. - // Since "current" will be NoOp during rendering the output for `tofu show`, - // we need this validation to include the write-only attributes in the output. - if !attr.WriteOnly { - // Don't record nil values at all in blocks. - continue - } + attrChange := blockValue.GetChild(key) + childChange := diffChildAttribute(attrChange, attr, current) + if childChange == nil { + continue + } + current = collections.CompareActions(current, childChange.Action) + attributes[key] = *childChange + } + // In the second iteration, now that the action of the resource is decided, we process only the write-only + // attributes. If the [current] action is NoOp, then none of the write-only attributes will be included, + // otherwise, will include all the write-only attributes. + for _, key := range writeOnlyAttributes { + attr := block.Attributes[key] + attrChange := blockValue.GetChild(key) + childChange := diffChildAttribute(attrChange, attr, current) + if childChange == nil { + continue } - - attributes[key] = childChange current = collections.CompareActions(current, childChange.Action) + attributes[key] = *childChange } blocks := renderers.Blocks{ @@ -138,3 +134,43 @@ return computed.NewDiff(renderers.Block(attributes, blocks), current, change.ReplacePaths.Matches()) } + +// diffChildAttribute computes a new [compute.Diff] for the given attribute change and its schema. +// When a resource for which the diff is built contains also write-only attributes, we want to process first +// all the non write-only attributes to get the actual change action on that resource, action that will decide +// if the write-only attributes will or not be included in the rendered output. +func diffChildAttribute(attrChange structured.Change, attrSchema *jsonprovider.Attribute, currentAction plans.Action) *computed.Diff { + if !attrChange.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + attrChange = attrChange.AsNoOp() + } + + // Empty strings in blocks should be considered null for legacy reasons. + // The SDK doesn't support null strings yet, so we work around this now. + if before, ok := attrChange.Before.(string); ok && len(before) == 0 { + attrChange.Before = nil + } + if after, ok := attrChange.After.(string); ok && len(after) == 0 { + attrChange.After = nil + } + + // Always treat changes to blocks as implicit. + attrChange.BeforeExplicit = false + attrChange.AfterExplicit = false + + // Because we want to render also the write-only attributes, we need to pass in the parent block + // action instead of the child one. + // This is because the child action will always result in NoOp since for write-only attributes, the + // values returned will be null. + childChange := ComputeDiffForAttribute(attrChange, attrSchema, currentAction) + if childChange.Action == plans.NoOp && attrChange.Before == nil && attrChange.After == nil { + // This validation is specifically added for `tofu show`. + // Since "current" will be NoOp during rendering the output for `tofu show`, + // we need this validation to include the write-only attributes in the output. + if !attrSchema.WriteOnly { + // Don't record nil values at all in blocks. + return nil + } + } + return &childChange +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/command/jsonformat/differ/differ_test.go new/opentofu-1.11.4/internal/command/jsonformat/differ/differ_test.go --- old/opentofu-1.11.3/internal/command/jsonformat/differ/differ_test.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/command/jsonformat/differ/differ_test.go 2026-01-21 16:57:28.000000000 +0100 @@ -134,7 +134,7 @@ "write_only_attribute": renderers.ValidateWriteOnly(plans.Delete, false), }, nil, nil, nil, nil, plans.Delete, false), }, - "create_with_write_only_value": { + "create_with_only_write_only_value": { input: structured.Change{ Before: nil, After: map[string]any{}, @@ -166,6 +166,66 @@ "write_only_attribute": renderers.ValidateWriteOnly(plans.Create, false), }, nil, nil, nil, nil, plans.Create, false), }, + // Before the fix for https://github.com/opentofu/opentofu/issues/3640, this test was failing randomnly. + // That was because the write-only attributes diffs could have been generated with NoOp instead of having + // the final action of the resource. After the fix, the write-only attributes diffs are generated after + // generating the diffs for any non write-only attribute meaning that the action generated from regular + // attributes will be used for all the write-only attributes. + "write_only_not_updated_but_regular_updated": { + input: structured.Change{ + Before: map[string]any{ + "regular_attribute": "before value", + }, + After: map[string]any{ + "regular_attribute": "after value", + }, + BeforeSensitive: false, + AfterSensitive: false, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "regular_attribute": { + AttributeType: unmarshalType(t, cty.String), + }, + "write_only_attribute": { + AttributeType: unmarshalType(t, cty.String), + WriteOnly: true, + }, + "write_only_attribute2": { + AttributeType: unmarshalType(t, cty.String), + WriteOnly: true, + }, + "write_only_attribute3": { + AttributeType: unmarshalType(t, cty.String), + WriteOnly: true, + }, + "write_only_attribute4": { + AttributeType: unmarshalType(t, cty.String), + WriteOnly: true, + }, + }, + BlockTypes: map[string]*jsonprovider.BlockType{ + "nested_with_write_only": { + NestingMode: "single", + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "inner_write_only": { + AttributeType: unmarshalType(t, cty.String), + WriteOnly: true, + }, + }, + }, + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "regular_attribute": renderers.ValidatePrimitive("before value", "after value", plans.Update, false), + "write_only_attribute": renderers.ValidateWriteOnly(plans.Update, false), + "write_only_attribute2": renderers.ValidateWriteOnly(plans.Update, false), + "write_only_attribute3": renderers.ValidateWriteOnly(plans.Update, false), + "write_only_attribute4": renderers.ValidateWriteOnly(plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + }, "update_with_write_only_value": { input: structured.Change{ Before: map[string]any{ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/configs/configload/loader_load_test.go new/opentofu-1.11.4/internal/configs/configload/loader_load_test.go --- old/opentofu-1.11.3/internal/configs/configload/loader_load_test.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/configs/configload/loader_load_test.go 2026-01-21 16:57:28.000000000 +0100 @@ -204,7 +204,7 @@ if !diags.HasErrors() { t.Fatalf("loading succeeded; want an error") } - if got, want := diags.Error(), "Module is incompatible with count, for_each, and depends_on"; !strings.Contains(got, want) { + if got, want := diags.Error(), "Module is incompatible with count, for_each, enabled and depends_on"; !strings.Contains(got, want) { t.Errorf("missing expected error\nwant substring: %s\ngot: %s", want, got) } }) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/configs/provider_validation.go new/opentofu-1.11.4/internal/configs/provider_validation.go --- old/opentofu-1.11.3/internal/configs/provider_validation.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/configs/provider_validation.go 2026-01-21 16:57:28.000000000 +0100 @@ -292,7 +292,7 @@ for name, child := range cfg.Children { mc := mod.ModuleCalls[name] childNoProviderConfigRange := noProviderConfigRange - // if the module call has any of count, for_each or depends_on, + // if the module call has any of count, for_each, enabled or depends_on, // providers are prohibited from being configured in this module, or // any module beneath this module. switch { @@ -300,6 +300,8 @@ childNoProviderConfigRange = mc.Count.Range().Ptr() case mc.ForEach != nil: childNoProviderConfigRange = mc.ForEach.Range().Ptr() + case mc.Enabled != nil: + childNoProviderConfigRange = mc.Enabled.Range().Ptr() case mc.DependsOn != nil: if len(mc.DependsOn) > 0 { childNoProviderConfigRange = mc.DependsOn[0].SourceRange().Ptr() @@ -595,9 +597,9 @@ // updated yet) than of the called module. diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Module is incompatible with count, for_each, and depends_on", + Summary: "Module is incompatible with count, for_each, enabled and depends_on", Detail: fmt.Sprintf( - "The module at %s is a legacy module which contains its own local provider configurations, and so calls to it may not use the count, for_each, or depends_on arguments.\n\nIf you also control the module %q, consider updating this module to instead expect provider configurations to be passed by its caller.", + "The module at %s is a legacy module which contains its own local provider configurations, and so calls to it may not use the count, for_each, enabled or depends_on arguments.\n\nIf you also control the module %q, consider updating this module to instead expect provider configurations to be passed by its caller.", cfg.Path, cfg.SourceAddr, ), Subject: noProviderConfigRange, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/configs/provider_validation_test.go new/opentofu-1.11.4/internal/configs/provider_validation_test.go --- old/opentofu-1.11.3/internal/configs/provider_validation_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/opentofu-1.11.4/internal/configs/provider_validation_test.go 2026-01-21 16:57:28.000000000 +0100 @@ -0,0 +1,141 @@ +// 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 ( + "strings" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/opentofu/opentofu/internal/addrs" +) + +func TestValidateProviderConfigs_WithMetaArguments(t *testing.T) { + tests := []struct { + name string + moduleCall *ModuleCall + childHasProviderConfig bool + wantError bool + }{ + { + name: "count", + moduleCall: &ModuleCall{ + Name: "child", + Count: &hclsyntax.LiteralValueExpr{}, + }, + childHasProviderConfig: true, + wantError: true, + }, + { + name: "for_each", + moduleCall: &ModuleCall{ + Name: "child", + ForEach: &hclsyntax.LiteralValueExpr{}, + }, + childHasProviderConfig: true, + wantError: true, + }, + { + name: "depends_on", + moduleCall: &ModuleCall{ + Name: "child", + DependsOn: []hcl.Traversal{{}}, + }, + childHasProviderConfig: true, + wantError: true, + }, + { + name: "enabled", + moduleCall: &ModuleCall{ + Name: "child", + Enabled: &hclsyntax.LiteralValueExpr{}, + }, + childHasProviderConfig: true, + wantError: true, + }, + { + name: "no meta-arguments", + moduleCall: &ModuleCall{ + Name: "child", + }, + childHasProviderConfig: true, + wantError: false, + }, + { + name: "count without child provider config", + moduleCall: &ModuleCall{ + Name: "child", + Count: &hclsyntax.LiteralValueExpr{}, + }, + childHasProviderConfig: false, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + childModule := &Module{ + ProviderConfigs: map[string]*Provider{}, + } + + if tt.childHasProviderConfig { + childModule.ProviderConfigs["aws"] = &Provider{ + Name: "aws", + Config: &hclsyntax.Body{ + Attributes: hclsyntax.Attributes{ + "region": &hclsyntax.Attribute{Name: "region"}, + }, + }, + } + } + + childCfg := &Config{ + Path: addrs.Module{"child"}, + Module: childModule, + SourceAddr: addrs.ModuleSourceLocal("./child"), + Children: map[string]*Config{}, + } + + parentModule := &Module{ + ModuleCalls: map[string]*ModuleCall{ + "child": tt.moduleCall, + }, + } + + parentCfg := &Config{ + Path: addrs.RootModule, + Module: parentModule, + Children: map[string]*Config{"child": childCfg}, + } + parentCfg.Root = parentCfg + childCfg.Root = parentCfg + childCfg.Parent = parentCfg + + diags := validateProviderConfigs(nil, parentCfg, nil) + + var foundError bool + for _, diag := range diags { + if diag.Severity == hcl.DiagError && + strings.Contains(diag.Summary, "Module is incompatible with count, for_each") { + foundError = true + if !strings.Contains(diag.Detail, "legacy module which contains its own local provider configurations") { + t.Errorf("expected error detail to mention 'legacy module', got: %s", diag.Detail) + } + break + } + } + + if tt.wantError && !foundError { + t.Errorf("expected error, but got none") + } + + if !tt.wantError && foundError { + t.Errorf("did not expect error, but got %s", diags) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/configs/state_encryption.go new/opentofu-1.11.4/internal/configs/state_encryption.go --- old/opentofu-1.11.3/internal/configs/state_encryption.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/configs/state_encryption.go 1970-01-01 01:00:00.000000000 +0100 @@ -1,16 +0,0 @@ -// 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 "github.com/hashicorp/hcl/v2" - -type StateEncryption struct { - Type string - Config hcl.Body - - TypeRange hcl.Range - DeclRange hcl.Range -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/configs/testdata/config-diagnostics/nested-provider/errors new/opentofu-1.11.4/internal/configs/testdata/config-diagnostics/nested-provider/errors --- old/opentofu-1.11.3/internal/configs/testdata/config-diagnostics/nested-provider/errors 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/configs/testdata/config-diagnostics/nested-provider/errors 2026-01-21 16:57:28.000000000 +0100 @@ -1 +1 @@ -nested-provider/root.tf:2,11-12: Module is incompatible with count, for_each, and depends_on; The module at module.child.module.child2 is a legacy module which contains its own local provider configurations, and so calls to it may not use the count, for_each, or depends_on arguments. +nested-provider/root.tf:2,11-12: Module is incompatible with count, for_each, enabled and depends_on; The module at module.child.module.child2 is a legacy module which contains its own local provider configurations, and so calls to it may not use the count, for_each, enabled or depends_on arguments. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/keyprovider/output.go new/opentofu-1.11.4/internal/encryption/keyprovider/output.go --- old/opentofu-1.11.3/internal/encryption/keyprovider/output.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/keyprovider/output.go 2026-01-21 16:57:28.000000000 +0100 @@ -5,7 +5,14 @@ package keyprovider -import "github.com/zclconf/go-cty/cty" +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) // Output is the standardized structure a key provider must return when providing a key. // It contains two keys because some key providers may prefer include random data (e.g. salt) @@ -15,6 +22,38 @@ DecryptionKey []byte `hcl:"decryption_key,optional" cty:"decryption_key" json:"decryption_key,omitempty" yaml:"decryption_key"` } +func DecodeOutput(val cty.Value, subject hcl.Range) (Output, hcl.Diagnostics) { + var out Output + if !val.CanIterateElements() { + return out, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "Expected key_provider value", + Detail: fmt.Sprintf("Expected a key_provider compatible value, found %s instead", val.Type().FriendlyName()), + Subject: &subject, + }} + } + + var diags hcl.Diagnostics + mapVal := val.AsValueMap() + if attr, ok := mapVal["encryption_key"]; ok { + decodeDiags := gohcl.DecodeExpression(&hclsyntax.LiteralValueExpr{Val: attr, SrcRange: subject}, nil, &out.EncryptionKey) + diags = diags.Extend(decodeDiags) + } else { + return out, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "Missing encryption_key value", + Detail: "An encryption_key value is required in the key_provider compatible object at this location", + Subject: &subject, + }} + } + + if attr, ok := mapVal["decryption_key"]; ok { + decodeDiags := gohcl.DecodeExpression(&hclsyntax.LiteralValueExpr{Val: attr, SrcRange: subject}, nil, &out.DecryptionKey) + diags = diags.Extend(decodeDiags) + } + return out, diags +} + // Cty turns the Output struct into a CTY value. func (o *Output) Cty() cty.Value { return cty.ObjectVal(map[string]cty.Value{ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/aesgcm/compliance_test.go new/opentofu-1.11.4/internal/encryption/method/aesgcm/compliance_test.go --- old/opentofu-1.11.3/internal/encryption/method/aesgcm/compliance_test.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/aesgcm/compliance_test.go 2026-01-21 16:57:28.000000000 +0100 @@ -180,16 +180,6 @@ Validate: nil, }, }, - ConfigStructTestCases: map[string]compliancetest.ConfigStructTestCase[*Config, *aesgcm]{ - "empty": { - Config: &Config{ - Keys: keyprovider.Output{}, - AAD: nil, - }, - ValidBuild: false, - Validate: nil, - }, - }, EncryptDecryptTestCase: compliancetest.EncryptDecryptTestCase[*Config, *aesgcm]{ ValidEncryptOnlyConfig: &Config{ Keys: keyprovider.Output{ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/aesgcm/config.go new/opentofu-1.11.4/internal/encryption/method/aesgcm/config.go --- old/opentofu-1.11.3/internal/encryption/method/aesgcm/config.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/aesgcm/config.go 2026-01-21 16:57:28.000000000 +0100 @@ -22,13 +22,13 @@ type Config struct { // Key is the encryption key for the AES-GCM encryption. It has to be 16, 24, or 32 bytes long for AES-128, 192, or // 256, respectively. - Keys keyprovider.Output `hcl:"keys" json:"keys" yaml:"keys"` + Keys keyprovider.Output // AAD is the Additional Authenticated Data that is authenticated, but not encrypted. In the Go implementation, this // data serves as a canary value against replay attacks. The AAD value on decryption must match this setting, // otherwise the decryption will fail. (Note: this is Go-specific and differs from the NIST SP 800-38D description // of the AAD.) - AAD []byte `hcl:"aad,optional" json:"aad,omitempty" yaml:"aad,omitempty"` + AAD []byte } // Build checks the validity of the configuration and returns a ready-to-use AES-GCM implementation. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/aesgcm/descriptor.go new/opentofu-1.11.4/internal/encryption/method/aesgcm/descriptor.go --- old/opentofu-1.11.3/internal/encryption/method/aesgcm/descriptor.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/aesgcm/descriptor.go 2026-01-21 16:57:28.000000000 +0100 @@ -6,37 +6,58 @@ package aesgcm import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/opentofu/opentofu/internal/encryption/keyprovider" "github.com/opentofu/opentofu/internal/encryption/method" ) -// Descriptor integrates the method.Descriptor and provides a TypedConfig for easier configuration. -type Descriptor interface { - method.Descriptor - - // TypedConfig returns a config typed for this method. - TypedConfig() *Config -} - // New creates a new descriptor for the AES-GCM encryption method, which requires a 32-byte key. -func New() Descriptor { +func New() method.Descriptor { return &descriptor{} } type descriptor struct { } -func (f *descriptor) TypedConfig() *Config { - return &Config{ - Keys: keyprovider.Output{}, - AAD: nil, - } -} - func (f *descriptor) ID() method.ID { return "aes_gcm" } -func (f *descriptor) ConfigStruct() method.Config { - return f.TypedConfig() +func (f *descriptor) DecodeConfig(methodCtx method.EvalContext, body hcl.Body) (method.Config, hcl.Diagnostics) { + var diags hcl.Diagnostics + methodCfg := &Config{} + + content, contentDiags := body.Content(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "keys", Required: true}, + {Name: "aad", Required: false}, + }, + }) + diags = diags.Extend(contentDiags) + if diags.HasErrors() { + return nil, diags + } + + keyExpr := content.Attributes["keys"].Expr + // keyExpr can either be raw data/references to raw data or a string reference to a key provider (JSON support) + keyVal, keyDiags := methodCtx.ValueForExpression(keyExpr) + diags = diags.Extend(keyDiags) + if diags.HasErrors() { + return nil, diags + } + + methodCfg.Keys, keyDiags = keyprovider.DecodeOutput(keyVal, keyExpr.Range()) + diags = diags.Extend(keyDiags) + + if attr, ok := content.Attributes["aad"]; ok { + attrVal, attrDiags := methodCtx.ValueForExpression(attr.Expr) + diags = diags.Extend(attrDiags) + + decodeDiags := gohcl.DecodeExpression(&hclsyntax.LiteralValueExpr{Val: attrVal, SrcRange: attr.Expr.Range()}, nil, &methodCfg.AAD) + diags = diags.Extend(decodeDiags) + } + + return methodCfg, diags } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/aesgcm/example_test.go new/opentofu-1.11.4/internal/encryption/method/aesgcm/example_test.go --- old/opentofu-1.11.3/internal/encryption/method/aesgcm/example_test.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/aesgcm/example_test.go 2026-01-21 16:57:28.000000000 +0100 @@ -6,62 +6,19 @@ package aesgcm_test import ( - "encoding/json" "fmt" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/opentofu/opentofu/internal/encryption/keyprovider" + "github.com/opentofu/opentofu/internal/encryption/method" "github.com/opentofu/opentofu/internal/encryption/method/aesgcm" + "github.com/zclconf/go-cty/cty" ) -func Example() { - descriptor := aesgcm.New() - - // Get the config struct. You can fill it manually by type-asserting it to aesgcm.Config, but you could also use - // it as JSON. - config := descriptor.ConfigStruct() - - if err := json.Unmarshal( - // Set up a randomly generated 32-byte key. In JSON, you can base64-encode the value. - []byte(`{ - "keys": { - "encryption_key": "Y29veTRhaXZ1NWFpeW9vMWlhMG9vR29vVGFlM1BhaTQ=", - "decryption_key": "Y29veTRhaXZ1NWFpeW9vMWlhMG9vR29vVGFlM1BhaTQ=" - } -}`), &config); err != nil { - panic(err) - } - - method, err := config.Build() - if err != nil { - panic(err) - } - - // Encrypt some data: - encrypted, err := method.Encrypt([]byte("Hello world!")) - if err != nil { - panic(err) - } - - // Now decrypt it: - decrypted, err := method.Decrypt(encrypted) - if err != nil { - panic(err) - } - - fmt.Printf("%s", decrypted) - // Output: Hello world! -} - func Example_config() { - // First, get the descriptor to make sure we always have the default values. - descriptor := aesgcm.New() - - // Obtain a modifiable, buildable config. Alternatively, you can also use ConfigStruct() method to obtain a - // struct you can fill with HCL or JSON tags. - config := descriptor.TypedConfig() + // Obtain a modifiable, buildable config. + config := aesgcm.Config{} // Set up an encryption key: config.Keys = keyprovider.Output{ @@ -91,54 +48,10 @@ // Output: Hello world! } -func Example_config_json() { - // First, get the descriptor to make sure we always have the default values. - descriptor := aesgcm.New() - - // Get an untyped config struct you can use for JSON unmarshalling: - config := descriptor.ConfigStruct() - - // Unmarshal JSON into the config struct: - if err := json.Unmarshal( - // Set up a randomly generated 32-byte key. In JSON, you can base64-encode the value. - []byte(`{ - "keys": { - "encryption_key": "Y29veTRhaXZ1NWFpeW9vMWlhMG9vR29vVGFlM1BhaTQ=", - "decryption_key": "Y29veTRhaXZ1NWFpeW9vMWlhMG9vR29vVGFlM1BhaTQ=" - } -}`), &config); err != nil { - panic(err) - } - - // Now you can build a method: - method, err := config.Build() - if err != nil { - panic(err) - } - - // Encrypt something: - encrypted, err := method.Encrypt([]byte("Hello world!")) - if err != nil { - panic(err) - } - - // Decrypt it: - decrypted, err := method.Decrypt(encrypted) - if err != nil { - panic(err) - } - - fmt.Printf("%s", decrypted) - // Output: Hello world! -} - func Example_config_hcl() { // First, get the descriptor to make sure we always have the default values. descriptor := aesgcm.New() - // Get an untyped config struct you can use for HCL unmarshalling: - config := descriptor.ConfigStruct() - // Unmarshal HCL code into the config struct. The input must be a list of bytes, so in a real world scenario // you may want to put in a hex-decoding function: rawHCLInput := `keys = { @@ -153,7 +66,12 @@ if diags.HasErrors() { panic(diags) } - if diags := gohcl.DecodeBody(file.Body, nil, config); diags.HasErrors() { + + methodCtx := method.EvalContext{ValueForExpression: func(expr hcl.Expression) (cty.Value, hcl.Diagnostics) { + return expr.Value(nil) + }} + config, diags := descriptor.DecodeConfig(methodCtx, file.Body) + if diags.HasErrors() { panic(diags) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/compliancetest/compliance.go new/opentofu-1.11.4/internal/encryption/method/compliancetest/compliance.go --- old/opentofu-1.11.3/internal/encryption/method/compliancetest/compliance.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/compliancetest/compliance.go 2026-01-21 16:57:28.000000000 +0100 @@ -11,10 +11,11 @@ "reflect" "testing" - "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2" "github.com/opentofu/opentofu/internal/encryption/compliancetest" "github.com/opentofu/opentofu/internal/encryption/config" "github.com/opentofu/opentofu/internal/encryption/method" + "github.com/zclconf/go-cty/cty" ) // ComplianceTest tests the functionality of a method to make sure it conforms to the expectations of the method @@ -32,9 +33,6 @@ // function. HCLParseTestCases map[string]HCLParseTestCase[TDescriptor, TConfig, TMethod] - // ConfigStructT validates that a certain config results or does not result in a valid Build() call. - ConfigStructTestCases map[string]ConfigStructTestCase[TConfig, TMethod] - // ProvideTestCase exercises the entire chain and generates two keys. EncryptDecryptTestCase EncryptDecryptTestCase[TConfig, TMethod] } @@ -46,9 +44,6 @@ t.Run("hcl", func(t *testing.T) { cfg.testHCL(t) }) - t.Run("config-struct", func(t *testing.T) { - cfg.testConfigStruct(t) - }) t.Run("encrypt-decrypt", func(t *testing.T) { cfg.EncryptDecryptTestCase.execute(t) }) @@ -100,20 +95,6 @@ }) } -func (cfg *TestConfiguration[TDescriptor, TConfig, TMethod]) testConfigStruct(t *testing.T) { - compliancetest.ConfigStruct[TConfig](t, cfg.Descriptor.ConfigStruct()) - - if cfg.ConfigStructTestCases == nil { - compliancetest.Fail(t, "Please provide a map to ConfigStructTestCases.") - } - - for name, tc := range cfg.ConfigStructTestCases { - t.Run(name, func(t *testing.T) { - tc.execute(t) - }) - } -} - // HCLParseTestCase contains a test case that parses HCL into a configuration. type HCLParseTestCase[TDescriptor method.Descriptor, TConfig method.Config, TMethod method.Method] struct { // HCL contains the code that should be parsed into the configuration structure. @@ -143,12 +124,11 @@ } } - configStruct := descriptor.ConfigStruct() - diags = gohcl.DecodeBody( - parsedConfig.MethodConfigs[0].Body, - nil, - configStruct, - ) + methodCtx := method.EvalContext{ValueForExpression: func(expr hcl.Expression) (cty.Value, hcl.Diagnostics) { + return expr.Value(nil) + }} + configStruct, diags := descriptor.DecodeConfig(methodCtx, parsedConfig.MethodConfigs[0].Body) + var m TMethod if h.ValidHCL { if diags.HasErrors() { @@ -177,22 +157,6 @@ } } -// ConfigStructTestCase validates that the config struct is behaving correctly when Build() is called. -type ConfigStructTestCase[TConfig method.Config, TMethod method.Method] struct { - Config TConfig - ValidBuild bool - Validate func(method TMethod) error -} - -func (m ConfigStructTestCase[TConfig, TMethod]) execute(t *testing.T) { - newMethod := buildConfigAndValidate[TMethod, TConfig](t, m.Config, m.ValidBuild) - if m.Validate != nil { - if err := m.Validate(newMethod); err != nil { - compliancetest.Fail(t, "method validation failed (%v)", err) - } - } -} - // EncryptDecryptTestCase handles a full encryption-decryption cycle. type EncryptDecryptTestCase[TConfig method.Config, TMethod method.Method] struct { // ValidEncryptOnlyConfig is a configuration that has no decryption key and can only be used for encryption. The diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/descriptor.go new/opentofu-1.11.4/internal/encryption/method/descriptor.go --- old/opentofu-1.11.3/internal/encryption/method/descriptor.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/descriptor.go 2026-01-21 16:57:28.000000000 +0100 @@ -5,16 +5,25 @@ package method +import ( + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" +) + // Descriptor contains the details on an encryption method and produces a configuration structure with default values. type Descriptor interface { // ID returns the unique identifier used when parsing HCL or JSON configs. ID() ID - // ConfigStruct creates a new configuration struct annotated with hcl tags. The Build() receiver on + // DecodeConfig creates a new configuration struct. The Build() receiver on // this struct must be able to build a Method from the configuration. // // Common errors: // - Returning a struct without a pointer // - Returning a non-struct - ConfigStruct() Config + DecodeConfig(methodCtx EvalContext, body hcl.Body) (Config, hcl.Diagnostics) +} + +type EvalContext struct { + ValueForExpression func(expr hcl.Expression) (cty.Value, hcl.Diagnostics) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/external/compliance_test.go new/opentofu-1.11.4/internal/encryption/method/external/compliance_test.go --- old/opentofu-1.11.3/internal/encryption/method/external/compliance_test.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/external/compliance_test.go 2026-01-21 16:57:28.000000000 +0100 @@ -74,13 +74,6 @@ }, }, }, - ConfigStructTestCases: map[string]compliancetest.ConfigStructTestCase[*Config, *command]{ - "empty": { - Config: &Config{}, - ValidBuild: false, - Validate: nil, - }, - }, EncryptDecryptTestCase: compliancetest.EncryptDecryptTestCase[*Config, *command]{ ValidEncryptOnlyConfig: &Config{ Keys: &keyprovider.Output{ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/external/config.go new/opentofu-1.11.4/internal/encryption/method/external/config.go --- old/opentofu-1.11.3/internal/encryption/method/external/config.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/external/config.go 2026-01-21 16:57:28.000000000 +0100 @@ -12,9 +12,9 @@ // Config is the configuration for the AES-GCM method. type Config struct { - Keys *keyprovider.Output `hcl:"keys,optional" json:"keys,omitempty" yaml:"keys"` - EncryptCommand []string `hcl:"encrypt_command" json:"encrypt_command" yaml:"encrypt_command"` - DecryptCommand []string `hcl:"decrypt_command" json:"decrypt_command" yaml:"decrypt_command"` + Keys *keyprovider.Output + EncryptCommand []string + DecryptCommand []string } // Build checks the validity of the configuration and returns a ready-to-use AES-GCM implementation. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/external/descriptor.go new/opentofu-1.11.4/internal/encryption/method/external/descriptor.go --- old/opentofu-1.11.3/internal/encryption/method/external/descriptor.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/external/descriptor.go 2026-01-21 16:57:28.000000000 +0100 @@ -6,33 +6,79 @@ package external import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/opentofu/opentofu/internal/encryption/keyprovider" "github.com/opentofu/opentofu/internal/encryption/method" ) -// Descriptor integrates the method.Descriptor and provides a TypedConfig for easier configuration. -type Descriptor interface { - method.Descriptor - - // TypedConfig returns a config typed for this method. - TypedConfig() *Config -} - // New creates a new descriptor for the AES-GCM encryption method, which requires a 32-byte key. -func New() Descriptor { +func New() method.Descriptor { return &descriptor{} } type descriptor struct { } -func (f *descriptor) TypedConfig() *Config { - return &Config{} -} - -func (f *descriptor) ID() method.ID { +func (d *descriptor) ID() method.ID { return "external" } -func (f *descriptor) ConfigStruct() method.Config { - return f.TypedConfig() +func (d *descriptor) DecodeConfig(methodCtx method.EvalContext, body hcl.Body) (method.Config, hcl.Diagnostics) { + var diags hcl.Diagnostics + methodCfg := &Config{} + + content, contentDiags := body.Content(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "keys", Required: false}, + {Name: "encrypt_command", Required: true}, + {Name: "decrypt_command", Required: true}, + }, + }) + diags = diags.Extend(contentDiags) + if diags.HasErrors() { + return nil, diags + } + + if keyAttr, ok := content.Attributes["keys"]; ok { + keyExpr := keyAttr.Expr + // keyExpr can either be raw data/references to raw data or a string reference to a key provider (JSON support) + keyVal, keyDiags := methodCtx.ValueForExpression(keyExpr) + diags = diags.Extend(keyDiags) + if diags.HasErrors() { + return nil, diags + } + keys, decodeDiags := keyprovider.DecodeOutput(keyVal, keyExpr.Range()) + diags = diags.Extend(decodeDiags) + if diags.HasErrors() { + return nil, diags + } + methodCfg.Keys = &keys + } + + encryptAttr := content.Attributes["encrypt_command"] + encryptVal, valueDiags := methodCtx.ValueForExpression(encryptAttr.Expr) + diags = diags.Extend(valueDiags) + if diags.HasErrors() { + return nil, diags + } + + decodeEncryptCmdDiags := gohcl.DecodeExpression(&hclsyntax.LiteralValueExpr{Val: encryptVal, SrcRange: encryptAttr.Expr.Range()}, nil, &methodCfg.EncryptCommand) + diags = diags.Extend(decodeEncryptCmdDiags) + if diags.HasErrors() { + return nil, diags + } + + decryptAttr := content.Attributes["decrypt_command"] + decryptVal, valueDiags := methodCtx.ValueForExpression(decryptAttr.Expr) + diags = diags.Extend(valueDiags) + if diags.HasErrors() { + return nil, diags + } + + decodeDecryptCmdDiags := gohcl.DecodeExpression(&hclsyntax.LiteralValueExpr{Val: decryptVal, SrcRange: decryptAttr.Expr.Range()}, nil, &methodCfg.DecryptCommand) + diags = diags.Extend(decodeDecryptCmdDiags) + + return methodCfg, diags } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/method/unencrypted/method.go new/opentofu-1.11.4/internal/encryption/method/unencrypted/method.go --- old/opentofu-1.11.3/internal/encryption/method/unencrypted/method.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/method/unencrypted/method.go 2026-01-21 16:57:28.000000000 +0100 @@ -6,6 +6,7 @@ package unencrypted import ( + "github.com/hashicorp/hcl/v2" "github.com/opentofu/opentofu/internal/encryption/config" "github.com/opentofu/opentofu/internal/encryption/method" ) @@ -19,8 +20,8 @@ func (f *descriptor) ID() method.ID { return "unencrypted" } -func (f *descriptor) ConfigStruct() method.Config { - return new(methodConfig) +func (f *descriptor) DecodeConfig(_ method.EvalContext, _ hcl.Body) (method.Config, hcl.Diagnostics) { + return new(methodConfig), nil } type methodConfig struct{} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/methods.go new/opentofu-1.11.4/internal/encryption/methods.go --- old/opentofu-1.11.3/internal/encryption/methods.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/methods.go 2026-01-21 16:57:28.000000000 +0100 @@ -9,14 +9,16 @@ "context" "errors" "fmt" + "strings" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" "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/method" "github.com/opentofu/opentofu/internal/encryption/registry" + "github.com/zclconf/go-cty/cty" ) // setupMethod sets up a single method for encryption. It returns a list of diagnostics if the method is invalid. @@ -43,37 +45,60 @@ }} } - methodConfig := encryptionMethod.ConfigStruct() + var methodCtx method.EvalContext + methodCtx = method.EvalContext{ValueForExpression: func(expr hcl.Expression) (cty.Value, hcl.Diagnostics) { + var diags hcl.Diagnostics + + deps := expr.Variables() + + kpConfigs, refs, kpDiags := filterKeyProviderReferences(enc, deps) + diags = diags.Extend(kpDiags) + if diags.HasErrors() { + return cty.NilVal, diags + } - deps, diags := gohcl.VariablesInBody(cfg.Body, methodConfig) - if diags.HasErrors() { - return nil, diags - } + hclCtx, kpDiags := setupKeyProviders(ctx, enc, kpConfigs, meta, reg, staticEval) + diags = diags.Extend(kpDiags) + if diags.HasErrors() { + return cty.NilVal, diags + } - kpConfigs, refs, kpDiags := filterKeyProviderReferences(enc, deps) - diags = diags.Extend(kpDiags) - if diags.HasErrors() { - return nil, diags - } + hclCtx, evalDiags := staticEval.EvalContextWithParent(ctx, hclCtx, configs.StaticIdentifier{ + Module: addrs.RootModule, + Subject: fmt.Sprintf("encryption.method.%s.%s", cfg.Type, cfg.Name), + DeclRange: enc.DeclRange, + }, refs) + diags = diags.Extend(evalDiags) + if diags.HasErrors() { + return cty.NilVal, diags + } - hclCtx, kpDiags := setupKeyProviders(ctx, enc, kpConfigs, meta, reg, staticEval) - diags = diags.Extend(kpDiags) - if diags.HasErrors() { - return nil, diags - } + val, valDiags := expr.Value(hclCtx) + diags = diags.Extend(valDiags) + if diags.HasErrors() { + return cty.NilVal, diags + } - hclCtx, evalDiags := staticEval.EvalContextWithParent(ctx, hclCtx, configs.StaticIdentifier{ - Module: addrs.RootModule, - Subject: fmt.Sprintf("encryption.method.%s.%s", cfg.Type, cfg.Name), - DeclRange: enc.DeclRange, - }, refs) - diags = diags.Extend(evalDiags) - if diags.HasErrors() { - return nil, diags - } + if val.Type() == cty.String { + // Try to be clever to see if it's a kp string that is actually a reference + // We might want a bool to opt-in to this functionality for JSON compat on specific fields + + str := val.AsString() + if strings.HasPrefix(str, "key_provider.") { + traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{}) + if !travDiags.HasErrors() { + // Call into the expr resolver again + val, valDiags = methodCtx.ValueForExpression(&hclsyntax.ScopeTraversalExpr{Traversal: traversal, SrcRange: expr.Range()}) + diags = diags.Extend(valDiags) + } + } + + } + + return val, diags + }} - methodDiags := gohcl.DecodeBody(cfg.Body, hclCtx, methodConfig) - diags = diags.Extend(methodDiags) + methodConfig, diags := encryptionMethod.DecodeConfig(methodCtx, cfg.Body) if diags.HasErrors() { return nil, diags } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/registry/compliancetest/compliance_method.go new/opentofu-1.11.4/internal/encryption/registry/compliancetest/compliance_method.go --- old/opentofu-1.11.3/internal/encryption/registry/compliancetest/compliance_method.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/registry/compliancetest/compliance_method.go 2026-01-21 16:57:28.000000000 +0100 @@ -10,6 +10,7 @@ "fmt" "testing" + "github.com/hashicorp/hcl/v2" "github.com/opentofu/opentofu/internal/encryption/method" "github.com/opentofu/opentofu/internal/encryption/registry" ) @@ -124,8 +125,8 @@ return t.id } -func (t testMethodDescriptor) ConfigStruct() method.Config { - return &testMethodConfig{} +func (t testMethodDescriptor) DecodeConfig(method.EvalContext, hcl.Body) (method.Config, hcl.Diagnostics) { + return &testMethodConfig{}, nil } type testMethodConfig struct { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/encryption/targets_test.go new/opentofu-1.11.4/internal/encryption/targets_test.go --- old/opentofu-1.11.3/internal/encryption/targets_test.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/encryption/targets_test.go 2026-01-21 16:57:28.000000000 +0100 @@ -361,17 +361,72 @@ } state { # Correct format but referencing a non-existent method - method = method.aes_gcm.nonexistent + method = method.aes_gcm.nonexistent # this is interpolated strictly to verify that things work as expected with interpolation of methods in the state block fallback { method = method.unencrypted.for_migration } } - `, +`, wantErr: `Test Config Source:12,15-41: Reference to undeclared encryption method; There is no method "aes_gcm" "nonexistent" block declared in the encryption block.`, wantMethods: []func(method.Method) bool{ unencrypted.Is, }, }, + // In https://github.com/opentofu/opentofu/issues/3482 was discovered that interpolation for + // target.method does not work, but only literal reference. + // This solves the inconsistencies between the way string expressions are evaluated for state.method vs method.keys. + "json-config-loads-state-method-interpolated": { + rawConfig: `{ + "key_provider": { + "static": { + "basic": { + "key": "6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169" + } + } + }, + "method": { + "aes_gcm": { + "example": { + "keys": "${key_provider.static.basic}" + } + } + }, + "state": { + "enforced": true, + "method": "method.aes_gcm.example" + } + } + `, + wantMethods: []func(method.Method) bool{ + aesgcm.Is, + }, + }, + "json-config-loads-state-method-not-interpolated": { + rawConfig: `{ + "key_provider": { + "static": { + "basic": { + "key": "6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169" + } + } + }, + "method": { + "aes_gcm": { + "example": { + "keys": "key_provider.static.basic" + } + } + }, + "state": { + "enforced": true, + "method": "method.aes_gcm.example" + } + } + `, + wantMethods: []func(method.Method) bool{ + aesgcm.Is, + }, + }, } reg := lockingencryptionregistry.New() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/internal/plugin6/grpc_provider.go new/opentofu-1.11.4/internal/plugin6/grpc_provider.go --- old/opentofu-1.11.3/internal/plugin6/grpc_provider.go 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/internal/plugin6/grpc_provider.go 2026-01-21 16:57:28.000000000 +0100 @@ -34,14 +34,6 @@ } var clientCapabilities = &proto6.ClientCapabilities{ - // DeferralAllowed tells the provider that it is allowed to respond to - // all of the various post-configuration requests (as described by the - // [providers.Configured] interface) by reporting that the request - // must be "deferred" because there isn't yet enough information to - // satisfy the request. Setting this means that we need to be prepared - // for there to be a "deferred" object in the response from various - // other provider RPC functions. - DeferralAllowed: true, // WriteOnlyAttributesAllowed indicates that the current system version // supports write-only attributes. // This enables the SDK to run specific validations and enable the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/version/VERSION new/opentofu-1.11.4/version/VERSION --- old/opentofu-1.11.3/version/VERSION 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/version/VERSION 2026-01-21 16:57:28.000000000 +0100 @@ -1 +1 @@ -1.11.3 +1.11.4 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.3/website/docs/language/syntax/json.mdx new/opentofu-1.11.4/website/docs/language/syntax/json.mdx --- old/opentofu-1.11.3/website/docs/language/syntax/json.mdx 2026-01-13 18:01:54.000000000 +0100 +++ new/opentofu-1.11.4/website/docs/language/syntax/json.mdx 2026-01-21 16:57:28.000000000 +0100 @@ -451,11 +451,12 @@ ### `terraform` blocks -Settings within `terraform` blocks are generally interpreted literally, except -for the `backend` block, which supports expressions. Other settings do not +Settings within `terraform` blocks are _generally_ interpreted literally, except +for the `backend` and `encryption` blocks, which support expressions. Other settings do not accept named object references or function calls and therefore do not treat string values as string templates. +#### `backend` block Since only one `backend` block is allowed per `terraform` block, the compact block mapping can be used to represent it, with a nested object containing a single property whose name represents the backend type. @@ -473,3 +474,145 @@ } } ``` + +#### `encryption` block +As visible in the [encryption documentation](../state/encryption.mdx), +the `encryption` block supports the following children blocks: +* `key_provider` +* `method` +* `state` +* `plan` +* `remote_state_data_sources` + +The `key_provider` and `method` can refer to other named blocks, like variables, while `state`, `plan` and +`remote_state_data_sources` can refer only to the other blocks inside the `encryption` block. + +A `key_provider` can refer to variables to be able to provide key information dynamically: + +```json +{ + "variable": { + "state_plan_passphrase": { + "type": "string", + "default": "myultrasecretpassphrase1!" + } + }, + "terraform": { + "encryption": { + "key_provider": { + "pbkdf2": { + "state_plan": { + "passphrase": "${var.state_plan_passphrase}" + } + } + } + } + } +} +``` + +A `method` block can refer to `key_provider` blocks by using static string references or regular terraform +interpolated expressions: +```json +{ + "terraform": { + "encryption": { + "key_provider": { + "pbkdf2": { + "state_plan": { + "passphrase": "myultrasecretpassphrase1!" + } + } + }, + "method": { + "aes_gcm": { + "my_key_for_state": { + "keys": "${key_provider.pbkdf2.state_plan}" + }, + "my_key_for_plan": { + "keys": "key_provider.pbkdf2.state_plan" + } + } + } + } + } +} +``` + +The `state`, `plan` and `remote_state_data_sources` can strictly refer to `method` blocks by using string +references. These are not expressions, are only references to the methods so the evaluation of these are not +performed in the same way expressions are evaluated in OpenTofu: + +```json +{ + "variable": { + "remote_state_passphrase": { + "type": "string", + "default": "mysecrettestpassword!" + }, + "state_plan_passphrase": { + "type": "string", + "default": "mysecrettestpassword2!" + } + }, + "terraform": { + "encryption": { + "key_provider": { + "pbkdf2": { + "remote_state": { + "passphrase": "remotestateultrasecretpassphrase!1" + }, + "state_plan": { + "passphrase": "stateplanultrasecretpassphrase!1" + } + } + }, + "method": { + "aes_gcm": { + "remote_state": { + "keys": "${key_provider.pbkdf2.remote_state_key}" + }, + "state_plan": { + "keys": "key_provider.pbkdf2.state_plan" + } + }, + "unencrypted": { + "unencrypted": {} + } + }, + "state": { + "method": "method.aes_gcm.state_plan", + "fallback": { + "method": "method.unencrypted.unencrypted" + } + }, + "plan": { + "method": "method.aes_gcm.state_plan", + "fallback": { + "method": "method.unencrypted.unencrypted" + } + }, + "remote_state_data_sources": { + "default": { + "method": "method.aes_gcm.remote_state" + }, + "remote_state_data_source": { + "another_state": { + "method": "method.aes_gcm.remote_state" + } + } + } + } + }, + "data": { + "terraform_remote_state": { + "another_state": { + "backend": "local", + "config": { + "path": "<path to an encrypted state>" + } + } + } + } +} +``` \ No newline at end of file ++++++ opentofu.obsinfo ++++++ --- /var/tmp/diff_new_pack.QTVh3R/_old 2026-01-27 16:12:02.177354795 +0100 +++ /var/tmp/diff_new_pack.QTVh3R/_new 2026-01-27 16:12:02.181354962 +0100 @@ -1,5 +1,5 @@ name: opentofu -version: 1.11.3 -mtime: 1768323714 -commit: f53d9e3a6f6e04a4f5f22b7c32c1ddc1272ba547 +version: 1.11.4 +mtime: 1769011048 +commit: 2a9b7ac61409f7f22bde65c192c5814eb4b75cdf ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/opentofu/vendor.tar.gz /work/SRC/openSUSE:Factory/.opentofu.new.1928/vendor.tar.gz differ: char 135, line 1
