From 5e545ff427a31dfe5b60d91dab367c4a7e7e5a2b Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 29 Feb 2024 16:14:31 -0800 Subject: [PATCH] configs: Provisioners in "removed" blocks When the removed_provisioners experiment is active, removed blocks referring to managed resources are allowed to include "connection" and "provisioner" blocks, as long as all of the "provisioner" blocks specify when = destroy to indicate that they should execute as part of the resource's "destroy" action. This commit only deals with parsing the configuration. The logic to react to this during the apply phase will follow in later commits. --- internal/configs/removed.go | 81 +++++++++++- internal/configs/removed_test.go | 208 +++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+), 3 deletions(-) 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, }, ``,