github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/e2etest/unmanaged_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package e2etest
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"io/ioutil"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  	"testing"
    14  
    15  	"github.com/hashicorp/go-hclog"
    16  	"github.com/hashicorp/go-plugin"
    17  	"github.com/terramate-io/tf/e2e"
    18  	"github.com/terramate-io/tf/grpcwrap"
    19  	tfplugin5 "github.com/terramate-io/tf/plugin"
    20  	tfplugin "github.com/terramate-io/tf/plugin6"
    21  	simple5 "github.com/terramate-io/tf/provider-simple"
    22  	simple "github.com/terramate-io/tf/provider-simple-v6"
    23  	proto5 "github.com/terramate-io/tf/tfplugin5"
    24  	proto "github.com/terramate-io/tf/tfplugin6"
    25  )
    26  
    27  // The tests in this file are for the "unmanaged provider workflow", which
    28  // includes variants of the following sequence, with different details:
    29  // terraform init
    30  // terraform plan
    31  // terraform apply
    32  //
    33  // These tests are run against an in-process server, and checked to make sure
    34  // they're not trying to control the lifecycle of the binary. They are not
    35  // checked for correctness of the operations themselves.
    36  
    37  type reattachConfig struct {
    38  	Protocol        string
    39  	ProtocolVersion int
    40  	Pid             int
    41  	Test            bool
    42  	Addr            reattachConfigAddr
    43  }
    44  
    45  type reattachConfigAddr struct {
    46  	Network string
    47  	String  string
    48  }
    49  
    50  type providerServer struct {
    51  	sync.Mutex
    52  	proto.ProviderServer
    53  	planResourceChangeCalled  bool
    54  	applyResourceChangeCalled bool
    55  }
    56  
    57  func (p *providerServer) PlanResourceChange(ctx context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) {
    58  	p.Lock()
    59  	defer p.Unlock()
    60  
    61  	p.planResourceChangeCalled = true
    62  	return p.ProviderServer.PlanResourceChange(ctx, req)
    63  }
    64  
    65  func (p *providerServer) ApplyResourceChange(ctx context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) {
    66  	p.Lock()
    67  	defer p.Unlock()
    68  
    69  	p.applyResourceChangeCalled = true
    70  	return p.ProviderServer.ApplyResourceChange(ctx, req)
    71  }
    72  
    73  func (p *providerServer) PlanResourceChangeCalled() bool {
    74  	p.Lock()
    75  	defer p.Unlock()
    76  
    77  	return p.planResourceChangeCalled
    78  }
    79  func (p *providerServer) ResetPlanResourceChangeCalled() {
    80  	p.Lock()
    81  	defer p.Unlock()
    82  
    83  	p.planResourceChangeCalled = false
    84  }
    85  
    86  func (p *providerServer) ApplyResourceChangeCalled() bool {
    87  	p.Lock()
    88  	defer p.Unlock()
    89  
    90  	return p.applyResourceChangeCalled
    91  }
    92  func (p *providerServer) ResetApplyResourceChangeCalled() {
    93  	p.Lock()
    94  	defer p.Unlock()
    95  
    96  	p.applyResourceChangeCalled = false
    97  }
    98  
    99  type providerServer5 struct {
   100  	sync.Mutex
   101  	proto5.ProviderServer
   102  	planResourceChangeCalled  bool
   103  	applyResourceChangeCalled bool
   104  }
   105  
   106  func (p *providerServer5) PlanResourceChange(ctx context.Context, req *proto5.PlanResourceChange_Request) (*proto5.PlanResourceChange_Response, error) {
   107  	p.Lock()
   108  	defer p.Unlock()
   109  
   110  	p.planResourceChangeCalled = true
   111  	return p.ProviderServer.PlanResourceChange(ctx, req)
   112  }
   113  
   114  func (p *providerServer5) ApplyResourceChange(ctx context.Context, req *proto5.ApplyResourceChange_Request) (*proto5.ApplyResourceChange_Response, error) {
   115  	p.Lock()
   116  	defer p.Unlock()
   117  
   118  	p.applyResourceChangeCalled = true
   119  	return p.ProviderServer.ApplyResourceChange(ctx, req)
   120  }
   121  
   122  func (p *providerServer5) PlanResourceChangeCalled() bool {
   123  	p.Lock()
   124  	defer p.Unlock()
   125  
   126  	return p.planResourceChangeCalled
   127  }
   128  func (p *providerServer5) ResetPlanResourceChangeCalled() {
   129  	p.Lock()
   130  	defer p.Unlock()
   131  
   132  	p.planResourceChangeCalled = false
   133  }
   134  
   135  func (p *providerServer5) ApplyResourceChangeCalled() bool {
   136  	p.Lock()
   137  	defer p.Unlock()
   138  
   139  	return p.applyResourceChangeCalled
   140  }
   141  func (p *providerServer5) ResetApplyResourceChangeCalled() {
   142  	p.Lock()
   143  	defer p.Unlock()
   144  
   145  	p.applyResourceChangeCalled = false
   146  }
   147  
   148  func TestUnmanagedSeparatePlan(t *testing.T) {
   149  	t.Parallel()
   150  
   151  	fixturePath := filepath.Join("testdata", "test-provider")
   152  	tf := e2e.NewBinary(t, terraformBin, fixturePath)
   153  
   154  	reattachCh := make(chan *plugin.ReattachConfig)
   155  	closeCh := make(chan struct{})
   156  	provider := &providerServer{
   157  		ProviderServer: grpcwrap.Provider6(simple.Provider()),
   158  	}
   159  	ctx, cancel := context.WithCancel(context.Background())
   160  	defer cancel()
   161  	go plugin.Serve(&plugin.ServeConfig{
   162  		Logger: hclog.New(&hclog.LoggerOptions{
   163  			Name:   "plugintest",
   164  			Level:  hclog.Trace,
   165  			Output: ioutil.Discard,
   166  		}),
   167  		Test: &plugin.ServeTestConfig{
   168  			Context:          ctx,
   169  			ReattachConfigCh: reattachCh,
   170  			CloseCh:          closeCh,
   171  		},
   172  		GRPCServer: plugin.DefaultGRPCServer,
   173  		VersionedPlugins: map[int]plugin.PluginSet{
   174  			6: {
   175  				"provider": &tfplugin.GRPCProviderPlugin{
   176  					GRPCProvider: func() proto.ProviderServer {
   177  						return provider
   178  					},
   179  				},
   180  			},
   181  		},
   182  	})
   183  	config := <-reattachCh
   184  	if config == nil {
   185  		t.Fatalf("no reattach config received")
   186  	}
   187  	reattachStr, err := json.Marshal(map[string]reattachConfig{
   188  		"hashicorp/test": {
   189  			Protocol:        string(config.Protocol),
   190  			ProtocolVersion: 6,
   191  			Pid:             config.Pid,
   192  			Test:            true,
   193  			Addr: reattachConfigAddr{
   194  				Network: config.Addr.Network(),
   195  				String:  config.Addr.String(),
   196  			},
   197  		},
   198  	})
   199  	if err != nil {
   200  		t.Fatal(err)
   201  	}
   202  
   203  	tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr))
   204  
   205  	//// INIT
   206  	stdout, stderr, err := tf.Run("init")
   207  	if err != nil {
   208  		t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
   209  	}
   210  
   211  	// Make sure we didn't download the binary
   212  	if strings.Contains(stdout, "Installing hashicorp/test v") {
   213  		t.Errorf("test provider download message is present in init output:\n%s", stdout)
   214  	}
   215  	if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "test")) {
   216  		t.Errorf("test provider binary found in .terraform dir")
   217  	}
   218  
   219  	//// PLAN
   220  	_, stderr, err = tf.Run("plan", "-out=tfplan")
   221  	if err != nil {
   222  		t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
   223  	}
   224  
   225  	if !provider.PlanResourceChangeCalled() {
   226  		t.Error("PlanResourceChange not called on un-managed provider")
   227  	}
   228  
   229  	//// APPLY
   230  	_, stderr, err = tf.Run("apply", "tfplan")
   231  	if err != nil {
   232  		t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
   233  	}
   234  
   235  	if !provider.ApplyResourceChangeCalled() {
   236  		t.Error("ApplyResourceChange not called on un-managed provider")
   237  	}
   238  	provider.ResetApplyResourceChangeCalled()
   239  
   240  	//// DESTROY
   241  	_, stderr, err = tf.Run("destroy", "-auto-approve")
   242  	if err != nil {
   243  		t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr)
   244  	}
   245  
   246  	if !provider.ApplyResourceChangeCalled() {
   247  		t.Error("ApplyResourceChange (destroy) not called on in-process provider")
   248  	}
   249  	cancel()
   250  	<-closeCh
   251  }
   252  
   253  func TestUnmanagedSeparatePlan_proto5(t *testing.T) {
   254  	t.Parallel()
   255  
   256  	fixturePath := filepath.Join("testdata", "test-provider")
   257  	tf := e2e.NewBinary(t, terraformBin, fixturePath)
   258  
   259  	reattachCh := make(chan *plugin.ReattachConfig)
   260  	closeCh := make(chan struct{})
   261  	provider := &providerServer5{
   262  		ProviderServer: grpcwrap.Provider(simple5.Provider()),
   263  	}
   264  	ctx, cancel := context.WithCancel(context.Background())
   265  	defer cancel()
   266  	go plugin.Serve(&plugin.ServeConfig{
   267  		Logger: hclog.New(&hclog.LoggerOptions{
   268  			Name:   "plugintest",
   269  			Level:  hclog.Trace,
   270  			Output: ioutil.Discard,
   271  		}),
   272  		Test: &plugin.ServeTestConfig{
   273  			Context:          ctx,
   274  			ReattachConfigCh: reattachCh,
   275  			CloseCh:          closeCh,
   276  		},
   277  		GRPCServer: plugin.DefaultGRPCServer,
   278  		VersionedPlugins: map[int]plugin.PluginSet{
   279  			5: {
   280  				"provider": &tfplugin5.GRPCProviderPlugin{
   281  					GRPCProvider: func() proto5.ProviderServer {
   282  						return provider
   283  					},
   284  				},
   285  			},
   286  		},
   287  	})
   288  	config := <-reattachCh
   289  	if config == nil {
   290  		t.Fatalf("no reattach config received")
   291  	}
   292  	reattachStr, err := json.Marshal(map[string]reattachConfig{
   293  		"hashicorp/test": {
   294  			Protocol:        string(config.Protocol),
   295  			ProtocolVersion: 5,
   296  			Pid:             config.Pid,
   297  			Test:            true,
   298  			Addr: reattachConfigAddr{
   299  				Network: config.Addr.Network(),
   300  				String:  config.Addr.String(),
   301  			},
   302  		},
   303  	})
   304  	if err != nil {
   305  		t.Fatal(err)
   306  	}
   307  
   308  	tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr))
   309  
   310  	//// INIT
   311  	stdout, stderr, err := tf.Run("init")
   312  	if err != nil {
   313  		t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
   314  	}
   315  
   316  	// Make sure we didn't download the binary
   317  	if strings.Contains(stdout, "Installing hashicorp/test v") {
   318  		t.Errorf("test provider download message is present in init output:\n%s", stdout)
   319  	}
   320  	if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "test")) {
   321  		t.Errorf("test provider binary found in .terraform dir")
   322  	}
   323  
   324  	//// PLAN
   325  	_, stderr, err = tf.Run("plan", "-out=tfplan")
   326  	if err != nil {
   327  		t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
   328  	}
   329  
   330  	if !provider.PlanResourceChangeCalled() {
   331  		t.Error("PlanResourceChange not called on un-managed provider")
   332  	}
   333  
   334  	//// APPLY
   335  	_, stderr, err = tf.Run("apply", "tfplan")
   336  	if err != nil {
   337  		t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
   338  	}
   339  
   340  	if !provider.ApplyResourceChangeCalled() {
   341  		t.Error("ApplyResourceChange not called on un-managed provider")
   342  	}
   343  	provider.ResetApplyResourceChangeCalled()
   344  
   345  	//// DESTROY
   346  	_, stderr, err = tf.Run("destroy", "-auto-approve")
   347  	if err != nil {
   348  		t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr)
   349  	}
   350  
   351  	if !provider.ApplyResourceChangeCalled() {
   352  		t.Error("ApplyResourceChange (destroy) not called on in-process provider")
   353  	}
   354  	cancel()
   355  	<-closeCh
   356  }