github.com/hashicorp/terraform-plugin-sdk@v1.17.2/helper/resource/plugin.go (about)

     1  package resource
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/hashicorp/go-hclog"
    13  	"github.com/hashicorp/terraform-exec/tfexec"
    14  	"github.com/hashicorp/terraform-plugin-sdk/acctest"
    15  	"github.com/hashicorp/terraform-plugin-sdk/helper/logging"
    16  	grpcplugin "github.com/hashicorp/terraform-plugin-sdk/internal/helper/plugin"
    17  	proto "github.com/hashicorp/terraform-plugin-sdk/internal/tfplugin5"
    18  	"github.com/hashicorp/terraform-plugin-sdk/plugin"
    19  	"github.com/hashicorp/terraform-plugin-sdk/terraform"
    20  	tftest "github.com/hashicorp/terraform-plugin-test/v2"
    21  	testing "github.com/mitchellh/go-testing-interface"
    22  )
    23  
    24  func runProviderCommand(t testing.T, f func() error, wd *tftest.WorkingDir, factories map[string]terraform.ResourceProviderFactory) error {
    25  	// don't point to this as a test failure location
    26  	// point to whatever called it
    27  	t.Helper()
    28  
    29  	// for backwards compatibility, make this opt-in
    30  	if os.Getenv("TF_ACCTEST_REATTACH") != "1" {
    31  		log.Println("[DEBUG] TF_ACCTEST_REATTACH not set to 1, not using reattach-based testing")
    32  		return f()
    33  	}
    34  	if acctest.TestHelper == nil {
    35  		log.Println("[DEBUG] acctest.TestHelper is nil, assuming we're not using binary acceptance testing")
    36  		return f()
    37  	}
    38  	log.Println("[DEBUG] TF_ACCTEST_REATTACH set to 1 and acctest.TestHelper is not nil, using reattach-based testing")
    39  
    40  	// Run the providers in the same process as the test runner using the
    41  	// reattach behavior in Terraform. This ensures we get test coverage
    42  	// and enables the use of delve as a debugger.
    43  	//
    44  	// This behavior is only available in Terraform 0.12.26 and later.
    45  
    46  	ctx, cancel := context.WithCancel(context.Background())
    47  	defer cancel()
    48  
    49  	// this is needed so Terraform doesn't default to expecting protocol 4;
    50  	// we're skipping the handshake because Terraform didn't launch the
    51  	// plugins.
    52  	os.Setenv("PLUGIN_PROTOCOL_VERSIONS", "5")
    53  
    54  	// Terraform 0.12.X and 0.13.X+ treat namespaceless providers
    55  	// differently in terms of what namespace they default to. So we're
    56  	// going to set both variations, as we don't know which version of
    57  	// Terraform we're talking to. We're also going to allow overriding
    58  	// the host or namespace using environment variables.
    59  	var namespaces []string
    60  	host := "registry.terraform.io"
    61  	if v := os.Getenv("TF_ACC_PROVIDER_NAMESPACE"); v != "" {
    62  		namespaces = append(namespaces, v)
    63  	} else {
    64  		namespaces = append(namespaces, "-", "hashicorp")
    65  	}
    66  	if v := os.Getenv("TF_ACC_PROVIDER_HOST"); v != "" {
    67  		host = v
    68  	}
    69  
    70  	// Spin up gRPC servers for every provider factory, start a
    71  	// WaitGroup to listen for all of the close channels.
    72  	var wg sync.WaitGroup
    73  	reattachInfo := map[string]tfexec.ReattachConfig{}
    74  	for providerName, factory := range factories {
    75  		// providerName may be returned as terraform-provider-foo, and
    76  		// we need just foo. So let's fix that.
    77  		providerName = strings.TrimPrefix(providerName, "terraform-provider-")
    78  
    79  		provider, err := factory()
    80  		if err != nil {
    81  			return fmt.Errorf("unable to create provider %q from factory: %v", providerName, err)
    82  		}
    83  
    84  		// keep track of the running factory, so we can make sure it's
    85  		// shut down.
    86  		wg.Add(1)
    87  
    88  		// configure the settings our plugin will be served with
    89  		// the GRPCProviderFunc wraps a non-gRPC provider server
    90  		// into a gRPC interface, and the logger just discards logs
    91  		// from go-plugin.
    92  		opts := &plugin.ServeOpts{
    93  			GRPCProviderFunc: func() proto.ProviderServer {
    94  				return grpcplugin.NewGRPCProviderServerShim(provider)
    95  			},
    96  			Logger: hclog.New(&hclog.LoggerOptions{
    97  				Name:   "plugintest",
    98  				Level:  hclog.Trace,
    99  				Output: ioutil.Discard,
   100  			}),
   101  		}
   102  
   103  		// let's actually start the provider server
   104  		config, closeCh, err := plugin.DebugServe(ctx, opts)
   105  		if err != nil {
   106  			return fmt.Errorf("unable to serve provider %q: %v", providerName, err)
   107  		}
   108  
   109  		tfexecConfig := tfexec.ReattachConfig{
   110  			Protocol: config.Protocol,
   111  			Pid:      config.Pid,
   112  			Test:     config.Test,
   113  			Addr: tfexec.ReattachConfigAddr{
   114  				Network: config.Addr.Network,
   115  				String:  config.Addr.String,
   116  			},
   117  		}
   118  
   119  		// plugin.DebugServe hijacks our log output location, so let's
   120  		// reset it
   121  		logging.SetTestOutput(t)
   122  
   123  		// when the provider exits, remove one from the waitgroup
   124  		// so we can track when everything is done
   125  		go func(c <-chan struct{}) {
   126  			<-c
   127  			wg.Done()
   128  		}(closeCh)
   129  
   130  		// set our provider's reattachinfo in our map, once
   131  		// for every namespace that different Terraform versions
   132  		// may expect.
   133  		for _, ns := range namespaces {
   134  			reattachInfo[strings.TrimSuffix(host, "/")+"/"+
   135  				strings.TrimSuffix(ns, "/")+"/"+
   136  				providerName] = tfexecConfig
   137  		}
   138  	}
   139  
   140  	// set the working directory reattach info that will tell Terraform how
   141  	// to connect to our various running servers.
   142  	wd.SetReattachInfo(reattachInfo)
   143  
   144  	// ok, let's call whatever Terraform command the test was trying to
   145  	// call, now that we know it'll attach back to those servers we just
   146  	// started.
   147  	err := f()
   148  	if err != nil {
   149  		log.Printf("[WARN] Got error running Terraform: %s", err)
   150  	}
   151  
   152  	// cancel the servers so they'll return. Otherwise, this closeCh won't
   153  	// get closed, and we'll hang here.
   154  	cancel()
   155  
   156  	// wait for the servers to actually shut down; it may take a moment for
   157  	// them to clean up, or whatever.
   158  	// TODO: add a timeout here?
   159  	// PC: do we need one? The test will time out automatically...
   160  	wg.Wait()
   161  
   162  	// once we've run the Terraform command, let's remove the reattach
   163  	// information from the WorkingDir's environment. The WorkingDir will
   164  	// persist until the next call, but the server in the reattach info
   165  	// doesn't exist anymore at this point, so the reattach info is no
   166  	// longer valid. In theory it should be overwritten in the next call,
   167  	// but just to avoid any confusing bug reports, let's just unset the
   168  	// environment variable altogether.
   169  	wd.UnsetReattachInfo()
   170  
   171  	// return any error returned from the orchestration code running
   172  	// Terraform commands
   173  	return err
   174  }