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