Merge pull request #27549 from hashicorp/jbardin/provisioner-lifecycle

Provisioner lifecycle
This commit is contained in:
James Bardin 2021-01-20 10:50:18 -05:00 committed by GitHub
commit 0403d89083
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 115 additions and 569 deletions

View File

@ -181,6 +181,5 @@ func (p *provisioner) Stop() error {
}
func (p *provisioner) Close() error {
p.cancel()
return nil
}

View File

@ -181,7 +181,6 @@ func (p *provisioner) Stop() error {
}
func (p *provisioner) Close() error {
p.cancel()
return nil
}

View File

@ -116,7 +116,6 @@ func (p *provisioner) Stop() error {
}
func (p *provisioner) Close() error {
p.cancel()
return nil
}

View File

@ -0,0 +1,44 @@
package e2etest
import (
"strings"
"testing"
"github.com/hashicorp/terraform/e2e"
)
// TestProviderDevOverrides is a test that terraform can execute a 3rd party
// provisioner plugin.
func TestProvisioner(t *testing.T) {
t.Parallel()
// This test reaches out to releases.hashicorp.com to download the
// template and null providers, so it can only run if network access is
// allowed.
skipIfCannotAccessNetwork(t)
tf := e2e.NewBinary(terraformBin, "testdata/provisioner")
defer tf.Close()
//// INIT
_, stderr, err := tf.Run("init")
if err != nil {
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
}
//// PLAN
_, stderr, err = tf.Run("plan", "-out=tfplan")
if err != nil {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}
//// APPLY
stdout, stderr, err := tf.Run("apply", "tfplan")
if err != nil {
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
}
if !strings.Contains(stdout, "HelloProvisioner") {
t.Fatalf("missing provisioner output:\n%s", stdout)
}
}

View File

@ -0,0 +1,5 @@
resource "null_resource" "a" {
provisioner "local-exec" {
command = "echo HelloProvisioner"
}
}

View File

@ -9801,6 +9801,7 @@ func TestContext2Apply_plannedConnectionRefs(t *testing.T) {
return resp
}
provisionerFactory := func() (provisioners.Interface, error) {
pr := testProvisioner()
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
host := req.Connection.GetAttr("host")
@ -9810,13 +9811,15 @@ func TestContext2Apply_plannedConnectionRefs(t *testing.T) {
return resp
}
return pr, nil
}
Providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
}
provisioners := map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
"shell": provisionerFactory,
}
hook := &testHook{}
@ -12163,6 +12166,7 @@ output "out" {
func TestContext2Apply_provisionerSensitive(t *testing.T) {
m := testModule(t, "apply-provisioner-sensitive")
p := testProvider("aws")
pr := testProvisioner()
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
if req.Config.ContainsMarked() {
@ -12201,6 +12205,9 @@ func TestContext2Apply_provisionerSensitive(t *testing.T) {
t.Fatal("plan failed")
}
// "restart" provisioner
pr.CloseCalled = false
state, diags := ctx.Apply()
if diags.HasErrors() {
logDiagnostics(t, diags)

View File

@ -77,22 +77,16 @@ type EvalContext interface {
ProviderInput(addrs.AbsProviderConfig) map[string]cty.Value
SetProviderInput(addrs.AbsProviderConfig, map[string]cty.Value)
// InitProvisioner initializes the provisioner with the given name.
// It is an error to initialize the same provisioner more than once.
InitProvisioner(string) error
// Provisioner gets the provisioner instance with the given name (already
// initialized) or returns nil if the provisioner isn't initialized.
Provisioner(string) provisioners.Interface
// Provisioner gets the provisioner instance with the given name.
Provisioner(string) (provisioners.Interface, error)
// ProvisionerSchema retrieves the main configuration schema for a
// particular provisioner, which must have already been initialized with
// InitProvisioner.
ProvisionerSchema(string) *configschema.Block
// CloseProvisioner closes provisioner connections that aren't needed
// anymore.
CloseProvisioner(string) error
// CloseProvisioner closes all provisioner plugins.
CloseProvisioners() error
// EvaluateBlock takes the given raw configuration block and associated
// schema and evaluates it to produce a value of an object type that

View File

@ -228,48 +228,41 @@ func (ctx *BuiltinEvalContext) SetProviderInput(pc addrs.AbsProviderConfig, c ma
ctx.ProviderLock.Unlock()
}
func (ctx *BuiltinEvalContext) InitProvisioner(n string) error {
// If we already initialized, it is an error
if p := ctx.Provisioner(n); p != nil {
return fmt.Errorf("Provisioner '%s' already initialized", n)
}
// Warning: make sure to acquire these locks AFTER the call to Provisioner
// above, since it also acquires locks.
func (ctx *BuiltinEvalContext) Provisioner(n string) (provisioners.Interface, error) {
ctx.ProvisionerLock.Lock()
defer ctx.ProvisionerLock.Unlock()
p, err := ctx.Components.ResourceProvisioner(n)
p, ok := ctx.ProvisionerCache[n]
if !ok {
var err error
p, err = ctx.Components.ResourceProvisioner(n)
if err != nil {
return err
return nil, err
}
ctx.ProvisionerCache[n] = p
}
return nil
}
func (ctx *BuiltinEvalContext) Provisioner(n string) provisioners.Interface {
ctx.ProvisionerLock.Lock()
defer ctx.ProvisionerLock.Unlock()
return ctx.ProvisionerCache[n]
return p, nil
}
func (ctx *BuiltinEvalContext) ProvisionerSchema(n string) *configschema.Block {
return ctx.Schemas.ProvisionerConfig(n)
}
func (ctx *BuiltinEvalContext) CloseProvisioner(n string) error {
func (ctx *BuiltinEvalContext) CloseProvisioners() error {
var diags tfdiags.Diagnostics
ctx.ProvisionerLock.Lock()
defer ctx.ProvisionerLock.Unlock()
prov := ctx.ProvisionerCache[n]
if prov != nil {
return prov.Close()
for name, prov := range ctx.ProvisionerCache {
err := prov.Close()
if err != nil {
diags = diags.Append(fmt.Errorf("provisioner.Close %s: %s", name, err))
}
}
return nil
return diags.Err()
}
func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) {

View File

@ -63,11 +63,6 @@ type MockEvalContext struct {
ConfigureProviderConfig cty.Value
ConfigureProviderDiags tfdiags.Diagnostics
InitProvisionerCalled bool
InitProvisionerName string
InitProvisionerProvisioner provisioners.Interface
InitProvisionerError error
ProvisionerCalled bool
ProvisionerName string
ProvisionerProvisioner provisioners.Interface
@ -76,9 +71,7 @@ type MockEvalContext struct {
ProvisionerSchemaName string
ProvisionerSchemaSchema *configschema.Block
CloseProvisionerCalled bool
CloseProvisionerName string
CloseProvisionerProvisioner provisioners.Interface
CloseProvisionersCalled bool
EvaluateBlockCalled bool
EvaluateBlockBody hcl.Body
@ -208,16 +201,10 @@ func (c *MockEvalContext) SetProviderInput(addr addrs.AbsProviderConfig, vals ma
c.SetProviderInputValues = vals
}
func (c *MockEvalContext) InitProvisioner(n string) error {
c.InitProvisionerCalled = true
c.InitProvisionerName = n
return c.InitProvisionerError
}
func (c *MockEvalContext) Provisioner(n string) provisioners.Interface {
func (c *MockEvalContext) Provisioner(n string) (provisioners.Interface, error) {
c.ProvisionerCalled = true
c.ProvisionerName = n
return c.ProvisionerProvisioner
return c.ProvisionerProvisioner, nil
}
func (c *MockEvalContext) ProvisionerSchema(n string) *configschema.Block {
@ -226,9 +213,8 @@ func (c *MockEvalContext) ProvisionerSchema(n string) *configschema.Block {
return c.ProvisionerSchemaSchema
}
func (c *MockEvalContext) CloseProvisioner(n string) error {
c.CloseProvisionerCalled = true
c.CloseProvisionerName = n
func (c *MockEvalContext) CloseProvisioners() error {
c.CloseProvisionersCalled = true
return nil
}

View File

@ -108,10 +108,6 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
// Attach the configuration to any resources
&AttachResourceConfigTransformer{Config: b.Config},
// Provisioner-related transformations
&MissingProvisionerTransformer{Provisioners: b.Components.ResourceProvisioners()},
&ProvisionerTransformer{},
// add providers
TransformProviders(b.Components.ResourceProviders(), concreteProvider, b.Config),
@ -162,7 +158,6 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
// Close opened plugin connections
&CloseProviderTransformer{},
&CloseProvisionerTransformer{},
// close the root module
&CloseRootModuleTransformer{},

View File

@ -423,70 +423,6 @@ func TestApplyGraphBuilder_moduleDestroy(t *testing.T) {
)
}
func TestApplyGraphBuilder_provisioner(t *testing.T) {
changes := &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: mustResourceInstanceAddr("test_object.foo"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
},
},
},
}
b := &ApplyGraphBuilder{
Config: testModule(t, "graph-builder-apply-provisioner"),
Changes: changes,
Components: simpleMockComponentFactory(),
Schemas: simpleTestSchemas(),
}
g, err := b.Build(addrs.RootModuleInstance)
if err != nil {
t.Fatalf("err: %s", err)
}
testGraphContains(t, g, "provisioner.test")
testGraphHappensBefore(
t, g,
"provisioner.test",
"test_object.foo",
)
}
func TestApplyGraphBuilder_provisionerDestroy(t *testing.T) {
changes := &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: mustResourceInstanceAddr("test_object.foo"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Delete,
},
},
},
}
b := &ApplyGraphBuilder{
Config: testModule(t, "graph-builder-apply-provisioner"),
Changes: changes,
Components: simpleMockComponentFactory(),
Schemas: simpleTestSchemas(),
}
g, err := b.Build(addrs.RootModuleInstance)
if err != nil {
t.Fatalf("err: %s", err)
}
testGraphContains(t, g, "provisioner.test")
testGraphHappensBefore(
t, g,
"provisioner.test",
"test_object.foo (destroy)",
)
}
func TestApplyGraphBuilder_targetModule(t *testing.T) {
changes := &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
@ -784,7 +720,6 @@ module.child.test_object.create
module.child.test_object.create (expand)
module.child (expand)
provider["registry.terraform.io/hashicorp/test"]
provisioner.test
module.child.test_object.other
module.child.test_object.create
module.child.test_object.other (expand)
@ -795,13 +730,9 @@ provider["registry.terraform.io/hashicorp/test"]
provider["registry.terraform.io/hashicorp/test"] (close)
module.child.test_object.other
test_object.other
provisioner.test
provisioner.test (close)
module.child.test_object.create
root
meta.count-boundary (EachMode fixup)
provider["registry.terraform.io/hashicorp/test"] (close)
provisioner.test (close)
test_object.create
test_object.create (expand)
test_object.create (expand)

View File

@ -115,10 +115,6 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
// Attach the configuration to any resources
&AttachResourceConfigTransformer{Config: b.Config},
// Provisioner-related transformations
&MissingProvisionerTransformer{Provisioners: b.Components.ResourceProvisioners()},
&ProvisionerTransformer{},
// add providers
TransformProviders(b.Components.ResourceProviders(), b.ConcreteProvider, b.Config),

View File

@ -141,6 +141,8 @@ func (n *nodeExpandModule) Execute(ctx EvalContext, op walkOperation) (diags tfd
// Besides providing a root node for dependency ordering, nodeCloseModule also
// cleans up state after all the module nodes have been evaluated, removing
// empty resources and modules from the state.
// The root module instance also closes any remaining provisioner plugins which
// do not have a lifecycle controlled by individual graph nodes.
type nodeCloseModule struct {
Addr addrs.Module
}
@ -174,6 +176,12 @@ func (n *nodeCloseModule) Name() string {
}
func (n *nodeCloseModule) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
if n.Addr.IsRoot() {
// If this is the root module, we are cleaning up the walk, so close
// any running provisioners
diags = diags.Append(ctx.CloseProvisioners())
}
switch op {
case walkApply, walkDestroy:
state := ctx.State().Lock()

View File

@ -1,45 +0,0 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/tfdiags"
)
// NodeProvisioner represents a provider that has no associated operations.
// It registers all the common interfaces across operations for providers.
type NodeProvisioner struct {
NameValue string
PathValue addrs.ModuleInstance
}
var (
_ GraphNodeModuleInstance = (*NodeProvisioner)(nil)
_ GraphNodeProvisioner = (*NodeProvisioner)(nil)
_ GraphNodeExecutable = (*NodeProvisioner)(nil)
)
func (n *NodeProvisioner) Name() string {
result := fmt.Sprintf("provisioner.%s", n.NameValue)
if len(n.PathValue) > 0 {
result = fmt.Sprintf("%s.%s", n.PathValue.String(), result)
}
return result
}
// GraphNodeModuleInstance
func (n *NodeProvisioner) Path() addrs.ModuleInstance {
return n.PathValue
}
// GraphNodeProvisioner
func (n *NodeProvisioner) ProvisionerName() string {
return n.NameValue
}
// GraphNodeExecutable impl.
func (n *NodeProvisioner) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
return diags.Append(ctx.InitProvisioner(n.NameValue))
}

View File

@ -1638,7 +1638,12 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state
log.Printf("[TRACE] applyProvisioners: provisioning %s with %q", n.Addr, prov.Type)
// Get the provisioner
provisioner := ctx.Provisioner(prov.Type)
provisioner, err := ctx.Provisioner(prov.Type)
if err != nil {
diags = diags.Append(err)
return diags.Err()
}
schema := ctx.ProvisionerSchema(prov.Type)
config, configDiags := evalScope(ctx, prov.Config, self, schema)

View File

@ -67,7 +67,12 @@ func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (di
func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *configs.Provisioner, hasCount, hasForEach bool) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
provisioner := ctx.Provisioner(p.Type)
provisioner, err := ctx.Provisioner(p.Type)
if err != nil {
diags = diags.Append(err)
return diags
}
if provisioner == nil {
return diags.Append(fmt.Errorf("provisioner %s not initialized", p.Type))
}

View File

@ -170,8 +170,10 @@ func testProviderFuncFixed(rp providers.Interface) providers.Factory {
}
}
func testProvisionerFuncFixed(rp provisioners.Interface) provisioners.Factory {
func testProvisionerFuncFixed(rp *MockProvisioner) provisioners.Factory {
return func() (provisioners.Interface, error) {
// make sure this provisioner has has not been closed
rp.CloseCalled = false
return rp, nil
}
}

View File

@ -1,182 +1,8 @@
package terraform
import (
"fmt"
"log"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/tfdiags"
)
// GraphNodeProvisioner is an interface that nodes that can be a provisioner
// must implement. The ProvisionerName returned is the name of the provisioner
// they satisfy.
type GraphNodeProvisioner interface {
ProvisionerName() string
}
// GraphNodeCloseProvisioner is an interface that nodes that can be a close
// provisioner must implement. The CloseProvisionerName returned is the name
// of the provisioner they satisfy.
type GraphNodeCloseProvisioner interface {
CloseProvisionerName() string
}
// GraphNodeProvisionerConsumer is an interface that nodes that require
// a provisioner must implement. ProvisionedBy must return the names of the
// provisioners to use.
type GraphNodeProvisionerConsumer interface {
ProvisionedBy() []string
}
// ProvisionerTransformer is a GraphTransformer that maps resources to
// provisioners within the graph. This will error if there are any resources
// that don't map to proper resources.
type ProvisionerTransformer struct{}
func (t *ProvisionerTransformer) Transform(g *Graph) error {
// Go through the other nodes and match them to provisioners they need
var err error
m := provisionerVertexMap(g)
for _, v := range g.Vertices() {
if pv, ok := v.(GraphNodeProvisionerConsumer); ok {
for _, p := range pv.ProvisionedBy() {
if m[p] == nil {
err = multierror.Append(err, fmt.Errorf(
"%s: provisioner %s couldn't be found",
dag.VertexName(v), p))
continue
}
log.Printf("[TRACE] ProvisionerTransformer: %s is provisioned by %s (%q)", dag.VertexName(v), p, dag.VertexName(m[p]))
g.Connect(dag.BasicEdge(v, m[p]))
}
}
}
return err
}
// MissingProvisionerTransformer is a GraphTransformer that adds nodes
// for missing provisioners into the graph.
type MissingProvisionerTransformer struct {
// Provisioners is the list of provisioners we support.
Provisioners []string
}
func (t *MissingProvisionerTransformer) Transform(g *Graph) error {
// Create a set of our supported provisioners
supported := make(map[string]struct{}, len(t.Provisioners))
for _, v := range t.Provisioners {
supported[v] = struct{}{}
}
// Get the map of provisioners we already have in our graph
m := provisionerVertexMap(g)
// Go through all the provisioner consumers and make sure we add
// that provisioner if it is missing.
for _, v := range g.Vertices() {
pv, ok := v.(GraphNodeProvisionerConsumer)
if !ok {
continue
}
for _, p := range pv.ProvisionedBy() {
if _, ok := m[p]; ok {
// This provisioner already exists as a configure node
continue
}
if _, ok := supported[p]; !ok {
// If we don't support the provisioner type, we skip it.
// Validation later will catch this as an error.
continue
}
// Build the vertex
var newV dag.Vertex = &NodeProvisioner{
NameValue: p,
}
// Add the missing provisioner node to the graph
m[p] = g.Add(newV)
log.Printf("[TRACE] MissingProviderTransformer: added implicit provisioner %s, first implied by %s", p, dag.VertexName(v))
}
}
return nil
}
// CloseProvisionerTransformer is a GraphTransformer that adds nodes to the
// graph that will close open provisioner connections that aren't needed
// anymore. A provisioner connection is not needed anymore once all depended
// resources in the graph are evaluated.
type CloseProvisionerTransformer struct{}
func (t *CloseProvisionerTransformer) Transform(g *Graph) error {
m := closeProvisionerVertexMap(g)
for _, v := range g.Vertices() {
if pv, ok := v.(GraphNodeProvisionerConsumer); ok {
for _, p := range pv.ProvisionedBy() {
source := m[p]
if source == nil {
// Create a new graphNodeCloseProvisioner and add it to the graph
source = &graphNodeCloseProvisioner{ProvisionerNameValue: p}
g.Add(source)
// Make sure we also add the new graphNodeCloseProvisioner to the map
// so we don't create and add any duplicate graphNodeCloseProvisioners.
m[p] = source
}
g.Connect(dag.BasicEdge(source, v))
}
}
}
return nil
}
func provisionerVertexMap(g *Graph) map[string]dag.Vertex {
m := make(map[string]dag.Vertex)
for _, v := range g.Vertices() {
if pv, ok := v.(GraphNodeProvisioner); ok {
m[pv.ProvisionerName()] = v
}
}
return m
}
func closeProvisionerVertexMap(g *Graph) map[string]dag.Vertex {
m := make(map[string]dag.Vertex)
for _, v := range g.Vertices() {
if pv, ok := v.(GraphNodeCloseProvisioner); ok {
m[pv.CloseProvisionerName()] = v
}
}
return m
}
type graphNodeCloseProvisioner struct {
ProvisionerNameValue string
}
var _ GraphNodeExecutable = (*graphNodeCloseProvisioner)(nil)
func (n *graphNodeCloseProvisioner) Name() string {
return fmt.Sprintf("provisioner.%s (close)", n.ProvisionerNameValue)
}
// GraphNodeExecutable impl.
func (n *graphNodeCloseProvisioner) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
return diags.Append(ctx.CloseProvisioner(n.ProvisionerNameValue))
}
func (n *graphNodeCloseProvisioner) CloseProvisionerName() string {
return n.ProvisionerNameValue
}

View File

@ -1,203 +0,0 @@
package terraform
import (
"strings"
"testing"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/states"
)
func TestMissingProvisionerTransformer(t *testing.T) {
mod := testModule(t, "transform-provisioner-basic")
g := Graph{Path: addrs.RootModuleInstance}
{
tf := &ConfigTransformer{Config: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &AttachResourceConfigTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &MissingProvisionerTransformer{Provisioners: []string{"shell"}}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &ProvisionerTransformer{}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformMissingProvisionerBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestMissingProvisionerTransformer_module(t *testing.T) {
mod := testModule(t, "transform-provisioner-module")
g := Graph{Path: addrs.RootModuleInstance}
{
concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex {
return a
}
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsFlat: map[string]string{
"id": "foo",
},
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)),
&states.ResourceInstanceObjectSrc{
AttrsFlat: map[string]string{
"id": "foo",
},
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
})
tf := &StateTransformer{
ConcreteCurrent: concreteResource,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
t.Logf("graph after StateTransformer:\n%s", g.StringWithNodeTypes())
}
{
transform := &AttachResourceConfigTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &MissingProvisionerTransformer{Provisioners: []string{"shell"}}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
t.Logf("graph after MissingProvisionerTransformer:\n%s", g.StringWithNodeTypes())
}
{
transform := &ProvisionerTransformer{}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
t.Logf("graph after ProvisionerTransformer:\n%s", g.StringWithNodeTypes())
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformMissingProvisionerModuleStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestCloseProvisionerTransformer(t *testing.T) {
mod := testModule(t, "transform-provisioner-basic")
g := Graph{Path: addrs.RootModuleInstance}
{
tf := &ConfigTransformer{Config: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &AttachResourceConfigTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &MissingProvisionerTransformer{Provisioners: []string{"shell"}}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &ProvisionerTransformer{}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &CloseProvisionerTransformer{}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformCloseProvisionerBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
const testTransformMissingProvisionerBasicStr = `
aws_instance.web
provisioner.shell
provisioner.shell
`
const testTransformMissingProvisionerModuleStr = `
aws_instance.foo
provisioner.shell
module.child.aws_instance.foo
provisioner.shell
provisioner.shell
`
const testTransformCloseProvisionerBasicStr = `
aws_instance.web
provisioner.shell
provisioner.shell
provisioner.shell (close)
aws_instance.web
`