From ec4e0cf0e2cefcca1d2ace792416c4e584c440a6 Mon Sep 17 00:00:00 2001 From: Ilia Gogotchuri Date: Thu, 6 Feb 2025 17:45:00 +0400 Subject: [PATCH] Migration of null resource to terraform data (#2481) Signed-off-by: Ilia Gogotchuri --- CHANGELOG.md | 1 + internal/builtin/providers/tf/provider.go | 7 ++- .../builtin/providers/tf/resource_data.go | 48 +++++++++++++++++ .../providers/tf/resource_data_test.go | 52 +++++++++++++++++++ website/docs/intro/whats-new.mdx | 6 +++ website/docs/language/resources/tf-data.mdx | 1 + 6 files changed, 113 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7289741375..08ce6d8207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ENHANCEMENTS: * State encryption now supports using external programs as key providers. Additionally, the PBKDF2 key provider now supports chaining via the `chain` parameter. ([#2023](https://github.com/opentofu/opentofu/pull/2023)) * The `element` function now accepts negative indices, which extends the existing "wrapping" model into the negative direction. In particular, choosing element `-1` selects the final element in the sequence. ([#2371](https://github.com/opentofu/opentofu/pull/2371)) * `moved` now supports moving between different types ([#2370](https://github.com/opentofu/opentofu/pull/2370)) +* `moved` block can now be used to migrate from the `null_resource` to the `terraform_data` resource. ([#2481](https://github.com/opentofu/opentofu/pull/2481)) BUG FIXES: diff --git a/internal/builtin/providers/tf/provider.go b/internal/builtin/providers/tf/provider.go index a841cd84d0..c8955d5157 100644 --- a/internal/builtin/providers/tf/provider.go +++ b/internal/builtin/providers/tf/provider.go @@ -169,11 +169,14 @@ func (p *Provider) ImportResourceState(req providers.ImportResourceStateRequest) panic("unimplemented - terraform_remote_state has no resources") } +// MoveResourceState is called when the state loader encounters an instance state +// that has been moved to a new type, and the state should be updated to reflect the change. +// This is used to move the old state to the new schema. func (p *Provider) MoveResourceState(r providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) { - panic("unimplmented") + return moveDataStoreResourceState(r) } -// ValidateResourceConfig is used to to validate the resource configuration values. +// ValidateResourceConfig is used to validate the resource configuration values. func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { return validateDataStoreResourceConfig(req) } diff --git a/internal/builtin/providers/tf/resource_data.go b/internal/builtin/providers/tf/resource_data.go index 92f2c45fda..aa244aa122 100644 --- a/internal/builtin/providers/tf/resource_data.go +++ b/internal/builtin/providers/tf/resource_data.go @@ -56,6 +56,54 @@ func upgradeDataStoreResourceState(req providers.UpgradeResourceStateRequest) (r return resp } +// nullResourceSchema returns a schema for a null_resource with relevant attributes for type migration. +func nullResourceSchema() providers.Schema { + return providers.Schema{ + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "triggers": {Type: cty.Map(cty.String), Optional: true}, + "id": {Type: cty.String, Computed: true}, + }, + }, + } + +} + +func moveDataStoreResourceState(req providers.MoveResourceStateRequest) providers.MoveResourceStateResponse { + var resp providers.MoveResourceStateResponse + if req.SourceTypeName != "null_resource" || req.TargetTypeName != "terraform_data" { + resp.Diagnostics = resp.Diagnostics.Append( + fmt.Errorf("unsupported move: %s -> %s; only move from null_resource to terraform_data is supported", + req.SourceTypeName, req.TargetTypeName)) + return resp + } + nullTy := nullResourceSchema().Block.ImpliedType() + oldState, err := ctyjson.Unmarshal(req.SourceStateJSON, nullTy) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + oldStateMap := oldState.AsValueMap() + newStateMap := map[string]cty.Value{} + + if trigger, ok := oldStateMap["triggers"]; ok && !trigger.IsNull() { + newStateMap["triggers_replace"] = cty.ObjectVal(trigger.AsValueMap()) + } + if id, ok := oldStateMap["id"]; ok && !id.IsNull() { + newStateMap["id"] = id + } + + currentSchema := dataStoreResourceSchema() + newState, err := currentSchema.Block.CoerceValue(cty.ObjectVal(newStateMap)) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resp.TargetState = newState + resp.TargetPrivate = req.SourcePrivate + return resp +} + func readDataStoreResourceState(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { resp.NewState = req.PriorState return resp diff --git a/internal/builtin/providers/tf/resource_data_test.go b/internal/builtin/providers/tf/resource_data_test.go index 8c01525bcb..ae255a1d0b 100644 --- a/internal/builtin/providers/tf/resource_data_test.go +++ b/internal/builtin/providers/tf/resource_data_test.go @@ -6,6 +6,7 @@ package tf import ( + "bytes" "strings" "testing" @@ -82,6 +83,57 @@ func TestManagedDataUpgradeState(t *testing.T) { } } +func TestManagedDataMovedState(t *testing.T) { + nullSchema := nullResourceSchema() + nullTy := nullSchema.Block.ImpliedType() + + state := cty.ObjectVal(map[string]cty.Value{ + "triggers": cty.MapVal(map[string]cty.Value{ + "examplekey": cty.StringVal("value"), + }), + "id": cty.StringVal("not-quite-unique"), + }) + + jsState, err := ctyjson.Marshal(state, nullTy) + if err != nil { + t.Fatal(err) + } + + // empty request should fail + req := providers.MoveResourceStateRequest{} + + resp := moveDataStoreResourceState(req) + if !resp.Diagnostics.HasErrors() { + t.Fatalf("expected error, got %#v", resp) + } + + // valid request + req = providers.MoveResourceStateRequest{ + TargetTypeName: "terraform_data", + SourceTypeName: "null_resource", + SourcePrivate: []byte("PRIVATE"), + SourceStateJSON: jsState, + } + + resp = moveDataStoreResourceState(req) + + expectedState := cty.ObjectVal(map[string]cty.Value{ + "triggers_replace": cty.ObjectVal(map[string]cty.Value{ + "examplekey": cty.StringVal("value"), + }), + "id": cty.StringVal("not-quite-unique"), + "input": cty.NullVal(cty.DynamicPseudoType), + "output": cty.NullVal(cty.DynamicPseudoType), + }) + if !resp.TargetState.RawEquals(expectedState) { + t.Errorf("prior state was:\n%#v\nmoved state is:\n%#v\n", expectedState, resp.TargetState) + } + + if !bytes.Equal(resp.TargetPrivate, req.SourcePrivate) { + t.Error("expected private data to be copied") + } + +} func TestManagedDataRead(t *testing.T) { req := providers.ReadResourceRequest{ TypeName: "terraform_data", diff --git a/website/docs/intro/whats-new.mdx b/website/docs/intro/whats-new.mdx index 059d80473a..236ea1d052 100644 --- a/website/docs/intro/whats-new.mdx +++ b/website/docs/intro/whats-new.mdx @@ -11,6 +11,7 @@ This page will run you through the most important changes in OpenTofu 1.10: - [New features](#new-features) - [New built-in functions](#new-built-in-functions) + - [Moved for different resource types](#moved-for-different-resource-types) - [Improvements to existing features](#improvements-to-existing-features) - [External programs as encryption key providers (experimental)](#external-programs-as-encryption-key-providers-experimental) - [Smaller improvements](#smaller-improvements) @@ -28,6 +29,11 @@ New builtin provider functions added ([#2306](https://github.com/opentofu/opento - `provider::terraform::encode_tfvars` - Encode an object into a string with the same format as a TFVars file. - `provider::terraform::encode_expr` - Encode an arbitrary expression into a string with valid OpenTofu syntax. +### Moved for different resource types + +- `moved` block can now be used to migrate between different types of resources ([docs](../language/modules/develop/refactoring.mdx#changing-a-resource-type)). +- Builtin provider now supports migration from `null_resource` to `terraform_data` resource. + ## Improvements to existing features ### External programs as encryption key providers (experimental) diff --git a/website/docs/language/resources/tf-data.mdx b/website/docs/language/resources/tf-data.mdx index d772c1b19f..c076706206 100644 --- a/website/docs/language/resources/tf-data.mdx +++ b/website/docs/language/resources/tf-data.mdx @@ -63,6 +63,7 @@ resource "terraform_data" "bootstrap" { } ``` +`moved` block can be used to migrate from the `null_resource` to the `terraform_data` resource. [Migration guide](https://search.opentofu.org/provider/hashicorp/null/latest/docs/guides/terraform-migration) ## Argument Reference