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 }