blob: 097cc3832a3ef77a442e1993b82a75267b1a3f2a [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package refactoring
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/lang/ephemeral"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// crossTypeMover is a collection of data that is needed to calculate the
// cross-provider move state changes.
type crossTypeMover struct {
State *states.State
ProviderFactories map[addrs.Provider]providers.Factory
ProviderCache map[addrs.Provider]providers.Interface
}
// close ensures the cached providers are closed.
func (m *crossTypeMover) close() tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for _, provider := range m.ProviderCache {
diags = diags.Append(provider.Close())
}
return diags
}
func (m *crossTypeMover) getProvider(providers addrs.Provider) (providers.Interface, error) {
if provider, ok := m.ProviderCache[providers]; ok {
return provider, nil
}
if factory, ok := m.ProviderFactories[providers]; ok {
provider, err := factory()
if err != nil {
return nil, err
}
m.ProviderCache[providers] = provider
return provider, nil
}
// Then we don't have a provider in the cache - this represents a bug in
// Terraform since we should have already loaded all the providers in the
// configuration and the state.
return nil, fmt.Errorf("provider %s implementation not found; this is a bug in Terraform - please report it", providers)
}
// prepareCrossTypeMove checks if the provided MoveStatement is a cross-type
// move and if so, prepares the data needed to perform the move.
func (m *crossTypeMover) prepareCrossTypeMove(stmt *MoveStatement, source, target addrs.AbsResource) (*crossTypeMove, tfdiags.Diagnostics) {
if stmt.Provider == nil {
// This means the resource was not in the configuration at all, so we
// can't process this. It'll be picked up in the validation errors
// later.
return nil, nil
}
targetProviderAddr := stmt.Provider
sourceProviderAddr := m.State.Resource(source).ProviderConfig
if targetProviderAddr.Provider.Equals(sourceProviderAddr.Provider) {
if source.Resource.Type == target.Resource.Type {
// Then this is a move within the same provider and type, so we
// don't need to do anything special.
return nil, nil
}
}
var diags tfdiags.Diagnostics
var err error
targetProvider, err := m.getProvider(targetProviderAddr.Provider)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to initialise provider", err.Error()))
return nil, diags
}
targetSchema := targetProvider.GetProviderSchema()
diags = diags.Append(targetSchema.Diagnostics)
if targetSchema.Diagnostics.HasErrors() {
return nil, diags
}
if !targetSchema.ServerCapabilities.MoveResourceState {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported `moved` across resource types",
Detail: fmt.Sprintf("The provider %q does not support moved operations across resource types and providers.", targetProviderAddr.Provider),
Subject: stmt.DeclRange.ToHCL().Ptr(),
})
return nil, diags
}
targetResourceSchema := targetSchema.SchemaForResourceAddr(target.Resource)
return &crossTypeMove{
targetProvider: targetProvider,
targetProviderAddr: *targetProviderAddr,
targetResourceSchema: targetResourceSchema,
sourceProviderAddr: sourceProviderAddr,
}, diags
}
type crossTypeMove struct {
targetProvider providers.Interface
targetProviderAddr addrs.AbsProviderConfig
targetResourceSchema providers.Schema
sourceProviderAddr addrs.AbsProviderConfig
}
// applyCrossTypeMove will update the provider states.SyncState so that value
// at source is the result of the providers move operation. Note, that this
// doesn't actually move the resource in the state file, it just updates the
// value at source ready to be moved.
func (move *crossTypeMove) applyCrossTypeMove(stmt *MoveStatement, source, target addrs.AbsResourceInstance, state *states.SyncState) tfdiags.Diagnostics {
if move == nil {
// Then we don't need to do any data transformation.
return nil
}
var diags tfdiags.Diagnostics
var sourceIdentity []byte
src := state.ResourceInstance(source).Current
if src != nil {
sourceIdentity = src.IdentityJSON
}
// Build the request.
request := providers.MoveResourceStateRequest{
SourceProviderAddress: move.sourceProviderAddr.Provider.String(),
SourceTypeName: source.Resource.Resource.Type,
SourceSchemaVersion: int64(src.SchemaVersion),
SourceStateJSON: src.AttrsJSON,
SourcePrivate: src.Private,
TargetTypeName: target.Resource.Resource.Type,
SourceIdentity: sourceIdentity,
}
// Ask the provider to transform the value into the type expected by
// the new resource type.
resp := move.targetProvider.MoveResourceState(request)
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
return diags
}
// Providers are supposed to return null values for all write-only attributes
writeOnlyDiags := ephemeral.ValidateWriteOnlyAttributes(
"Provider returned invalid value",
func(path cty.Path) string {
return fmt.Sprintf(
"The provider %q returned a value for the write-only attribute \"%s%s\" during an across type move operation to %s. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.",
move.targetProviderAddr, target, tfdiags.FormatCtyPath(path), target,
)
},
resp.TargetState,
move.targetResourceSchema.Body,
)
diags = diags.Append(writeOnlyDiags)
if writeOnlyDiags.HasErrors() {
return diags
}
if !resp.TargetIdentity.IsNull() {
// Identities can not contain unknown values
if !resp.TargetIdentity.IsWhollyKnown() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Provider produced invalid identity",
fmt.Sprintf(
"Provider %q planned an identity with unknown values for the move from %s to %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
move.targetProviderAddr, source, target,
),
))
}
// Identities can not contain marks
if _, marks := resp.TargetIdentity.UnmarkDeep(); len(marks) > 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Provider produced invalid identity",
fmt.Sprintf(
"Provider %q planned an identity with marks for the move from %s to %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
move.targetProviderAddr, source, target,
),
))
}
}
if resp.TargetState == cty.NilVal {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider returned invalid value",
Detail: fmt.Sprintf("The provider returned an invalid value during an across type move operation to %s. This is a bug in the relevant provider; Please report it.", target),
Subject: stmt.DeclRange.ToHCL().Ptr(),
})
return diags
}
if !resp.TargetState.IsWhollyKnown() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider returned invalid value",
Detail: fmt.Sprintf("The provider %s returned an invalid value during an across type move operation: The returned state contains unknown values. This is a bug in the relevant provider; Please report it.",
move.targetProviderAddr),
Subject: stmt.DeclRange.ToHCL().Ptr(),
})
}
// Finally, we can update the source value with the new value.
newValue := &states.ResourceInstanceObject{
Value: resp.TargetState,
Private: resp.TargetPrivate,
Status: src.Status,
Dependencies: src.Dependencies,
CreateBeforeDestroy: src.CreateBeforeDestroy,
Identity: resp.TargetIdentity,
}
data, err := newValue.Encode(move.targetResourceSchema)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to encode source value",
Detail: fmt.Sprintf("Terraform failed to encode the value in state for %s: %v. This is a bug in Terraform; Please report it.", source.String(), err),
Subject: stmt.DeclRange.ToHCL().Ptr(),
})
return diags
}
state.SetResourceInstanceCurrent(source, data, move.targetProviderAddr)
return diags
}