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 }