diff --git a/internal/configs/removed.go b/internal/configs/removed.go index 7ec39f0191..dadaee17ba 100644 --- a/internal/configs/removed.go +++ b/internal/configs/removed.go @@ -4,6 +4,8 @@ package configs import ( + "fmt" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/hcl/v2" @@ -19,6 +21,12 @@ type Removed struct { // from state. Defaults to true. Destroy bool + // Managed captures a number of metadata fields that are applicable only + // for managed resources, and not for other resource modes. + // + // "removed" blocks support only a subset of the fields in [ManagedResource]. + Managed *ManagedResource + DeclRange hcl.Range } @@ -31,6 +39,8 @@ func decodeRemovedBlock(block *hcl.Block) (*Removed, hcl.Diagnostics) { content, moreDiags := block.Body.Content(removedBlockSchema) diags = append(diags, moreDiags...) + var targetKind addrs.RemoveTargetKind + var resourceMode addrs.ResourceMode // only valid if targetKind is addrs.RemoveTargetResource if attr, exists := content.Attributes["from"]; exists { from, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr) diags = append(diags, traversalDiags...) @@ -38,11 +48,21 @@ func decodeRemovedBlock(block *hcl.Block) (*Removed, hcl.Diagnostics) { from, fromDiags := addrs.ParseRemoveTarget(from) diags = append(diags, fromDiags.ToHCL()...) removed.From = from + if removed.From != nil { + targetKind = removed.From.ObjectKind() + if targetKind == addrs.RemoveTargetResource { + resourceMode = removed.From.RelSubject.(addrs.ConfigResource).Resource.Mode + } + } } } removed.Destroy = true + if resourceMode == addrs.ManagedResourceMode { + removed.Managed = &ManagedResource{} + } + var seenConnection *hcl.Block for _, block := range content.Blocks { switch block.Type { case "lifecycle": @@ -53,6 +73,61 @@ func decodeRemovedBlock(block *hcl.Block) (*Removed, hcl.Diagnostics) { valDiags := gohcl.DecodeExpression(attr.Expr, nil, &removed.Destroy) diags = append(diags, valDiags...) } + + case "connection": + if removed.Managed == nil { + // target is not a managed resource, then + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid connection block", + Detail: "Provisioner connection configuration is valid only when a removed block targets a managed resource.", + Subject: &block.DefRange, + }) + continue + } + + if seenConnection != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate connection block", + Detail: fmt.Sprintf("This \"removed\" block already has a connection block at %s.", seenConnection.DefRange), + Subject: &block.DefRange, + }) + continue + } + seenConnection = block + + removed.Managed.Connection = &Connection{ + Config: block.Body, + DeclRange: block.DefRange, + } + + case "provisioner": + if removed.Managed == nil { + // target is not a managed resource, then + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provisioner block", + Detail: "Provisioners are valid only when a removed block targets a managed resource.", + Subject: &block.DefRange, + }) + continue + } + + pv, pvDiags := decodeProvisionerBlock(block) + diags = append(diags, pvDiags...) + if pv != nil { + removed.Managed.Provisioners = append(removed.Managed.Provisioners, pv) + + if pv.When != ProvisionerWhenDestroy { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provisioner block", + Detail: "Only destroy-time provisioners are valid in \"removed\" blocks. To declare a destroy-time provisioner, use:\n when = destroy", + Subject: &block.DefRange, + }) + } + } } } @@ -67,9 +142,9 @@ var removedBlockSchema = &hcl.BodySchema{ }, }, Blocks: []hcl.BlockHeaderSchema{ - { - Type: "lifecycle", - }, + {Type: "lifecycle"}, + {Type: "connection"}, + {Type: "provisioner", LabelNames: []string{"type"}}, }, } diff --git a/internal/configs/removed_test.go b/internal/configs/removed_test.go index 5ac1480f05..dc43225b8f 100644 --- a/internal/configs/removed_test.go +++ b/internal/configs/removed_test.go @@ -60,6 +60,7 @@ func TestRemovedBlock_decode(t *testing.T) { &Removed{ From: mustRemoveEndpointFromExpr(foo_expr), Destroy: true, + Managed: &ManagedResource{}, DeclRange: blockRange, }, ``, @@ -93,10 +94,155 @@ func TestRemovedBlock_decode(t *testing.T) { &Removed{ From: mustRemoveEndpointFromExpr(foo_expr), Destroy: false, + Managed: &ManagedResource{}, DeclRange: blockRange, }, ``, }, + "provisioner when = destroy": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "provisioner", + Labels: []string{"remote-exec"}, + LabelRanges: []hcl.Range{{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "when": { + Name: "when", + Expr: hcltest.MockExprTraversalSrc("destroy"), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(foo_expr), + Destroy: true, + Managed: &ManagedResource{ + Provisioners: []*Provisioner{ + { + Type: "remote-exec", + Config: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{}, + Blocks: hcl.Blocks{}, + }), + When: ProvisionerWhenDestroy, + OnFailure: ProvisionerOnFailureFail, + }, + }, + }, + DeclRange: blockRange, + }, + ``, + }, + "provisioner when = create": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "provisioner", + Labels: []string{"local-exec"}, + LabelRanges: []hcl.Range{{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "when": { + Name: "when", + Expr: hcltest.MockExprTraversalSrc("create"), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(foo_expr), + Destroy: true, + Managed: &ManagedResource{ + Provisioners: []*Provisioner{ + { + Type: "local-exec", + Config: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{}, + Blocks: hcl.Blocks{}, + }), + When: ProvisionerWhenCreate, + OnFailure: ProvisionerOnFailureFail, + }, + }, + }, + DeclRange: blockRange, + }, + `Invalid provisioner block`, + }, + "provisioner no when": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "connection", + Body: hcltest.MockBody(&hcl.BodyContent{}), + }, + &hcl.Block{ + Type: "provisioner", + Labels: []string{"local-exec"}, + LabelRanges: []hcl.Range{{}}, + Body: hcltest.MockBody(&hcl.BodyContent{}), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(foo_expr), + Destroy: true, + Managed: &ManagedResource{ + Connection: &Connection{ + Config: hcltest.MockBody(&hcl.BodyContent{}), + }, + Provisioners: []*Provisioner{ + { + Type: "local-exec", + Config: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{}, + Blocks: hcl.Blocks{}, + }), + When: ProvisionerWhenCreate, + OnFailure: ProvisionerOnFailureFail, + }, + }, + }, + DeclRange: blockRange, + }, + `Invalid provisioner block`, + }, "modules": { &hcl.Block{ Type: "removed", @@ -130,6 +276,67 @@ func TestRemovedBlock_decode(t *testing.T) { }, ``, }, + "provisioner for module": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: mod_foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "provisioner", + Labels: []string{"local-exec"}, + LabelRanges: []hcl.Range{{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "when": { + Name: "when", + Expr: hcltest.MockExprTraversalSrc("destroy"), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(mod_foo_expr), + Destroy: true, + DeclRange: blockRange, + }, + `Invalid provisioner block`, + }, + "connection for module": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: mod_foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "connection", + Body: hcltest.MockBody(&hcl.BodyContent{}), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(mod_foo_expr), + Destroy: true, + DeclRange: blockRange, + }, + `Invalid connection block`, + }, // KEM Unspecified behaviour "no lifecycle block": { &hcl.Block{ @@ -147,6 +354,7 @@ func TestRemovedBlock_decode(t *testing.T) { &Removed{ From: mustRemoveEndpointFromExpr(foo_expr), Destroy: true, + Managed: &ManagedResource{}, DeclRange: blockRange, }, ``,