| package e2etest |
| |
| import ( |
| "context" |
| "encoding/json" |
| "io/ioutil" |
| "path/filepath" |
| "strings" |
| "sync" |
| "testing" |
| |
| "github.com/hashicorp/go-hclog" |
| "github.com/hashicorp/go-plugin" |
| "github.com/hashicorp/terraform/internal/e2e" |
| "github.com/hashicorp/terraform/internal/grpcwrap" |
| tfplugin5 "github.com/hashicorp/terraform/internal/plugin" |
| tfplugin "github.com/hashicorp/terraform/internal/plugin6" |
| simple5 "github.com/hashicorp/terraform/internal/provider-simple" |
| simple "github.com/hashicorp/terraform/internal/provider-simple-v6" |
| proto5 "github.com/hashicorp/terraform/internal/tfplugin5" |
| proto "github.com/hashicorp/terraform/internal/tfplugin6" |
| ) |
| |
| // The tests in this file are for the "unmanaged provider workflow", which |
| // includes variants of the following sequence, with different details: |
| // terraform init |
| // terraform plan |
| // terraform apply |
| // |
| // These tests are run against an in-process server, and checked to make sure |
| // they're not trying to control the lifecycle of the binary. They are not |
| // checked for correctness of the operations themselves. |
| |
| type reattachConfig struct { |
| Protocol string |
| ProtocolVersion int |
| Pid int |
| Test bool |
| Addr reattachConfigAddr |
| } |
| |
| type reattachConfigAddr struct { |
| Network string |
| String string |
| } |
| |
| type providerServer struct { |
| sync.Mutex |
| proto.ProviderServer |
| planResourceChangeCalled bool |
| applyResourceChangeCalled bool |
| } |
| |
| func (p *providerServer) PlanResourceChange(ctx context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) { |
| p.Lock() |
| defer p.Unlock() |
| |
| p.planResourceChangeCalled = true |
| return p.ProviderServer.PlanResourceChange(ctx, req) |
| } |
| |
| func (p *providerServer) ApplyResourceChange(ctx context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) { |
| p.Lock() |
| defer p.Unlock() |
| |
| p.applyResourceChangeCalled = true |
| return p.ProviderServer.ApplyResourceChange(ctx, req) |
| } |
| |
| func (p *providerServer) PlanResourceChangeCalled() bool { |
| p.Lock() |
| defer p.Unlock() |
| |
| return p.planResourceChangeCalled |
| } |
| func (p *providerServer) ResetPlanResourceChangeCalled() { |
| p.Lock() |
| defer p.Unlock() |
| |
| p.planResourceChangeCalled = false |
| } |
| |
| func (p *providerServer) ApplyResourceChangeCalled() bool { |
| p.Lock() |
| defer p.Unlock() |
| |
| return p.applyResourceChangeCalled |
| } |
| func (p *providerServer) ResetApplyResourceChangeCalled() { |
| p.Lock() |
| defer p.Unlock() |
| |
| p.applyResourceChangeCalled = false |
| } |
| |
| type providerServer5 struct { |
| sync.Mutex |
| proto5.ProviderServer |
| planResourceChangeCalled bool |
| applyResourceChangeCalled bool |
| } |
| |
| func (p *providerServer5) PlanResourceChange(ctx context.Context, req *proto5.PlanResourceChange_Request) (*proto5.PlanResourceChange_Response, error) { |
| p.Lock() |
| defer p.Unlock() |
| |
| p.planResourceChangeCalled = true |
| return p.ProviderServer.PlanResourceChange(ctx, req) |
| } |
| |
| func (p *providerServer5) ApplyResourceChange(ctx context.Context, req *proto5.ApplyResourceChange_Request) (*proto5.ApplyResourceChange_Response, error) { |
| p.Lock() |
| defer p.Unlock() |
| |
| p.applyResourceChangeCalled = true |
| return p.ProviderServer.ApplyResourceChange(ctx, req) |
| } |
| |
| func (p *providerServer5) PlanResourceChangeCalled() bool { |
| p.Lock() |
| defer p.Unlock() |
| |
| return p.planResourceChangeCalled |
| } |
| func (p *providerServer5) ResetPlanResourceChangeCalled() { |
| p.Lock() |
| defer p.Unlock() |
| |
| p.planResourceChangeCalled = false |
| } |
| |
| func (p *providerServer5) ApplyResourceChangeCalled() bool { |
| p.Lock() |
| defer p.Unlock() |
| |
| return p.applyResourceChangeCalled |
| } |
| func (p *providerServer5) ResetApplyResourceChangeCalled() { |
| p.Lock() |
| defer p.Unlock() |
| |
| p.applyResourceChangeCalled = false |
| } |
| |
| func TestUnmanagedSeparatePlan(t *testing.T) { |
| t.Parallel() |
| |
| fixturePath := filepath.Join("testdata", "test-provider") |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) |
| |
| reattachCh := make(chan *plugin.ReattachConfig) |
| closeCh := make(chan struct{}) |
| provider := &providerServer{ |
| ProviderServer: grpcwrap.Provider6(simple.Provider()), |
| } |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| go plugin.Serve(&plugin.ServeConfig{ |
| Logger: hclog.New(&hclog.LoggerOptions{ |
| Name: "plugintest", |
| Level: hclog.Trace, |
| Output: ioutil.Discard, |
| }), |
| Test: &plugin.ServeTestConfig{ |
| Context: ctx, |
| ReattachConfigCh: reattachCh, |
| CloseCh: closeCh, |
| }, |
| GRPCServer: plugin.DefaultGRPCServer, |
| VersionedPlugins: map[int]plugin.PluginSet{ |
| 6: { |
| "provider": &tfplugin.GRPCProviderPlugin{ |
| GRPCProvider: func() proto.ProviderServer { |
| return provider |
| }, |
| }, |
| }, |
| }, |
| }) |
| config := <-reattachCh |
| if config == nil { |
| t.Fatalf("no reattach config received") |
| } |
| reattachStr, err := json.Marshal(map[string]reattachConfig{ |
| "hashicorp/test": { |
| Protocol: string(config.Protocol), |
| ProtocolVersion: 6, |
| Pid: config.Pid, |
| Test: true, |
| Addr: reattachConfigAddr{ |
| Network: config.Addr.Network(), |
| String: config.Addr.String(), |
| }, |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr)) |
| |
| //// INIT |
| stdout, stderr, err := tf.Run("init") |
| if err != nil { |
| t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) |
| } |
| |
| // Make sure we didn't download the binary |
| if strings.Contains(stdout, "Installing hashicorp/test v") { |
| t.Errorf("test provider download message is present in init output:\n%s", stdout) |
| } |
| if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "test")) { |
| t.Errorf("test provider binary found in .terraform dir") |
| } |
| |
| //// PLAN |
| _, stderr, err = tf.Run("plan", "-out=tfplan") |
| if err != nil { |
| t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) |
| } |
| |
| if !provider.PlanResourceChangeCalled() { |
| t.Error("PlanResourceChange not called on un-managed provider") |
| } |
| |
| //// APPLY |
| _, stderr, err = tf.Run("apply", "tfplan") |
| if err != nil { |
| t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr) |
| } |
| |
| if !provider.ApplyResourceChangeCalled() { |
| t.Error("ApplyResourceChange not called on un-managed provider") |
| } |
| provider.ResetApplyResourceChangeCalled() |
| |
| //// DESTROY |
| _, stderr, err = tf.Run("destroy", "-auto-approve") |
| if err != nil { |
| t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr) |
| } |
| |
| if !provider.ApplyResourceChangeCalled() { |
| t.Error("ApplyResourceChange (destroy) not called on in-process provider") |
| } |
| cancel() |
| <-closeCh |
| } |
| |
| func TestUnmanagedSeparatePlan_proto5(t *testing.T) { |
| t.Parallel() |
| |
| fixturePath := filepath.Join("testdata", "test-provider") |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) |
| |
| reattachCh := make(chan *plugin.ReattachConfig) |
| closeCh := make(chan struct{}) |
| provider := &providerServer5{ |
| ProviderServer: grpcwrap.Provider(simple5.Provider()), |
| } |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| go plugin.Serve(&plugin.ServeConfig{ |
| Logger: hclog.New(&hclog.LoggerOptions{ |
| Name: "plugintest", |
| Level: hclog.Trace, |
| Output: ioutil.Discard, |
| }), |
| Test: &plugin.ServeTestConfig{ |
| Context: ctx, |
| ReattachConfigCh: reattachCh, |
| CloseCh: closeCh, |
| }, |
| GRPCServer: plugin.DefaultGRPCServer, |
| VersionedPlugins: map[int]plugin.PluginSet{ |
| 5: { |
| "provider": &tfplugin5.GRPCProviderPlugin{ |
| GRPCProvider: func() proto5.ProviderServer { |
| return provider |
| }, |
| }, |
| }, |
| }, |
| }) |
| config := <-reattachCh |
| if config == nil { |
| t.Fatalf("no reattach config received") |
| } |
| reattachStr, err := json.Marshal(map[string]reattachConfig{ |
| "hashicorp/test": { |
| Protocol: string(config.Protocol), |
| ProtocolVersion: 5, |
| Pid: config.Pid, |
| Test: true, |
| Addr: reattachConfigAddr{ |
| Network: config.Addr.Network(), |
| String: config.Addr.String(), |
| }, |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr)) |
| |
| //// INIT |
| stdout, stderr, err := tf.Run("init") |
| if err != nil { |
| t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) |
| } |
| |
| // Make sure we didn't download the binary |
| if strings.Contains(stdout, "Installing hashicorp/test v") { |
| t.Errorf("test provider download message is present in init output:\n%s", stdout) |
| } |
| if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "test")) { |
| t.Errorf("test provider binary found in .terraform dir") |
| } |
| |
| //// PLAN |
| _, stderr, err = tf.Run("plan", "-out=tfplan") |
| if err != nil { |
| t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) |
| } |
| |
| if !provider.PlanResourceChangeCalled() { |
| t.Error("PlanResourceChange not called on un-managed provider") |
| } |
| |
| //// APPLY |
| _, stderr, err = tf.Run("apply", "tfplan") |
| if err != nil { |
| t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr) |
| } |
| |
| if !provider.ApplyResourceChangeCalled() { |
| t.Error("ApplyResourceChange not called on un-managed provider") |
| } |
| provider.ResetApplyResourceChangeCalled() |
| |
| //// DESTROY |
| _, stderr, err = tf.Run("destroy", "-auto-approve") |
| if err != nil { |
| t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr) |
| } |
| |
| if !provider.ApplyResourceChangeCalled() { |
| t.Error("ApplyResourceChange (destroy) not called on in-process provider") |
| } |
| cancel() |
| <-closeCh |
| } |