diff --git a/command/e2etest/provider_plugin_test.go b/command/e2etest/provider_plugin_test.go new file mode 100644 index 000000000..62099fa8c --- /dev/null +++ b/command/e2etest/provider_plugin_test.go @@ -0,0 +1,70 @@ +package e2etest + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/terraform/e2e" + "github.com/hashicorp/terraform/internal/getproviders" +) + +// TestProviderProtocols verifies that Terraform can execute provider plugins +// with both supported protocol versions. +func TestProviderProtocols(t *testing.T) { + t.Parallel() + + tf := e2e.NewBinary(terraformBin, "testdata/provider-plugin") + defer tf.Close() + + // In order to do a decent end-to-end test for this case we will need a real + // enough provider plugin to try to run and make sure we are able to + // actually run it. Here will build the simple and simple6 (built with + // protocol v6) providers. + simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") + simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) + + simpleProvider := filepath.Join(tf.WorkDir(), "terraform-provider-simple") + simpleProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple/main", simpleProvider) + + // Move the provider binaries into a directory that we will point terraform + // to using the -plugin-dir cli flag. + platform := getproviders.CurrentPlatform.String() + hashiDir := "cache/registry.terraform.io/hashicorp/" + if err := os.MkdirAll(tf.Path(hashiDir, "simple6/0.0.1/", platform), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.Rename(simple6ProviderExe, tf.Path(hashiDir, "simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil { + t.Fatal(err) + } + + if err := os.MkdirAll(tf.Path(hashiDir, "simple/0.0.1/", platform), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.Rename(simpleProviderExe, tf.Path(hashiDir, "simple/0.0.1/", platform, "terraform-provider-simple")); err != nil { + t.Fatal(err) + } + + //// INIT + _, stderr, err := tf.Run("init", "-plugin-dir=cache") + if err != nil { + t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) + } + + //// PLAN + _, stderr, err = tf.Run("plan", "-out=tfplan") + if err != nil { + t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) + } + + //// APPLY + stdout, stderr, err := tf.Run("apply", "tfplan") + if err != nil { + t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr) + } + + if !strings.Contains(stdout, "Apply complete! Resources: 2 added, 0 changed, 0 destroyed.") { + t.Fatalf("wrong output:\n%s", stdout) + } +} diff --git a/command/e2etest/provisioner_plugin_test.go b/command/e2etest/provisioner_plugin_test.go index d801eaef7..285df73ba 100644 --- a/command/e2etest/provisioner_plugin_test.go +++ b/command/e2etest/provisioner_plugin_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform/e2e" ) -// TestProviderDevOverrides is a test that terraform can execute a 3rd party +// TestProvisionerPlugin is a test that terraform can execute a 3rd party // provisioner plugin. func TestProvisionerPlugin(t *testing.T) { t.Parallel() diff --git a/command/e2etest/testdata/provider-plugin/main.tf b/command/e2etest/testdata/provider-plugin/main.tf new file mode 100644 index 000000000..ea4de021f --- /dev/null +++ b/command/e2etest/testdata/provider-plugin/main.tf @@ -0,0 +1,20 @@ +// the provider-plugin tests uses the -plugin-cache flag so terraform pulls the +// test binaries instead of reaching out to the registry. +terraform { + required_providers { + simple5 = { + source = "registry.terraform.io/hashicorp/simple" + } + simple6 = { + source = "registry.terraform.io/hashicorp/simple6" + } + } +} + +resource "simple_resource" "test-proto5" { + provider = simple5 +} + +resource "simple_resource" "test-proto6" { + provider = simple6 +} diff --git a/command/meta_providers.go b/command/meta_providers.go index 3d09a9975..5a10ce017 100644 --- a/command/meta_providers.go +++ b/command/meta_providers.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/providercache" tfplugin "github.com/hashicorp/terraform/plugin" + tfplugin6 "github.com/hashicorp/terraform/plugin6" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/tfdiags" ) @@ -360,10 +361,19 @@ func providerFactory(meta *providercache.CachedProvider) providers.Factory { } // store the client so that the plugin can kill the child process - p := raw.(*tfplugin.GRPCProvider) - p.PluginClient = client - - return p, nil + protoVer := client.NegotiatedVersion() + switch protoVer { + case 5: + p := raw.(*tfplugin.GRPCProvider) + p.PluginClient = client + return p, nil + case 6: + p := raw.(*tfplugin6.GRPCProvider) + p.PluginClient = client + return p, nil + default: + panic("unsupported protocol version") + } } } diff --git a/internal/grpcwrap/provider6.go b/internal/grpcwrap/provider6.go new file mode 100644 index 000000000..ebd114ee9 --- /dev/null +++ b/internal/grpcwrap/provider6.go @@ -0,0 +1,415 @@ +package grpcwrap + +import ( + "context" + + "github.com/hashicorp/terraform/internal/tfplugin6" + "github.com/hashicorp/terraform/plugin6/convert" + "github.com/hashicorp/terraform/providers" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/zclconf/go-cty/cty/msgpack" +) + +// New wraps a providers.Interface to implement a grpc ProviderServer using +// plugin protocol v6. This is useful for creating a test binary out of an +// internal provider implementation. +func Provider6(p providers.Interface) tfplugin6.ProviderServer { + return &provider6{ + provider: p, + schema: p.GetProviderSchema(), + } +} + +type provider6 struct { + provider providers.Interface + schema providers.GetProviderSchemaResponse +} + +func (p *provider6) GetProviderSchema(_ context.Context, req *tfplugin6.GetProviderSchema_Request) (*tfplugin6.GetProviderSchema_Response, error) { + resp := &tfplugin6.GetProviderSchema_Response{ + ResourceSchemas: make(map[string]*tfplugin6.Schema), + DataSourceSchemas: make(map[string]*tfplugin6.Schema), + } + + resp.Provider = &tfplugin6.Schema{ + Block: &tfplugin6.Schema_Block{}, + } + if p.schema.Provider.Block != nil { + resp.Provider.Block = convert.ConfigSchemaToProto(p.schema.Provider.Block) + } + + resp.ProviderMeta = &tfplugin6.Schema{ + Block: &tfplugin6.Schema_Block{}, + } + if p.schema.ProviderMeta.Block != nil { + resp.ProviderMeta.Block = convert.ConfigSchemaToProto(p.schema.ProviderMeta.Block) + } + + for typ, res := range p.schema.ResourceTypes { + resp.ResourceSchemas[typ] = &tfplugin6.Schema{ + Version: res.Version, + Block: convert.ConfigSchemaToProto(res.Block), + } + } + for typ, dat := range p.schema.DataSources { + resp.DataSourceSchemas[typ] = &tfplugin6.Schema{ + Version: dat.Version, + Block: convert.ConfigSchemaToProto(dat.Block), + } + } + + // include any diagnostics from the original GetSchema call + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, p.schema.Diagnostics) + + return resp, nil +} + +func (p *provider6) ValidateProviderConfig(_ context.Context, req *tfplugin6.ValidateProviderConfig_Request) (*tfplugin6.ValidateProviderConfig_Response, error) { + resp := &tfplugin6.ValidateProviderConfig_Response{} + ty := p.schema.Provider.Block.ImpliedType() + + configVal, err := decodeDynamicValue6(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + prepareResp := p.provider.ValidateProviderConfig(providers.ValidateProviderConfigRequest{ + Config: configVal, + }) + + // the PreparedConfig value is no longer used + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, prepareResp.Diagnostics) + return resp, nil +} + +func (p *provider6) ValidateResourceConfig(_ context.Context, req *tfplugin6.ValidateResourceConfig_Request) (*tfplugin6.ValidateResourceConfig_Response, error) { + resp := &tfplugin6.ValidateResourceConfig_Response{} + ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + + configVal, err := decodeDynamicValue6(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + validateResp := p.provider.ValidateResourceConfig(providers.ValidateResourceConfigRequest{ + TypeName: req.TypeName, + Config: configVal, + }) + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics) + return resp, nil +} + +func (p *provider6) ValidateDataSourceConfig(_ context.Context, req *tfplugin6.ValidateDataSourceConfig_Request) (*tfplugin6.ValidateDataSourceConfig_Response, error) { + resp := &tfplugin6.ValidateDataSourceConfig_Response{} + ty := p.schema.DataSources[req.TypeName].Block.ImpliedType() + + configVal, err := decodeDynamicValue6(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + validateResp := p.provider.ValidateDataSourceConfig(providers.ValidateDataSourceConfigRequest{ + TypeName: req.TypeName, + Config: configVal, + }) + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics) + return resp, nil +} + +func (p *provider6) UpgradeResourceState(_ context.Context, req *tfplugin6.UpgradeResourceState_Request) (*tfplugin6.UpgradeResourceState_Response, error) { + resp := &tfplugin6.UpgradeResourceState_Response{} + ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + + upgradeResp := p.provider.UpgradeResourceState(providers.UpgradeResourceStateRequest{ + TypeName: req.TypeName, + Version: req.Version, + RawStateJSON: req.RawState.Json, + }) + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, upgradeResp.Diagnostics) + if upgradeResp.Diagnostics.HasErrors() { + return resp, nil + } + + dv, err := encodeDynamicValue6(upgradeResp.UpgradedState, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + resp.UpgradedState = dv + + return resp, nil +} + +func (p *provider6) ConfigureProvider(_ context.Context, req *tfplugin6.ConfigureProvider_Request) (*tfplugin6.ConfigureProvider_Response, error) { + resp := &tfplugin6.ConfigureProvider_Response{} + ty := p.schema.Provider.Block.ImpliedType() + + configVal, err := decodeDynamicValue6(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + configureResp := p.provider.ConfigureProvider(providers.ConfigureProviderRequest{ + TerraformVersion: req.TerraformVersion, + Config: configVal, + }) + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, configureResp.Diagnostics) + return resp, nil +} + +func (p *provider6) ReadResource(_ context.Context, req *tfplugin6.ReadResource_Request) (*tfplugin6.ReadResource_Response, error) { + resp := &tfplugin6.ReadResource_Response{} + ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + + stateVal, err := decodeDynamicValue6(req.CurrentState, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaVal, err := decodeDynamicValue6(req.ProviderMeta, metaTy) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + readResp := p.provider.ReadResource(providers.ReadResourceRequest{ + TypeName: req.TypeName, + PriorState: stateVal, + Private: req.Private, + ProviderMeta: metaVal, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, readResp.Diagnostics) + if readResp.Diagnostics.HasErrors() { + return resp, nil + } + resp.Private = readResp.Private + + dv, err := encodeDynamicValue6(readResp.NewState, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.NewState = dv + + return resp, nil +} + +func (p *provider6) PlanResourceChange(_ context.Context, req *tfplugin6.PlanResourceChange_Request) (*tfplugin6.PlanResourceChange_Response, error) { + resp := &tfplugin6.PlanResourceChange_Response{} + ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + + priorStateVal, err := decodeDynamicValue6(req.PriorState, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + proposedStateVal, err := decodeDynamicValue6(req.ProposedNewState, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + configVal, err := decodeDynamicValue6(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaVal, err := decodeDynamicValue6(req.ProviderMeta, metaTy) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + planResp := p.provider.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: req.TypeName, + PriorState: priorStateVal, + ProposedNewState: proposedStateVal, + Config: configVal, + PriorPrivate: req.PriorPrivate, + ProviderMeta: metaVal, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, planResp.Diagnostics) + if planResp.Diagnostics.HasErrors() { + return resp, nil + } + + resp.PlannedPrivate = planResp.PlannedPrivate + + resp.PlannedState, err = encodeDynamicValue6(planResp.PlannedState, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + for _, path := range planResp.RequiresReplace { + resp.RequiresReplace = append(resp.RequiresReplace, convert.PathToAttributePath(path)) + } + + return resp, nil +} + +func (p *provider6) ApplyResourceChange(_ context.Context, req *tfplugin6.ApplyResourceChange_Request) (*tfplugin6.ApplyResourceChange_Response, error) { + resp := &tfplugin6.ApplyResourceChange_Response{} + ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + + priorStateVal, err := decodeDynamicValue6(req.PriorState, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + plannedStateVal, err := decodeDynamicValue6(req.PlannedState, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + configVal, err := decodeDynamicValue6(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaVal, err := decodeDynamicValue6(req.ProviderMeta, metaTy) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + applyResp := p.provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ + TypeName: req.TypeName, + PriorState: priorStateVal, + PlannedState: plannedStateVal, + Config: configVal, + PlannedPrivate: req.PlannedPrivate, + ProviderMeta: metaVal, + }) + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, applyResp.Diagnostics) + if applyResp.Diagnostics.HasErrors() { + return resp, nil + } + resp.Private = applyResp.Private + + resp.NewState, err = encodeDynamicValue6(applyResp.NewState, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + return resp, nil +} + +func (p *provider6) ImportResourceState(_ context.Context, req *tfplugin6.ImportResourceState_Request) (*tfplugin6.ImportResourceState_Response, error) { + resp := &tfplugin6.ImportResourceState_Response{} + + importResp := p.provider.ImportResourceState(providers.ImportResourceStateRequest{ + TypeName: req.TypeName, + ID: req.Id, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, importResp.Diagnostics) + + for _, res := range importResp.ImportedResources { + ty := p.schema.ResourceTypes[res.TypeName].Block.ImpliedType() + state, err := encodeDynamicValue6(res.State, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + continue + } + + resp.ImportedResources = append(resp.ImportedResources, &tfplugin6.ImportResourceState_ImportedResource{ + TypeName: res.TypeName, + State: state, + Private: res.Private, + }) + } + + return resp, nil +} + +func (p *provider6) ReadDataSource(_ context.Context, req *tfplugin6.ReadDataSource_Request) (*tfplugin6.ReadDataSource_Response, error) { + resp := &tfplugin6.ReadDataSource_Response{} + ty := p.schema.DataSources[req.TypeName].Block.ImpliedType() + + configVal, err := decodeDynamicValue6(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaVal, err := decodeDynamicValue6(req.ProviderMeta, metaTy) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + readResp := p.provider.ReadDataSource(providers.ReadDataSourceRequest{ + TypeName: req.TypeName, + Config: configVal, + ProviderMeta: metaVal, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, readResp.Diagnostics) + if readResp.Diagnostics.HasErrors() { + return resp, nil + } + + resp.State, err = encodeDynamicValue6(readResp.State, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + return resp, nil +} + +func (p *provider6) StopProvider(context.Context, *tfplugin6.StopProvider_Request) (*tfplugin6.StopProvider_Response, error) { + resp := &tfplugin6.StopProvider_Response{} + err := p.provider.Stop() + if err != nil { + resp.Error = err.Error() + } + return resp, nil +} + +// decode a DynamicValue from either the JSON or MsgPack encoding. +func decodeDynamicValue6(v *tfplugin6.DynamicValue, ty cty.Type) (cty.Value, error) { + // always return a valid value + var err error + res := cty.NullVal(ty) + if v == nil { + return res, nil + } + + switch { + case len(v.Msgpack) > 0: + res, err = msgpack.Unmarshal(v.Msgpack, ty) + case len(v.Json) > 0: + res, err = ctyjson.Unmarshal(v.Json, ty) + } + return res, err +} + +// encode a cty.Value into a DynamicValue msgpack payload. +func encodeDynamicValue6(v cty.Value, ty cty.Type) (*tfplugin6.DynamicValue, error) { + mp, err := msgpack.Marshal(v, ty) + return &tfplugin6.DynamicValue{ + Msgpack: mp, + }, err +} diff --git a/internal/grpcwrap/provisioner.go b/internal/grpcwrap/provisioner.go index 1fffc40ac..8213b5082 100644 --- a/internal/grpcwrap/provisioner.go +++ b/internal/grpcwrap/provisioner.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/terraform/provisioners" ) -// New wraps a providers.Interface to implement a grpc ProviderServer. +// New wraps a provisioners.Interface to implement a grpc ProviderServer. // This is useful for creating a test binary out of an internal provider // implementation. func Provisioner(p provisioners.Interface) tfplugin5.ProvisionerServer { diff --git a/internal/provider-simple-v6/main/main.go b/internal/provider-simple-v6/main/main.go new file mode 100644 index 000000000..e0acf496d --- /dev/null +++ b/internal/provider-simple-v6/main/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "github.com/hashicorp/terraform/internal/grpcwrap" + simple "github.com/hashicorp/terraform/internal/provider-simple-v6" + "github.com/hashicorp/terraform/internal/tfplugin6" + plugin "github.com/hashicorp/terraform/plugin6" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + GRPCProviderFunc: func() tfplugin6.ProviderServer { + return grpcwrap.Provider6(simple.Provider()) + }, + }) +} diff --git a/internal/provider-simple-v6/provider.go b/internal/provider-simple-v6/provider.go new file mode 100644 index 000000000..510c11bfc --- /dev/null +++ b/internal/provider-simple-v6/provider.go @@ -0,0 +1,128 @@ +// simple provider a minimal provider implementation for testing +package simple + +import ( + "errors" + "time" + + "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/providers" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" +) + +type simple struct { + schema providers.GetProviderSchemaResponse +} + +func Provider() providers.Interface { + simpleResource := providers.Schema{ + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Computed: true, + Type: cty.String, + }, + "value": { + Optional: true, + Type: cty.String, + }, + }, + }, + } + + return simple{ + schema: providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Block: nil, + }, + ResourceTypes: map[string]providers.Schema{ + "simple_resource": simpleResource, + }, + DataSources: map[string]providers.Schema{ + "simple_resource": simpleResource, + }, + }, + } +} + +func (s simple) GetProviderSchema() providers.GetProviderSchemaResponse { + return s.schema +} + +func (s simple) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { + return resp +} + +func (s simple) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) (resp providers.ValidateResourceConfigResponse) { + return resp +} + +func (s simple) ValidateDataSourceConfig(req providers.ValidateDataSourceConfigRequest) (resp providers.ValidateDataSourceConfigResponse) { + return resp +} + +func (p simple) UpgradeResourceState(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { + ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + val, err := ctyjson.Unmarshal(req.RawStateJSON, ty) + resp.Diagnostics = resp.Diagnostics.Append(err) + resp.UpgradedState = val + return resp +} + +func (s simple) ConfigureProvider(providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { + return resp +} + +func (s simple) Stop() error { + return nil +} + +func (s simple) ReadResource(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { + // just return the same state we received + resp.NewState = req.PriorState + return resp +} + +func (s simple) PlanResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + m := req.ProposedNewState.AsValueMap() + _, ok := m["id"] + if !ok { + m["id"] = cty.UnknownVal(cty.String) + } + + resp.PlannedState = cty.ObjectVal(m) + return resp +} + +func (s simple) ApplyResourceChange(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + if req.PlannedState.IsNull() { + resp.NewState = req.PlannedState + return resp + } + + m := req.PlannedState.AsValueMap() + _, ok := m["id"] + if !ok { + m["id"] = cty.StringVal(time.Now().String()) + } + resp.NewState = cty.ObjectVal(m) + + return resp +} + +func (s simple) ImportResourceState(providers.ImportResourceStateRequest) (resp providers.ImportResourceStateResponse) { + resp.Diagnostics = resp.Diagnostics.Append(errors.New("unsupported")) + return resp +} + +func (s simple) ReadDataSource(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { + m := req.Config.AsValueMap() + m["id"] = cty.StringVal("static_id") + resp.State = cty.ObjectVal(m) + return resp +} + +func (s simple) Close() error { + return nil +} diff --git a/plugin/plugin.go b/plugin/plugin.go index 07a3000c0..3f962f113 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -2,11 +2,18 @@ package plugin import ( "github.com/hashicorp/go-plugin" + "github.com/hashicorp/terraform/plugin6" ) +// VersionedPlugins includes both protocol 5 and 6 because this is the function +// called in providerFactory (command/meta_providers.go) to set up the initial +// plugin client config. var VersionedPlugins = map[int]plugin.PluginSet{ 5: { "provider": &GRPCProviderPlugin{}, "provisioner": &GRPCProvisionerPlugin{}, }, + 6: { + "provider": &plugin6.GRPCProviderPlugin{}, + }, } diff --git a/plugin6/grpc_provider.go b/plugin6/grpc_provider.go index 68f423860..551e81e27 100644 --- a/plugin6/grpc_provider.go +++ b/plugin6/grpc_provider.go @@ -39,7 +39,7 @@ func (p *GRPCProviderPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Serve // GRPCProvider handles the client, or core side of the plugin rpc connection. // The GRPCProvider methods are mostly a translation layer between the -// terraform provioders types and the grpc proto types, directly converting +// terraform providers types and the grpc proto types, directly converting // between the two. type GRPCProvider struct { // PluginClient provides a reference to the plugin.Client which controls the plugin process. @@ -72,10 +72,10 @@ func New(client proto6.ProviderClient, ctx context.Context) GRPCProvider { // getSchema is used internally to get the saved provider schema. The schema // should have already been fetched from the provider, but we have to -// synchronize access to avoid being called concurrently with GetSchema. +// synchronize access to avoid being called concurrently with GetProviderSchema. func (p *GRPCProvider) getSchema() providers.GetProviderSchemaResponse { p.mu.Lock() - // unlock inline in case GetSchema needs to be called + // unlock inline in case GetProviderSchema needs to be called if p.schemas.Provider.Block != nil { p.mu.Unlock() return p.schemas @@ -85,7 +85,7 @@ func (p *GRPCProvider) getSchema() providers.GetProviderSchemaResponse { // the schema should have been fetched already, but give it another shot // just in case things are being called out of order. This may happen for // tests. - schemas := p.GetSchema() + schemas := p.GetProviderSchema() if schemas.Diagnostics.HasErrors() { panic(schemas.Diagnostics.Err()) } @@ -122,8 +122,8 @@ func (p *GRPCProvider) getProviderMetaSchema() providers.Schema { return schema.ProviderMeta } -func (p *GRPCProvider) GetSchema() (resp providers.GetProviderSchemaResponse) { - logger.Trace("GRPCProvider.v6: GetSchema") +func (p *GRPCProvider) GetProviderSchema() (resp providers.GetProviderSchemaResponse) { + logger.Trace("GRPCProvider.v6: GetProviderSchema") p.mu.Lock() defer p.mu.Unlock() @@ -286,8 +286,8 @@ func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ return resp } -func (p *GRPCProvider) Configure(r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { - logger.Trace("GRPCProvider.v6: Configure") +func (p *GRPCProvider) ConfigureProvider(r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { + logger.Trace("GRPCProvider.v6: ConfigureProvider") schema := p.getSchema() diff --git a/plugin6/grpc_provider_test.go b/plugin6/grpc_provider_test.go new file mode 100644 index 000000000..a270d3344 --- /dev/null +++ b/plugin6/grpc_provider_test.go @@ -0,0 +1,722 @@ +package plugin6 + +import ( + "bytes" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform/configs/hcl2shim" + "github.com/hashicorp/terraform/providers" + "github.com/hashicorp/terraform/tfdiags" + "github.com/zclconf/go-cty/cty" + + proto "github.com/hashicorp/terraform/internal/tfplugin6" + mockproto "github.com/hashicorp/terraform/plugin6/mock_proto" +) + +var _ providers.Interface = (*GRPCProvider)(nil) + +var ( + equateEmpty = cmpopts.EquateEmpty() + typeComparer = cmp.Comparer(cty.Type.Equals) + valueComparer = cmp.Comparer(cty.Value.RawEquals) +) + +func mockProviderClient(t *testing.T) *mockproto.MockProviderClient { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + // we always need a GetSchema method + client.EXPECT().GetProviderSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerProtoSchema(), nil) + + return client +} + +func checkDiags(t *testing.T, d tfdiags.Diagnostics) { + t.Helper() + if d.HasErrors() { + t.Fatal(d.Err()) + } +} + +func providerProtoSchema() *proto.GetProviderSchema_Response { + return &proto.GetProviderSchema_Response{ + Provider: &proto.Schema{ + Block: &proto.Schema_Block{ + Attributes: []*proto.Schema_Attribute{ + { + Name: "attr", + Type: []byte(`"string"`), + Required: true, + }, + }, + }, + }, + ResourceSchemas: map[string]*proto.Schema{ + "resource": { + Version: 1, + Block: &proto.Schema_Block{ + Attributes: []*proto.Schema_Attribute{ + { + Name: "attr", + Type: []byte(`"string"`), + Required: true, + }, + }, + }, + }, + }, + DataSourceSchemas: map[string]*proto.Schema{ + "data": { + Version: 1, + Block: &proto.Schema_Block{ + Attributes: []*proto.Schema_Attribute{ + { + Name: "attr", + Type: []byte(`"string"`), + Required: true, + }, + }, + }, + }, + }, + } +} + +func TestGRPCProvider_GetSchema(t *testing.T) { + p := &GRPCProvider{ + client: mockProviderClient(t), + } + + resp := p.GetProviderSchema() + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_PrepareProviderConfig(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ValidateProviderConfig( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ValidateProviderConfig_Response{}, nil) + + cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"}) + resp := p.ValidateProviderConfig(providers.ValidateProviderConfigRequest{Config: cfg}) + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_ValidateResourceConfig(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ValidateResourceConfig( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ValidateResourceConfig_Response{}, nil) + + cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"}) + resp := p.ValidateResourceConfig(providers.ValidateResourceConfigRequest{ + TypeName: "resource", + Config: cfg, + }) + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_ValidateDataSourceConfig(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ValidateDataSourceConfig( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ValidateDataSourceConfig_Response{}, nil) + + cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"}) + resp := p.ValidateDataSourceConfig(providers.ValidateDataSourceConfigRequest{ + TypeName: "data", + Config: cfg, + }) + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_UpgradeResourceState(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().UpgradeResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.UpgradeResourceState_Response{ + UpgradedState: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + }, nil) + + resp := p.UpgradeResourceState(providers.UpgradeResourceStateRequest{ + TypeName: "resource", + Version: 0, + RawStateJSON: []byte(`{"old_attr":"bar"}`), + }) + checkDiags(t, resp.Diagnostics) + + expected := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_UpgradeResourceStateJSON(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().UpgradeResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.UpgradeResourceState_Response{ + UpgradedState: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + }, nil) + + resp := p.UpgradeResourceState(providers.UpgradeResourceStateRequest{ + TypeName: "resource", + Version: 0, + RawStateJSON: []byte(`{"old_attr":"bar"}`), + }) + checkDiags(t, resp.Diagnostics) + + expected := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_Configure(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ConfigureProvider( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ConfigureProvider_Response{}, nil) + + resp := p.ConfigureProvider(providers.ConfigureProviderRequest{ + Config: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }) + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_Stop(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().StopProvider( + gomock.Any(), + gomock.Any(), + ).Return(&proto.StopProvider_Response{}, nil) + + err := p.Stop() + if err != nil { + t.Fatal(err) + } +} + +func TestGRPCProvider_ReadResource(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ReadResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ReadResource_Response{ + NewState: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + }, nil) + + resp := p.ReadResource(providers.ReadResourceRequest{ + TypeName: "resource", + PriorState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expected := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_ReadResourceJSON(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ReadResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ReadResource_Response{ + NewState: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + }, nil) + + resp := p.ReadResource(providers.ReadResourceRequest{ + TypeName: "resource", + PriorState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expected := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_ReadEmptyJSON(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ReadResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ReadResource_Response{ + NewState: &proto.DynamicValue{ + Json: []byte(``), + }, + }, nil) + + obj := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }) + resp := p.ReadResource(providers.ReadResourceRequest{ + TypeName: "resource", + PriorState: obj, + }) + + checkDiags(t, resp.Diagnostics) + + expected := cty.NullVal(obj.Type()) + + if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_PlanResourceChange(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedPrivate := []byte(`{"meta": "data"}`) + + client.EXPECT().PlanResourceChange( + gomock.Any(), + gomock.Any(), + ).Return(&proto.PlanResourceChange_Response{ + PlannedState: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + RequiresReplace: []*proto.AttributePath{ + { + Steps: []*proto.AttributePath_Step{ + { + Selector: &proto.AttributePath_Step_AttributeName{ + AttributeName: "attr", + }, + }, + }, + }, + }, + PlannedPrivate: expectedPrivate, + }, nil) + + resp := p.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: "resource", + PriorState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + ProposedNewState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + Config: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expectedState := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty)) + } + + expectedReplace := `[]cty.Path{cty.Path{cty.GetAttrStep{Name:"attr"}}}` + replace := fmt.Sprintf("%#v", resp.RequiresReplace) + if expectedReplace != replace { + t.Fatalf("expected %q, got %q", expectedReplace, replace) + } + + if !bytes.Equal(expectedPrivate, resp.PlannedPrivate) { + t.Fatalf("expected %q, got %q", expectedPrivate, resp.PlannedPrivate) + } +} + +func TestGRPCProvider_PlanResourceChangeJSON(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedPrivate := []byte(`{"meta": "data"}`) + + client.EXPECT().PlanResourceChange( + gomock.Any(), + gomock.Any(), + ).Return(&proto.PlanResourceChange_Response{ + PlannedState: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + RequiresReplace: []*proto.AttributePath{ + { + Steps: []*proto.AttributePath_Step{ + { + Selector: &proto.AttributePath_Step_AttributeName{ + AttributeName: "attr", + }, + }, + }, + }, + }, + PlannedPrivate: expectedPrivate, + }, nil) + + resp := p.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: "resource", + PriorState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + ProposedNewState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + Config: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expectedState := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty)) + } + + expectedReplace := `[]cty.Path{cty.Path{cty.GetAttrStep{Name:"attr"}}}` + replace := fmt.Sprintf("%#v", resp.RequiresReplace) + if expectedReplace != replace { + t.Fatalf("expected %q, got %q", expectedReplace, replace) + } + + if !bytes.Equal(expectedPrivate, resp.PlannedPrivate) { + t.Fatalf("expected %q, got %q", expectedPrivate, resp.PlannedPrivate) + } +} + +func TestGRPCProvider_ApplyResourceChange(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedPrivate := []byte(`{"meta": "data"}`) + + client.EXPECT().ApplyResourceChange( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ApplyResourceChange_Response{ + NewState: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + Private: expectedPrivate, + }, nil) + + resp := p.ApplyResourceChange(providers.ApplyResourceChangeRequest{ + TypeName: "resource", + PriorState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + Config: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + PlannedPrivate: expectedPrivate, + }) + + checkDiags(t, resp.Diagnostics) + + expectedState := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty)) + } + + if !bytes.Equal(expectedPrivate, resp.Private) { + t.Fatalf("expected %q, got %q", expectedPrivate, resp.Private) + } +} +func TestGRPCProvider_ApplyResourceChangeJSON(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedPrivate := []byte(`{"meta": "data"}`) + + client.EXPECT().ApplyResourceChange( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ApplyResourceChange_Response{ + NewState: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + Private: expectedPrivate, + }, nil) + + resp := p.ApplyResourceChange(providers.ApplyResourceChangeRequest{ + TypeName: "resource", + PriorState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + Config: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + PlannedPrivate: expectedPrivate, + }) + + checkDiags(t, resp.Diagnostics) + + expectedState := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty)) + } + + if !bytes.Equal(expectedPrivate, resp.Private) { + t.Fatalf("expected %q, got %q", expectedPrivate, resp.Private) + } +} + +func TestGRPCProvider_ImportResourceState(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedPrivate := []byte(`{"meta": "data"}`) + + client.EXPECT().ImportResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ImportResourceState_Response{ + ImportedResources: []*proto.ImportResourceState_ImportedResource{ + { + TypeName: "resource", + State: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + Private: expectedPrivate, + }, + }, + }, nil) + + resp := p.ImportResourceState(providers.ImportResourceStateRequest{ + TypeName: "resource", + ID: "foo", + }) + + checkDiags(t, resp.Diagnostics) + + expectedResource := providers.ImportedResource{ + TypeName: "resource", + State: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + Private: expectedPrivate, + } + + imported := resp.ImportedResources[0] + if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty)) + } +} +func TestGRPCProvider_ImportResourceStateJSON(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedPrivate := []byte(`{"meta": "data"}`) + + client.EXPECT().ImportResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ImportResourceState_Response{ + ImportedResources: []*proto.ImportResourceState_ImportedResource{ + { + TypeName: "resource", + State: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + Private: expectedPrivate, + }, + }, + }, nil) + + resp := p.ImportResourceState(providers.ImportResourceStateRequest{ + TypeName: "resource", + ID: "foo", + }) + + checkDiags(t, resp.Diagnostics) + + expectedResource := providers.ImportedResource{ + TypeName: "resource", + State: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + Private: expectedPrivate, + } + + imported := resp.ImportedResources[0] + if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_ReadDataSource(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ReadDataSource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ReadDataSource_Response{ + State: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + }, nil) + + resp := p.ReadDataSource(providers.ReadDataSourceRequest{ + TypeName: "data", + Config: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expected := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expected, resp.State, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_ReadDataSourceJSON(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ReadDataSource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ReadDataSource_Response{ + State: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + }, nil) + + resp := p.ReadDataSource(providers.ReadDataSourceRequest{ + TypeName: "data", + Config: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expected := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expected, resp.State, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty)) + } +} diff --git a/plugin6/mock_proto/generate.go b/plugin6/mock_proto/generate.go new file mode 100644 index 000000000..cde637e4b --- /dev/null +++ b/plugin6/mock_proto/generate.go @@ -0,0 +1,3 @@ +//go:generate go run github.com/golang/mock/mockgen -destination mock.go github.com/hashicorp/terraform/internal/tfplugin6 ProviderClient + +package mock_tfplugin6 diff --git a/plugin6/mock_proto/mock.go b/plugin6/mock_proto/mock.go new file mode 100644 index 000000000..c661b8756 --- /dev/null +++ b/plugin6/mock_proto/mock.go @@ -0,0 +1,276 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/hashicorp/terraform/internal/tfplugin6 (interfaces: ProviderClient) + +// Package mock_tfplugin6 is a generated GoMock package. +package mock_tfplugin6 + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + tfplugin6 "github.com/hashicorp/terraform/internal/tfplugin6" + grpc "google.golang.org/grpc" + reflect "reflect" +) + +// MockProviderClient is a mock of ProviderClient interface +type MockProviderClient struct { + ctrl *gomock.Controller + recorder *MockProviderClientMockRecorder +} + +// MockProviderClientMockRecorder is the mock recorder for MockProviderClient +type MockProviderClientMockRecorder struct { + mock *MockProviderClient +} + +// NewMockProviderClient creates a new mock instance +func NewMockProviderClient(ctrl *gomock.Controller) *MockProviderClient { + mock := &MockProviderClient{ctrl: ctrl} + mock.recorder = &MockProviderClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockProviderClient) EXPECT() *MockProviderClientMockRecorder { + return m.recorder +} + +// ApplyResourceChange mocks base method +func (m *MockProviderClient) ApplyResourceChange(arg0 context.Context, arg1 *tfplugin6.ApplyResourceChange_Request, arg2 ...grpc.CallOption) (*tfplugin6.ApplyResourceChange_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ApplyResourceChange", varargs...) + ret0, _ := ret[0].(*tfplugin6.ApplyResourceChange_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ApplyResourceChange indicates an expected call of ApplyResourceChange +func (mr *MockProviderClientMockRecorder) ApplyResourceChange(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyResourceChange", reflect.TypeOf((*MockProviderClient)(nil).ApplyResourceChange), varargs...) +} + +// ConfigureProvider mocks base method +func (m *MockProviderClient) ConfigureProvider(arg0 context.Context, arg1 *tfplugin6.ConfigureProvider_Request, arg2 ...grpc.CallOption) (*tfplugin6.ConfigureProvider_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ConfigureProvider", varargs...) + ret0, _ := ret[0].(*tfplugin6.ConfigureProvider_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfigureProvider indicates an expected call of ConfigureProvider +func (mr *MockProviderClientMockRecorder) ConfigureProvider(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigureProvider", reflect.TypeOf((*MockProviderClient)(nil).ConfigureProvider), varargs...) +} + +// GetProviderSchema mocks base method +func (m *MockProviderClient) GetProviderSchema(arg0 context.Context, arg1 *tfplugin6.GetProviderSchema_Request, arg2 ...grpc.CallOption) (*tfplugin6.GetProviderSchema_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetProviderSchema", varargs...) + ret0, _ := ret[0].(*tfplugin6.GetProviderSchema_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProviderSchema indicates an expected call of GetProviderSchema +func (mr *MockProviderClientMockRecorder) GetProviderSchema(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProviderSchema", reflect.TypeOf((*MockProviderClient)(nil).GetProviderSchema), varargs...) +} + +// ImportResourceState mocks base method +func (m *MockProviderClient) ImportResourceState(arg0 context.Context, arg1 *tfplugin6.ImportResourceState_Request, arg2 ...grpc.CallOption) (*tfplugin6.ImportResourceState_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ImportResourceState", varargs...) + ret0, _ := ret[0].(*tfplugin6.ImportResourceState_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ImportResourceState indicates an expected call of ImportResourceState +func (mr *MockProviderClientMockRecorder) ImportResourceState(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportResourceState", reflect.TypeOf((*MockProviderClient)(nil).ImportResourceState), varargs...) +} + +// PlanResourceChange mocks base method +func (m *MockProviderClient) PlanResourceChange(arg0 context.Context, arg1 *tfplugin6.PlanResourceChange_Request, arg2 ...grpc.CallOption) (*tfplugin6.PlanResourceChange_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PlanResourceChange", varargs...) + ret0, _ := ret[0].(*tfplugin6.PlanResourceChange_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PlanResourceChange indicates an expected call of PlanResourceChange +func (mr *MockProviderClientMockRecorder) PlanResourceChange(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PlanResourceChange", reflect.TypeOf((*MockProviderClient)(nil).PlanResourceChange), varargs...) +} + +// ReadDataSource mocks base method +func (m *MockProviderClient) ReadDataSource(arg0 context.Context, arg1 *tfplugin6.ReadDataSource_Request, arg2 ...grpc.CallOption) (*tfplugin6.ReadDataSource_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ReadDataSource", varargs...) + ret0, _ := ret[0].(*tfplugin6.ReadDataSource_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadDataSource indicates an expected call of ReadDataSource +func (mr *MockProviderClientMockRecorder) ReadDataSource(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDataSource", reflect.TypeOf((*MockProviderClient)(nil).ReadDataSource), varargs...) +} + +// ReadResource mocks base method +func (m *MockProviderClient) ReadResource(arg0 context.Context, arg1 *tfplugin6.ReadResource_Request, arg2 ...grpc.CallOption) (*tfplugin6.ReadResource_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ReadResource", varargs...) + ret0, _ := ret[0].(*tfplugin6.ReadResource_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadResource indicates an expected call of ReadResource +func (mr *MockProviderClientMockRecorder) ReadResource(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadResource", reflect.TypeOf((*MockProviderClient)(nil).ReadResource), varargs...) +} + +// StopProvider mocks base method +func (m *MockProviderClient) StopProvider(arg0 context.Context, arg1 *tfplugin6.StopProvider_Request, arg2 ...grpc.CallOption) (*tfplugin6.StopProvider_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "StopProvider", varargs...) + ret0, _ := ret[0].(*tfplugin6.StopProvider_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StopProvider indicates an expected call of StopProvider +func (mr *MockProviderClientMockRecorder) StopProvider(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopProvider", reflect.TypeOf((*MockProviderClient)(nil).StopProvider), varargs...) +} + +// UpgradeResourceState mocks base method +func (m *MockProviderClient) UpgradeResourceState(arg0 context.Context, arg1 *tfplugin6.UpgradeResourceState_Request, arg2 ...grpc.CallOption) (*tfplugin6.UpgradeResourceState_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpgradeResourceState", varargs...) + ret0, _ := ret[0].(*tfplugin6.UpgradeResourceState_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpgradeResourceState indicates an expected call of UpgradeResourceState +func (mr *MockProviderClientMockRecorder) UpgradeResourceState(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpgradeResourceState", reflect.TypeOf((*MockProviderClient)(nil).UpgradeResourceState), varargs...) +} + +// ValidateDataSourceConfig mocks base method +func (m *MockProviderClient) ValidateDataSourceConfig(arg0 context.Context, arg1 *tfplugin6.ValidateDataSourceConfig_Request, arg2 ...grpc.CallOption) (*tfplugin6.ValidateDataSourceConfig_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ValidateDataSourceConfig", varargs...) + ret0, _ := ret[0].(*tfplugin6.ValidateDataSourceConfig_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateDataSourceConfig indicates an expected call of ValidateDataSourceConfig +func (mr *MockProviderClientMockRecorder) ValidateDataSourceConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateDataSourceConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateDataSourceConfig), varargs...) +} + +// ValidateProviderConfig mocks base method +func (m *MockProviderClient) ValidateProviderConfig(arg0 context.Context, arg1 *tfplugin6.ValidateProviderConfig_Request, arg2 ...grpc.CallOption) (*tfplugin6.ValidateProviderConfig_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ValidateProviderConfig", varargs...) + ret0, _ := ret[0].(*tfplugin6.ValidateProviderConfig_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateProviderConfig indicates an expected call of ValidateProviderConfig +func (mr *MockProviderClientMockRecorder) ValidateProviderConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateProviderConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateProviderConfig), varargs...) +} + +// ValidateResourceConfig mocks base method +func (m *MockProviderClient) ValidateResourceConfig(arg0 context.Context, arg1 *tfplugin6.ValidateResourceConfig_Request, arg2 ...grpc.CallOption) (*tfplugin6.ValidateResourceConfig_Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ValidateResourceConfig", varargs...) + ret0, _ := ret[0].(*tfplugin6.ValidateResourceConfig_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateResourceConfig indicates an expected call of ValidateResourceConfig +func (mr *MockProviderClientMockRecorder) ValidateResourceConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateResourceConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateResourceConfig), varargs...) +} diff --git a/plugin6/serve.go b/plugin6/serve.go index 7c201a7fa..8c5203fcd 100644 --- a/plugin6/serve.go +++ b/plugin6/serve.go @@ -8,24 +8,21 @@ import ( const ( // The constants below are the names of the plugins that can be dispensed // from the plugin server. - ProviderPluginName = "provider" - ProvisionerPluginName = "provisioner" + ProviderPluginName = "provider" - // DefaultProtocolVersion is the protocol version assumed for legacy clients that don't specify - // a particular version during their handshake. This is the version used when Terraform 0.10 - // and 0.11 launch plugins that were built with support for both versions 4 and 5, and must - // stay unchanged at 4 until we intentionally build plugins that are not compatible with 0.10 and - // 0.11. + // DefaultProtocolVersion is the protocol version assumed for legacy clients + // that don't specify a particular version during their handshake. Since we + // explicitly set VersionedPlugins in Serve, this number does not need to + // change with the protocol version and can effectively stay 4 forever + // (unless we need the "biggest hammer" approach to break all provider + // compatibility). DefaultProtocolVersion = 4 ) // Handshake is the HandshakeConfig used to configure clients and servers. var Handshake = plugin.HandshakeConfig{ // The ProtocolVersion is the version that must match between TF core - // and TF plugins. This should be bumped whenever a change happens in - // one or the other that makes it so that they can't safely communicate. - // This could be adding a new interface value, it could be how - // helper/schema computes diffs, etc. + // and TF plugins. ProtocolVersion: DefaultProtocolVersion, // The magic cookie values should NEVER be changed. @@ -37,8 +34,6 @@ type GRPCProviderFunc func() proto.ProviderServer // ServeOpts are the configurations to serve a plugin. type ServeOpts struct { - // Wrapped versions of the above plugins will automatically shimmed and - // added to the GRPC functions when possible. GRPCProviderFunc GRPCProviderFunc } @@ -57,7 +52,7 @@ func pluginSet(opts *ServeOpts) map[int]plugin.PluginSet { // add the new protocol versions if they're configured if opts.GRPCProviderFunc != nil { - plugins[5] = plugin.PluginSet{} + plugins[6] = plugin.PluginSet{} if opts.GRPCProviderFunc != nil { plugins[6]["provider"] = &GRPCProviderPlugin{ GRPCProvider: opts.GRPCProviderFunc,