mirror of
https://github.com/opentofu/opentofu.git
synced 2026-01-11 20:06:30 +00:00
Handle ignoring computed fields in ignore_changes blocks for testing (#3646)
Some checks are pending
build / Build for linux_386 (push) Waiting to run
build / Build for openbsd_386 (push) Waiting to run
build / Build for windows_386 (push) Waiting to run
build / Build for freebsd_amd64 (push) Waiting to run
build / Build for solaris_amd64 (push) Waiting to run
build / Build for windows_amd64 (push) Waiting to run
build / Build for freebsd_386 (push) Waiting to run
build / End-to-end Tests for linux_386 (push) Waiting to run
build / End-to-end Tests for windows_386 (push) Waiting to run
build / End-to-end Tests for linux_amd64 (push) Waiting to run
Website checks / Test Installation Instructions (push) Blocked by required conditions
Website checks / List files changed for pull request (push) Waiting to run
Website checks / Build (push) Blocked by required conditions
build / Build for linux_amd64 (push) Waiting to run
build / Build for openbsd_amd64 (push) Waiting to run
build / Build for freebsd_arm (push) Waiting to run
build / Build for linux_arm (push) Waiting to run
build / Build for linux_arm64 (push) Waiting to run
build / Build for darwin_amd64 (push) Waiting to run
build / End-to-end Tests for darwin_amd64 (push) Waiting to run
build / End-to-end Tests for windows_amd64 (push) Waiting to run
build / Build for darwin_arm64 (push) Waiting to run
Quick Checks / List files changed for pull request (push) Waiting to run
Quick Checks / Unit tests for linux_386 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for windows_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm (push) Blocked by required conditions
Quick Checks / Unit tests for darwin_arm64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm64 (push) Blocked by required conditions
Quick Checks / Race Tests (push) Blocked by required conditions
Quick Checks / End-to-end Tests (push) Blocked by required conditions
Quick Checks / Code Consistency Checks (push) Blocked by required conditions
Quick Checks / License Checks (push) Waiting to run
Some checks are pending
build / Build for linux_386 (push) Waiting to run
build / Build for openbsd_386 (push) Waiting to run
build / Build for windows_386 (push) Waiting to run
build / Build for freebsd_amd64 (push) Waiting to run
build / Build for solaris_amd64 (push) Waiting to run
build / Build for windows_amd64 (push) Waiting to run
build / Build for freebsd_386 (push) Waiting to run
build / End-to-end Tests for linux_386 (push) Waiting to run
build / End-to-end Tests for windows_386 (push) Waiting to run
build / End-to-end Tests for linux_amd64 (push) Waiting to run
Website checks / Test Installation Instructions (push) Blocked by required conditions
Website checks / List files changed for pull request (push) Waiting to run
Website checks / Build (push) Blocked by required conditions
build / Build for linux_amd64 (push) Waiting to run
build / Build for openbsd_amd64 (push) Waiting to run
build / Build for freebsd_arm (push) Waiting to run
build / Build for linux_arm (push) Waiting to run
build / Build for linux_arm64 (push) Waiting to run
build / Build for darwin_amd64 (push) Waiting to run
build / End-to-end Tests for darwin_amd64 (push) Waiting to run
build / End-to-end Tests for windows_amd64 (push) Waiting to run
build / Build for darwin_arm64 (push) Waiting to run
Quick Checks / List files changed for pull request (push) Waiting to run
Quick Checks / Unit tests for linux_386 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for windows_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm (push) Blocked by required conditions
Quick Checks / Unit tests for darwin_arm64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm64 (push) Blocked by required conditions
Quick Checks / Race Tests (push) Blocked by required conditions
Quick Checks / End-to-end Tests (push) Blocked by required conditions
Quick Checks / Code Consistency Checks (push) Blocked by required conditions
Quick Checks / License Checks (push) Waiting to run
Signed-off-by: James Humphries <james@james-humphries.co.uk>
This commit is contained in:
parent
65ee51c736
commit
16f6b2d119
6 changed files with 213 additions and 2 deletions
|
|
@ -23,6 +23,7 @@ BUG FIXES:
|
|||
- `for_each` inside `dynamic` blocks can now call provider-defined functions. ([#3429](https://github.com/opentofu/opentofu/issues/3429))
|
||||
- In the unlikely event that text included in a diagnostic message includes C0 control characters (e.g. terminal escape sequences), OpenTofu will now replace them with printable characters to avoid the risk of inadvertently changing terminal state when stdout or stderr is a terminal. ([#3479](https://github.com/opentofu/opentofu/issues/3479))
|
||||
- Fixed `length(module.foo)` returning 0 for module instances without outputs, even when `count` or `for_each` is set. ([#3067](https://github.com/opentofu/opentofu/issues/3067))
|
||||
- Fixed `tofu test` with `mock_provider` failing during cleanup when `lifecycle { ignore_changes }` references a block. ([#3644](https://github.com/opentofu/opentofu/issues/3644))
|
||||
|
||||
## Previous Releases
|
||||
|
||||
|
|
|
|||
|
|
@ -80,3 +80,43 @@ func TestMocksAndOverrides(t *testing.T) {
|
|||
t.Errorf("output doesn't have expected success string:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMockProviderComputedBlockCleanup ensures we don't regress
|
||||
// a fix for this issue https://github.com/opentofu/opentofu/issues/3644
|
||||
//
|
||||
// The bug occurs when:
|
||||
// 1. A resource has lifecycle { ignore_changes = [block] } on a BLOCK (not a simple attribute)
|
||||
// 2. mock_provider is used
|
||||
// 3. Cleanup/destroy runs after apply
|
||||
// The cleanup fails with "Config value can not be specified for computed field"
|
||||
func TestMockProviderComputedBlockCleanup(t *testing.T) {
|
||||
// This test fetches providers from registry.
|
||||
skipIfCannotAccessNetwork(t)
|
||||
|
||||
tf := e2e.NewBinary(t, tofuBin, filepath.Join("testdata", "mock-computed-block-cleanup"))
|
||||
|
||||
stdout, stderr, err := tf.Run("init")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error on 'init': %v", err)
|
||||
}
|
||||
if stderr != "" {
|
||||
t.Errorf("unexpected stderr output on 'init':\n%s", stderr)
|
||||
}
|
||||
if stdout == "" {
|
||||
t.Errorf("expected some output on 'init', got nothing")
|
||||
}
|
||||
|
||||
stdout, stderr, err = tf.Run("test")
|
||||
if err != nil {
|
||||
if strings.Contains(stdout, "Config value can not be specified for computed field") {
|
||||
t.Errorf("Bug reproduced: mock provider fails with computed field error.\n"+
|
||||
"This is the bug from https://github.com/opentofu/opentofu/issues/3644\n"+
|
||||
"stdout:\n%s", stdout)
|
||||
return
|
||||
}
|
||||
t.Errorf("unexpected error on 'test': %v\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "1 passed, 0 failed") {
|
||||
t.Errorf("output doesn't have expected success string:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
internal/command/e2etest/testdata/mock-computed-block-cleanup/main.tf
vendored
Normal file
21
internal/command/e2etest/testdata/mock-computed-block-cleanup/main.tf
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Minimal reproducer for https://github.com/opentofu/opentofu/issues/3644
|
||||
# Bug requires ignore_changes on a block (not a simple attribute), here I use network_interface
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
vsphere = {
|
||||
source = "vmware/vsphere"
|
||||
version = "~> 2.14"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "vsphere_virtual_machine" "vm" {
|
||||
name = "x"
|
||||
resource_pool_id = "x"
|
||||
network_interface { network_id = "x" }
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [network_interface]
|
||||
}
|
||||
}
|
||||
5
internal/command/e2etest/testdata/mock-computed-block-cleanup/main.tftest.hcl
vendored
Normal file
5
internal/command/e2etest/testdata/mock-computed-block-cleanup/main.tftest.hcl
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Minimal reproducer for https://github.com/opentofu/opentofu/issues/3644
|
||||
|
||||
mock_provider "vsphere" {}
|
||||
|
||||
run "create" {}
|
||||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"hash/fnv"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/configs/hcl2shim"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
|
|
@ -68,14 +69,36 @@ func (p providerForTest) PlanResourceChange(_ context.Context, r providers.PlanR
|
|||
|
||||
resSchema, _ := p.schema.SchemaForResourceType(addrs.ManagedResourceMode, r.TypeName)
|
||||
|
||||
var resp providers.PlanResourceChangeResponse
|
||||
// Filter out computed-only attributes from the schema to avoid them being used incorrectly
|
||||
// later on. This resolves https://github.com/opentofu/opentofu/issues/3644
|
||||
filteredConfig := filterComputedOnlyAttributes(resSchema, r.Config)
|
||||
|
||||
var resp providers.PlanResourceChangeResponse
|
||||
resp.PlannedState, resp.Diagnostics = newMockValueComposer(r.TypeName).
|
||||
ComposeBySchema(resSchema, r.Config, p.overrideValues)
|
||||
ComposeBySchema(resSchema, filteredConfig, p.overrideValues)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// filterComputedOnlyAttributes returns a copy of value where all computed-only attributes
|
||||
// (i.e. computed and not optional) defined in resSchema are replaced with null values.
|
||||
func filterComputedOnlyAttributes(resSchema *configschema.Block, value cty.Value) cty.Value {
|
||||
if resSchema == nil || value.IsNull() || !value.IsKnown() {
|
||||
// Nothing to filter here, or we dont know how to filter
|
||||
// so we should move on
|
||||
return value
|
||||
}
|
||||
|
||||
ret, _ := cty.Transform(value, func(path cty.Path, v cty.Value) (cty.Value, error) {
|
||||
attr := resSchema.AttributeByPath(path)
|
||||
if attr != nil && attr.Computed && !attr.Optional {
|
||||
return cty.NullVal(v.Type()), nil
|
||||
}
|
||||
return v, nil
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p providerForTest) ApplyResourceChange(_ context.Context, r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
||||
return providers.ApplyResourceChangeResponse{
|
||||
NewState: r.PlannedState,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestProviderForTest_ReadResource(t *testing.T) {
|
||||
|
|
@ -34,3 +36,122 @@ func TestProviderForTest_ReadResource(t *testing.T) {
|
|||
t.Fatalf("expected prior state not found error but got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterComputedOnlyAttributes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema *configschema.Block
|
||||
value cty.Value
|
||||
expected cty.Value
|
||||
}{
|
||||
{
|
||||
name: "nil schema returns value unchanged",
|
||||
schema: nil,
|
||||
value: cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
||||
expected: cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
||||
},
|
||||
{
|
||||
name: "null value returns null",
|
||||
schema: &configschema.Block{},
|
||||
value: cty.NullVal(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||
expected: cty.NullVal(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||
},
|
||||
{
|
||||
name: "unknown value returns unknown",
|
||||
schema: &configschema.Block{},
|
||||
value: cty.UnknownVal(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||
expected: cty.UnknownVal(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||
},
|
||||
{
|
||||
name: "computed-only attribute is nulled out",
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"computed_only": {Computed: true, Optional: false, Type: cty.String},
|
||||
"normal": {Optional: true, Type: cty.String},
|
||||
},
|
||||
},
|
||||
value: cty.ObjectVal(map[string]cty.Value{
|
||||
"computed_only": cty.StringVal("should be nulled"),
|
||||
"normal": cty.StringVal("keep me"),
|
||||
}),
|
||||
expected: cty.ObjectVal(map[string]cty.Value{
|
||||
"computed_only": cty.NullVal(cty.String),
|
||||
"normal": cty.StringVal("keep me"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "optional+computed attribute is NOT nulled",
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"optional_computed": {Computed: true, Optional: true, Type: cty.String},
|
||||
},
|
||||
},
|
||||
value: cty.ObjectVal(map[string]cty.Value{
|
||||
"optional_computed": cty.StringVal("keep me"),
|
||||
}),
|
||||
expected: cty.ObjectVal(map[string]cty.Value{
|
||||
"optional_computed": cty.StringVal("keep me"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "required attribute is NOT nulled",
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"required": {Required: true, Type: cty.String},
|
||||
},
|
||||
},
|
||||
value: cty.ObjectVal(map[string]cty.Value{
|
||||
"required": cty.StringVal("keep me"),
|
||||
}),
|
||||
expected: cty.ObjectVal(map[string]cty.Value{
|
||||
"required": cty.StringVal("keep me"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "nested block with computed-only attribute is nulled",
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"name": {Optional: true, Type: cty.String},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"nested": {
|
||||
Nesting: configschema.NestingList,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Computed: true, Optional: false, Type: cty.String},
|
||||
"user_specified": {Optional: true, Type: cty.String},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
value: cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("test"),
|
||||
"nested": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("computed-id"),
|
||||
"user_specified": cty.StringVal("user-value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expected: cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("test"),
|
||||
"nested": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.NullVal(cty.String),
|
||||
"user_specified": cty.StringVal("user-value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filterComputedOnlyAttributes(tt.schema, tt.value)
|
||||
if !result.RawEquals(tt.expected) {
|
||||
t.Errorf("filterComputedOnlyAttributes() = %v, want %v", result.GoString(), tt.expected.GoString())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue