github.com/opentofu/opentofu@v1.7.1/internal/command/e2etest/unmanaged_test.go (about)

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