helper/plugin package for grpc servers

The new helper/plugin package contains the grpc servers for handling the
new plugin protocol

The GRPCProviderServer and GRPCProvisionerServer handle the grpc plugin
protocol, and convert the requests to the legacy schema.Provider and
schema.Provisioner methods.
This commit is contained in:
James Bardin 2018-08-08 17:02:24 -04:00 committed by Martin Atkins
parent 1ec792cabc
commit 63dcdbe948
11 changed files with 2246 additions and 0 deletions

View File

@ -0,0 +1,41 @@
package plugin
import (
"github.com/hashicorp/terraform/plugin/proto"
)
// diagsFromWarnsErrs converts the warnings and errors return by the lagacy
// provider to diagnostics.
func diagsFromWarnsErrs(warns []string, errs []error) (diags []*proto.Diagnostic) {
for _, w := range warns {
diags = appendDiag(diags, w)
}
for _, e := range errs {
diags = appendDiag(diags, e)
}
return diags
}
// appendDiag appends a new diagnostic from a warning string or an error. This
// panics if d is not a string or error.
func appendDiag(diags []*proto.Diagnostic, d interface{}) []*proto.Diagnostic {
switch d := d.(type) {
case error:
diags = append(diags, &proto.Diagnostic{
Severity: proto.Diagnostic_ERROR,
Summary: d.Error(),
})
case string:
diags = append(diags, &proto.Diagnostic{
Severity: proto.Diagnostic_WARNING,
Summary: d,
})
case *proto.Diagnostic:
diags = append(diags, d)
case []*proto.Diagnostic:
diags = append(diags, d...)
}
return diags
}

View File

@ -0,0 +1,45 @@
package plugin
import (
"errors"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/plugin/proto"
)
func TestDiagnostics(t *testing.T) {
diags := diagsFromWarnsErrs(
[]string{
"warning 1",
"warning 2",
},
[]error{
errors.New("error 1"),
errors.New("error 2"),
},
)
expected := []*proto.Diagnostic{
{
Severity: proto.Diagnostic_WARNING,
Summary: "warning 1",
},
{
Severity: proto.Diagnostic_WARNING,
Summary: "warning 2",
},
{
Severity: proto.Diagnostic_ERROR,
Summary: "error 1",
},
{
Severity: proto.Diagnostic_ERROR,
Summary: "error 2",
},
}
if !cmp.Equal(expected, diags) {
t.Fatal(cmp.Diff(expected, diags))
}
}

6
helper/plugin/doc.go Normal file
View File

@ -0,0 +1,6 @@
// Package plugin contains types and functions to help Terraform plugins
// implement the plugin rpc interface.
// The primary Provider type will be responsible for converting from the grpc
// wire protocol to the types and methods known to the provider
// implementations.
package plugin

View File

@ -0,0 +1,616 @@
package plugin
import (
"encoding/json"
"errors"
"strconv"
context "golang.org/x/net/context"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/plugin/proto"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/msgpack"
)
// GRPCProviderServer handles the server, or plugin side of the rpc connection.
type GRPCProviderServer struct {
provider *schema.Provider
}
func (s *GRPCProviderServer) GetSchema(_ context.Context, req *proto.GetProviderSchema_Request) (*proto.GetProviderSchema_Response, error) {
resp := &proto.GetProviderSchema_Response{
ResourceSchemas: make(map[string]*proto.Schema),
DataSourceSchemas: make(map[string]*proto.Schema),
}
resp.Provider = &proto.Schema{
Block: protoSchemaBlock(s.getProviderSchemaBlock()),
}
for typ, res := range s.provider.ResourcesMap {
resp.ResourceSchemas[typ] = &proto.Schema{
Version: int64(res.SchemaVersion),
Block: protoSchemaBlock(res.CoreConfigSchema()),
}
}
for typ, dat := range s.provider.DataSourcesMap {
resp.DataSourceSchemas[typ] = &proto.Schema{
Version: int64(dat.SchemaVersion),
Block: protoSchemaBlock(dat.CoreConfigSchema()),
}
}
return resp, nil
}
func (s *GRPCProviderServer) getProviderSchemaBlock() *configschema.Block {
return schema.InternalMap(s.provider.Schema).CoreConfigSchema()
}
func (s *GRPCProviderServer) getResourceSchemaBlock(name string) *configschema.Block {
res := s.provider.ResourcesMap[name]
return res.CoreConfigSchema()
}
func (s *GRPCProviderServer) getDatasourceSchemaBlock(name string) *configschema.Block {
dat := s.provider.DataSourcesMap[name]
return dat.CoreConfigSchema()
}
func (s *GRPCProviderServer) ValidateProviderConfig(_ context.Context, req *proto.ValidateProviderConfig_Request) (*proto.ValidateProviderConfig_Response, error) {
resp := &proto.ValidateProviderConfig_Response{}
block := s.getProviderSchemaBlock()
configVal, err := msgpack.Unmarshal(req.Config.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
config := terraform.NewResourceConfigShimmed(configVal, block)
warns, errs := s.provider.Validate(config)
resp.Diagnostics = appendDiag(resp.Diagnostics, diagsFromWarnsErrs(warns, errs))
return resp, nil
}
func (s *GRPCProviderServer) ValidateResourceTypeConfig(_ context.Context, req *proto.ValidateResourceTypeConfig_Request) (*proto.ValidateResourceTypeConfig_Response, error) {
resp := &proto.ValidateResourceTypeConfig_Response{}
block := s.getResourceSchemaBlock(req.TypeName)
configVal, err := msgpack.Unmarshal(req.Config.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
config := terraform.NewResourceConfigShimmed(configVal, block)
warns, errs := s.provider.ValidateResource(req.TypeName, config)
resp.Diagnostics = appendDiag(resp.Diagnostics, diagsFromWarnsErrs(warns, errs))
return resp, nil
}
func (s *GRPCProviderServer) ValidateDataSourceConfig(_ context.Context, req *proto.ValidateDataSourceConfig_Request) (*proto.ValidateDataSourceConfig_Response, error) {
resp := &proto.ValidateDataSourceConfig_Response{}
block := s.getDatasourceSchemaBlock(req.TypeName)
configVal, err := msgpack.Unmarshal(req.Config.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
config := terraform.NewResourceConfigShimmed(configVal, block)
warns, errs := s.provider.ValidateResource(req.TypeName, config)
resp.Diagnostics = appendDiag(resp.Diagnostics, diagsFromWarnsErrs(warns, errs))
return resp, nil
}
func (s *GRPCProviderServer) UpgradeResourceState(_ context.Context, req *proto.UpgradeResourceState_Request) (*proto.UpgradeResourceState_Response, error) {
resp := &proto.UpgradeResourceState_Response{}
res := s.provider.ResourcesMap[req.TypeName]
block := res.CoreConfigSchema()
version := int(req.Version)
var jsonMap map[string]interface{}
var err error
// if there's a JSON state, we need to decode it.
if req.RawState.Json != nil {
err = json.Unmarshal(req.RawState.Json, &jsonMap)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
}
// We first need to upgrade a flatmap state if it exists.
// There should never be both a JSON and Flatmap state in the request.
if req.RawState.Flatmap != nil {
jsonMap, version, err = s.upgradeFlatmapState(version, req.RawState.Flatmap, res)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
}
// complete the upgrade of the JSON states
jsonMap, err = s.upgradeJSONState(version, jsonMap, res)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
// now we need to turn the state into the default json representation, so
// that it can be re-decoded using the actual schema.
val, err := schema.JSONMapToStateValue(jsonMap, block)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
// encode the final state to the expected msgpack format
newStateMP, err := msgpack.Marshal(val, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
resp.UpgradedState = &proto.DynamicValue{Msgpack: newStateMP}
return resp, nil
}
// upgradeFlatmapState takes a legacy flatmap state, upgrades it using Migrate
// state if necessary, and converts it to the new JSON state format decoded as a
// map[string]interface{}.
// upgradeFlatmapState returns the json map along with the corresponding schema
// version.
func (s *GRPCProviderServer) upgradeFlatmapState(version int, m map[string]string, res *schema.Resource) (map[string]interface{}, int, error) {
// this will be the version we've upgraded so, defaulting to the given
// version in case no migration was called.
upgradedVersion := version
// first determine if we need to call the legacy MigrateState func
requiresMigrate := version < res.SchemaVersion
schemaType := res.CoreConfigSchema().ImpliedType()
// if there are any StateUpgraders, then we need to only compare
// against the first version there
if len(res.StateUpgraders) > 0 {
requiresMigrate = version < res.StateUpgraders[0].Version
}
if requiresMigrate {
if res.MigrateState == nil {
return nil, 0, errors.New("cannot upgrade state, missing MigrateState function")
}
is := &terraform.InstanceState{
ID: m["id"],
Attributes: m,
Meta: map[string]interface{}{
"schema_version": strconv.Itoa(version),
},
}
is, err := res.MigrateState(version, is, s.provider.Meta())
if err != nil {
return nil, 0, err
}
// re-assign the map in case there was a copy made, making sure to keep
// the ID
m := is.Attributes
m["id"] = is.ID
// if there are further upgraders, then we've only updated that far
if len(res.StateUpgraders) > 0 {
schemaType = res.StateUpgraders[0].Type
upgradedVersion = res.StateUpgraders[0].Version
}
} else {
// the schema version may be newer than the MigrateState functions
// handled and older than the current, but still stored in the flatmap
// form. If that's the case, we need to find the correct schema type to
// convert the state.
for _, upgrader := range res.StateUpgraders {
if upgrader.Version == version {
schemaType = upgrader.Type
break
}
}
}
// now we know the state is up to the latest version that handled the
// flatmap format state. Now we can upgrade the format and continue from
// there.
newConfigVal, err := hcl2shim.HCL2ValueFromFlatmap(m, schemaType)
if err != nil {
return nil, 0, err
}
jsonMap, err := schema.StateValueToJSONMap(newConfigVal, schemaType)
return jsonMap, upgradedVersion, err
}
func (s *GRPCProviderServer) upgradeJSONState(version int, m map[string]interface{}, res *schema.Resource) (map[string]interface{}, error) {
var err error
for _, upgrader := range res.StateUpgraders {
if version != upgrader.Version {
continue
}
m, err = upgrader.Upgrade(m, s.provider.Meta())
if err != nil {
return nil, err
}
version++
}
return m, nil
}
func (s *GRPCProviderServer) Stop(_ context.Context, _ *proto.Stop_Request) (*proto.Stop_Response, error) {
resp := &proto.Stop_Response{}
err := s.provider.Stop()
if err != nil {
resp.Error = err.Error()
}
return resp, nil
}
func (s *GRPCProviderServer) Configure(_ context.Context, req *proto.Configure_Request) (*proto.Configure_Response, error) {
resp := &proto.Configure_Response{}
block := s.getProviderSchemaBlock()
configVal, err := msgpack.Unmarshal(req.Config.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
config := terraform.NewResourceConfigShimmed(configVal, block)
err = s.provider.Configure(config)
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
func (s *GRPCProviderServer) ReadResource(_ context.Context, req *proto.ReadResource_Request) (*proto.ReadResource_Response, error) {
resp := &proto.ReadResource_Response{}
res := s.provider.ResourcesMap[req.TypeName]
block := res.CoreConfigSchema()
stateVal, err := msgpack.Unmarshal(req.CurrentState.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
instanceState := schema.InstanceStateFromStateValue(stateVal, res.SchemaVersion)
newInstanceState, err := res.RefreshWithoutUpgrade(instanceState, s.provider.Meta())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
// helper/schema should always copy the ID over, but do it again just to be safe
newInstanceState.Attributes["id"] = newInstanceState.ID
newConfigVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Attributes, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
newConfigMP, err := msgpack.Marshal(newConfigVal, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
resp.NewState = &proto.DynamicValue{
Msgpack: newConfigMP,
}
return resp, nil
}
func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) {
resp := &proto.PlanResourceChange_Response{}
res := s.provider.ResourcesMap[req.TypeName]
block := res.CoreConfigSchema()
priorStateVal, err := msgpack.Unmarshal(req.PriorState.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
proposedNewStateVal, err := msgpack.Unmarshal(req.ProposedNewState.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
info := &terraform.InstanceInfo{
Type: req.TypeName,
}
priorState := schema.InstanceStateFromStateValue(priorStateVal, res.SchemaVersion)
// turn the propsed state into a legacy configuration
config := terraform.NewResourceConfigShimmed(proposedNewStateVal, block)
diff, err := s.provider.Diff(info, priorState, config)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
// now we need to apply the diff to the prior state, so get the planned state
plannedStateVal, err := schema.ApplyDiff(priorStateVal, diff, block)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
plannedMP, err := msgpack.Marshal(plannedStateVal, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
resp.PlannedState.Msgpack = plannedMP
// the Meta field gets encoded into PlannedPrivate
plannedPrivate, err := json.Marshal(diff.Meta)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
resp.PlannedPrivate = plannedPrivate
// collect the attributes that require instance replacement, and convert
// them to cty.Paths.
var requiresNew []string
for attr, d := range diff.Attributes {
if d.RequiresNew {
requiresNew = append(requiresNew, attr)
}
}
requiresReplace, err := hcl2shim.RequiresReplace(requiresNew, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
// convert these to the protocol structures
for _, p := range requiresReplace {
resp.RequiresReplace = append(resp.RequiresReplace, pathToAttributePath(p))
}
return resp, nil
}
func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) {
resp := &proto.ApplyResourceChange_Response{}
res := s.provider.ResourcesMap[req.TypeName]
block := res.CoreConfigSchema()
priorStateVal, err := msgpack.Unmarshal(req.PriorState.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
plannedStateVal, err := msgpack.Unmarshal(req.PlannedState.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
info := &terraform.InstanceInfo{
Type: req.TypeName,
}
priorState := schema.InstanceStateFromStateValue(priorStateVal, res.SchemaVersion)
var private map[string]interface{}
if err := json.Unmarshal(req.PlannedPrivate, &private); err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
diff, err := schema.DiffFromValues(priorStateVal, plannedStateVal, res)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
newInstanceState, err := s.provider.Apply(info, priorState, diff)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
newStateVal, err := schema.StateValueFromInstanceState(newInstanceState, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
resp.NewState.Msgpack = newStateMP
meta, err := json.Marshal(newInstanceState.Meta)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
resp.Private = meta
return resp, nil
}
func (s *GRPCProviderServer) ImportResourceState(_ context.Context, req *proto.ImportResourceState_Request) (*proto.ImportResourceState_Response, error) {
resp := &proto.ImportResourceState_Response{}
block := s.getResourceSchemaBlock(req.TypeName)
info := &terraform.InstanceInfo{
Type: req.TypeName,
}
newInstanceStates, err := s.provider.ImportState(info, req.Id)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
for _, is := range newInstanceStates {
// copy the ID again just to be sure it wasn't missed
is.Attributes["id"] = is.ID
newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(is.Attributes, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
meta, err := json.Marshal(is.Meta)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
// the legacy implementation could only import one type at a time
importedResource := &proto.ImportResourceState_ImportedResource{
TypeName: req.TypeName,
State: &proto.DynamicValue{
Msgpack: newStateMP,
},
Private: meta,
}
resp.ImportedResources = append(resp.ImportedResources, importedResource)
}
return resp, nil
}
func (s *GRPCProviderServer) ReadDataSource(_ context.Context, req *proto.ReadDataSource_Request) (*proto.ReadDataSource_Response, error) {
resp := &proto.ReadDataSource_Response{}
res := s.provider.DataSourcesMap[req.TypeName]
block := res.CoreConfigSchema()
configVal, err := msgpack.Unmarshal(req.Config.Msgpack, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
info := &terraform.InstanceInfo{
Type: req.TypeName,
}
config := terraform.NewResourceConfigShimmed(configVal, block)
// we need to still build the diff separately with the Read method to match
// the old behavior
diff, err := s.provider.ReadDataDiff(info, config)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
// now we can get the new complete data source
newInstanceState, err := s.provider.ReadDataApply(info, diff)
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
newStateVal, err := schema.StateValueFromInstanceState(newInstanceState, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
resp.State.Msgpack = newStateMP
return resp, nil
}
func pathToAttributePath(path cty.Path) *proto.AttributePath {
var steps []*proto.AttributePath_Step
for _, step := range path {
switch s := step.(type) {
case cty.GetAttrStep:
steps = append(steps, &proto.AttributePath_Step{
Selector: &proto.AttributePath_Step_AttributeName{
AttributeName: s.Name,
},
})
case cty.IndexStep:
ty := s.Key.Type()
switch ty {
case cty.Number:
i, _ := s.Key.AsBigFloat().Int64()
steps = append(steps, &proto.AttributePath_Step{
Selector: &proto.AttributePath_Step_ElementKeyInt{
ElementKeyInt: i,
},
})
case cty.String:
steps = append(steps, &proto.AttributePath_Step{
Selector: &proto.AttributePath_Step_ElementKeyString{
ElementKeyString: s.Key.AsString(),
},
})
}
}
}
return &proto.AttributePath{Steps: steps}
}

View File

@ -0,0 +1,295 @@
package plugin
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/plugin/proto"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/msgpack"
)
// The GRPCProviderServer will directly implement the go protobuf server
var _ proto.ProviderServer = (*GRPCProviderServer)(nil)
var (
typeComparer = cmp.Comparer(cty.Type.Equals)
valueComparer = cmp.Comparer(cty.Value.RawEquals)
equateEmpty = cmpopts.EquateEmpty()
)
func TestUpgradeState_jsonState(t *testing.T) {
r := &schema.Resource{
SchemaVersion: 2,
Schema: map[string]*schema.Schema{
"two": {
Type: schema.TypeInt,
Optional: true,
},
},
}
r.StateUpgraders = []schema.StateUpgrader{
{
Version: 0,
Type: cty.Object(map[string]cty.Type{
"id": cty.String,
"zero": cty.Number,
}),
Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
_, ok := m["zero"].(float64)
if !ok {
return nil, fmt.Errorf("zero not found in %#v", m)
}
m["one"] = float64(1)
delete(m, "zero")
return m, nil
},
},
{
Version: 1,
Type: cty.Object(map[string]cty.Type{
"id": cty.String,
"one": cty.Number,
}),
Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
_, ok := m["one"].(float64)
if !ok {
return nil, fmt.Errorf("one not found in %#v", m)
}
m["two"] = float64(2)
delete(m, "one")
return m, nil
},
},
}
server := &GRPCProviderServer{
provider: &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"test": r,
},
},
}
req := &proto.UpgradeResourceState_Request{
TypeName: "test",
Version: 0,
RawState: &proto.RawState{
Json: []byte(`{"id":"bar","zero":0}`),
},
}
resp, err := server.UpgradeResourceState(nil, req)
if err != nil {
t.Fatal(err)
}
if len(resp.Diagnostics) > 0 {
for _, d := range resp.Diagnostics {
t.Errorf("%#v", d)
}
t.Fatal("error")
}
val, err := msgpack.Unmarshal(resp.UpgradedState.Msgpack, r.CoreConfigSchema().ImpliedType())
if err != nil {
t.Fatal(err)
}
expected := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("bar"),
"two": cty.NumberIntVal(2),
})
if !cmp.Equal(expected, val, valueComparer, equateEmpty) {
t.Fatal(cmp.Diff(expected, val, valueComparer, equateEmpty))
}
}
func TestUpgradeState_flatmapState(t *testing.T) {
r := &schema.Resource{
SchemaVersion: 4,
Schema: map[string]*schema.Schema{
"four": {
Type: schema.TypeInt,
Required: true,
},
},
// this MigrateState will take the state to version 2
MigrateState: func(v int, is *terraform.InstanceState, _ interface{}) (*terraform.InstanceState, error) {
switch v {
case 0:
_, ok := is.Attributes["zero"]
if !ok {
return nil, fmt.Errorf("zero not found in %#v", is.Attributes)
}
is.Attributes["one"] = "1"
delete(is.Attributes, "zero")
fallthrough
case 1:
_, ok := is.Attributes["one"]
if !ok {
return nil, fmt.Errorf("one not found in %#v", is.Attributes)
}
is.Attributes["two"] = "2"
delete(is.Attributes, "one")
default:
return nil, fmt.Errorf("invalid schema version %d", v)
}
return is, nil
},
}
r.StateUpgraders = []schema.StateUpgrader{
{
Version: 2,
Type: cty.Object(map[string]cty.Type{
"id": cty.String,
"two": cty.Number,
}),
Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
_, ok := m["two"].(float64)
if !ok {
return nil, fmt.Errorf("two not found in %#v", m)
}
m["three"] = float64(3)
delete(m, "two")
return m, nil
},
},
{
Version: 3,
Type: cty.Object(map[string]cty.Type{
"id": cty.String,
"three": cty.Number,
}),
Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
_, ok := m["three"].(float64)
if !ok {
return nil, fmt.Errorf("three not found in %#v", m)
}
m["four"] = float64(4)
delete(m, "three")
return m, nil
},
},
}
server := &GRPCProviderServer{
provider: &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"test": r,
},
},
}
testReqs := []*proto.UpgradeResourceState_Request{
{
TypeName: "test",
Version: 0,
RawState: &proto.RawState{
Flatmap: map[string]string{
"id": "bar",
"zero": "0",
},
},
},
{
TypeName: "test",
Version: 1,
RawState: &proto.RawState{
Flatmap: map[string]string{
"id": "bar",
"one": "1",
},
},
},
// two and up could be stored in flatmap or json states
{
TypeName: "test",
Version: 2,
RawState: &proto.RawState{
Flatmap: map[string]string{
"id": "bar",
"two": "2",
},
},
},
{
TypeName: "test",
Version: 2,
RawState: &proto.RawState{
Json: []byte(`{"id":"bar","two":2}`),
},
},
{
TypeName: "test",
Version: 3,
RawState: &proto.RawState{
Flatmap: map[string]string{
"id": "bar",
"three": "3",
},
},
},
{
TypeName: "test",
Version: 3,
RawState: &proto.RawState{
Json: []byte(`{"id":"bar","three":3}`),
},
},
{
TypeName: "test",
Version: 4,
RawState: &proto.RawState{
Flatmap: map[string]string{
"id": "bar",
"four": "4",
},
},
},
{
TypeName: "test",
Version: 4,
RawState: &proto.RawState{
Json: []byte(`{"id":"bar","four":4}`),
},
},
}
for i, req := range testReqs {
t.Run(fmt.Sprintf("%d-%d", i, req.Version), func(t *testing.T) {
resp, err := server.UpgradeResourceState(nil, req)
if err != nil {
t.Fatal(err)
}
if len(resp.Diagnostics) > 0 {
for _, d := range resp.Diagnostics {
t.Errorf("%#v", d)
}
t.Fatal("error")
}
val, err := msgpack.Unmarshal(resp.UpgradedState.Msgpack, r.CoreConfigSchema().ImpliedType())
if err != nil {
t.Fatal(err)
}
expected := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("bar"),
"four": cty.NumberIntVal(4),
})
if !cmp.Equal(expected, val, valueComparer, equateEmpty) {
t.Fatal(cmp.Diff(expected, val, valueComparer, equateEmpty))
}
})
}
}

View File

@ -0,0 +1,132 @@
package plugin
import (
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/plugin/proto"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/msgpack"
context "golang.org/x/net/context"
)
type GRPCProvisionerServer struct {
provisioner *schema.Provisioner
}
func (s *GRPCProvisionerServer) GetSchema(_ context.Context, req *proto.GetProvisionerSchema_Request) (*proto.GetProvisionerSchema_Response, error) {
resp := &proto.GetProvisionerSchema_Response{}
resp.Provisioner = &proto.Schema{
Block: protoSchemaBlock(schema.InternalMap(s.provisioner.Schema).CoreConfigSchema()),
}
return resp, nil
}
func (s *GRPCProvisionerServer) ValidateProvisionerConfig(_ context.Context, req *proto.ValidateProvisionerConfig_Request) (*proto.ValidateProvisionerConfig_Response, error) {
resp := &proto.ValidateProvisionerConfig_Response{}
cfgSchema := schema.InternalMap(s.provisioner.Schema).CoreConfigSchema()
configVal, err := msgpack.Unmarshal(req.Config.Msgpack, cfgSchema.ImpliedType())
if err != nil {
resp.Diagnostics = appendDiag(resp.Diagnostics, err)
return resp, nil
}
config := terraform.NewResourceConfigShimmed(configVal, cfgSchema)
warns, errs := s.provisioner.Validate(config)
resp.Diagnostics = appendDiag(resp.Diagnostics, diagsFromWarnsErrs(warns, errs))
return resp, nil
}
// stringMapFromValue converts a cty.Value to a map[stirng]string.
// This will panic if the val is not a cty.Map(cty.String).
func stringMapFromValue(val cty.Value) map[string]string {
m := map[string]string{}
if val.IsNull() || !val.IsKnown() {
return m
}
for it := val.ElementIterator(); it.Next(); {
ak, av := it.Element()
name := ak.AsString()
if !av.IsKnown() || av.IsNull() {
continue
}
av, _ = convert.Convert(av, cty.String)
m[name] = av.AsString()
}
return m
}
// uiOutput implements the terraform.UIOutput interface to adapt the grpc
// stream to the legacy Provisioner.Apply method.
type uiOutput struct {
srv proto.Provisioner_ProvisionResourceServer
}
func (o uiOutput) Output(s string) {
err := o.srv.Send(&proto.ProvisionResource_Response{
Output: s,
})
if err != nil {
log.Printf("[ERROR] %s", err)
}
}
func (s *GRPCProvisionerServer) ProvisionResource(req *proto.ProvisionResource_Request, srv proto.Provisioner_ProvisionResourceServer) error {
// We send back a diagnostics over the stream if there was a
// provisioner-side problem.
srvResp := &proto.ProvisionResource_Response{}
cfgSchema := schema.InternalMap(s.provisioner.Schema).CoreConfigSchema()
cfgVal, err := msgpack.Unmarshal(req.Config.Msgpack, cfgSchema.ImpliedType())
if err != nil {
srvResp.Diagnostics = appendDiag(srvResp.Diagnostics, err)
srv.Send(srvResp)
return nil
}
resourceConfig := terraform.NewResourceConfigShimmed(cfgVal, cfgSchema)
connVal, err := msgpack.Unmarshal(req.Connection.Msgpack, cty.Map(cty.String))
if err != nil {
srvResp.Diagnostics = appendDiag(srvResp.Diagnostics, err)
srv.Send(srvResp)
return nil
}
conn := stringMapFromValue(connVal)
instanceState := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: conn,
},
}
err = s.provisioner.Apply(uiOutput{srv}, instanceState, resourceConfig)
if err != nil {
srvResp.Diagnostics = appendDiag(srvResp.Diagnostics, err)
srv.Send(srvResp)
}
return nil
}
func (s *GRPCProvisionerServer) Stop(_ context.Context, req *proto.Stop_Request) (*proto.Stop_Response, error) {
resp := &proto.Stop_Response{}
err := s.provisioner.Stop()
if err != nil {
resp.Error = err.Error()
}
return resp, nil
}

View File

@ -0,0 +1,5 @@
package plugin
import "github.com/hashicorp/terraform/plugin/proto"
var _ proto.ProvisionerServer = (*GRPCProvisionerServer)(nil)

70
helper/plugin/schema.go Normal file
View File

@ -0,0 +1,70 @@
package plugin
import (
"encoding/json"
"reflect"
"sort"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/plugin/proto"
)
// protoSchemaBlock takes a *configschema.Block and converts it to a
// proto.Schema_Block for a grpc response.
func protoSchemaBlock(b *configschema.Block) *proto.Schema_Block {
block := &proto.Schema_Block{}
for _, name := range sortedKeys(b.Attributes) {
a := b.Attributes[name]
attr := &proto.Schema_Attribute{
Name: name,
Description: a.Description,
Optional: a.Optional,
Computed: a.Computed,
Required: a.Required,
Sensitive: a.Sensitive,
}
ty, err := json.Marshal(a.Type)
if err != nil {
panic(err)
}
attr.Type = ty
block.Attributes = append(block.Attributes, attr)
}
for _, name := range sortedKeys(b.BlockTypes) {
b := b.BlockTypes[name]
block.BlockTypes = append(block.BlockTypes, protoSchemaNestedBlock(name, b))
}
return block
}
func protoSchemaNestedBlock(name string, b *configschema.NestedBlock) *proto.Schema_NestedBlock {
return &proto.Schema_NestedBlock{
TypeName: name,
Block: protoSchemaBlock(&b.Block),
Nesting: proto.Schema_NestedBlock_NestingMode(b.Nesting),
MinItems: int64(b.MinItems),
MaxItems: int64(b.MaxItems),
}
}
// sortedKeys returns the lexically sorted keys from the given map. This is
// used to make schema conversions are deterministic. This panics if map keys
// are not a string.
func sortedKeys(m interface{}) []string {
v := reflect.ValueOf(m)
keys := make([]string, v.Len())
mapKeys := v.MapKeys()
for i, k := range mapKeys {
keys[i] = k.Interface().(string)
}
sort.Strings(keys)
return keys
}

View File

@ -0,0 +1,516 @@
package plugin
import (
"errors"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/plugin/proto"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/msgpack"
)
// the TestProvider functions have been adapted from the helper/schema fixtures
func TestProviderGetSchema(t *testing.T) {
p := &schema.Provider{
Schema: map[string]*schema.Schema{
"bar": {
Type: schema.TypeString,
Required: true,
},
},
ResourcesMap: map[string]*schema.Resource{
"foo": &schema.Resource{
SchemaVersion: 1,
Schema: map[string]*schema.Schema{
"bar": {
Type: schema.TypeString,
Required: true,
},
},
},
},
DataSourcesMap: map[string]*schema.Resource{
"baz": &schema.Resource{
SchemaVersion: 2,
Schema: map[string]*schema.Schema{
"bur": {
Type: schema.TypeString,
Required: true,
},
},
},
},
}
want := providers.GetSchemaResponse{
Provider: providers.Schema{
Version: 0,
Block: schema.InternalMap(p.Schema).CoreConfigSchema(),
},
ResourceTypes: map[string]providers.Schema{
"foo": {
Version: 1,
Block: p.ResourcesMap["foo"].CoreConfigSchema(),
},
},
DataSources: map[string]providers.Schema{
"baz": {
Version: 2,
Block: p.DataSourcesMap["baz"].CoreConfigSchema(),
},
},
}
provider := &GRPCProviderServer{
provider: p,
}
resp, err := provider.GetSchema(nil, nil)
if err != nil {
t.Fatalf("unexpected error %s", err)
}
diags := plugin.ProtoToDiagnostics(resp.Diagnostics)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
schemaResp := providers.GetSchemaResponse{
Provider: plugin.ProtoToProviderSchema(resp.Provider),
ResourceTypes: map[string]providers.Schema{
"foo": plugin.ProtoToProviderSchema(resp.ResourceSchemas["foo"]),
},
DataSources: map[string]providers.Schema{
"baz": plugin.ProtoToProviderSchema(resp.DataSourceSchemas["baz"]),
},
}
if !cmp.Equal(schemaResp, want, equateEmpty, typeComparer) {
t.Error("wrong result:\n", cmp.Diff(schemaResp, want, equateEmpty, typeComparer))
}
}
func TestProviderValidate(t *testing.T) {
cases := []struct {
Name string
P *schema.Provider
Err bool
Warn bool
}{
{
Name: "warning",
P: &schema.Provider{
Schema: map[string]*schema.Schema{
"foo": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: func(_ interface{}, _ string) ([]string, []error) {
return []string{"warning"}, nil
},
},
},
},
Warn: true,
},
{
Name: "error",
P: &schema.Provider{
Schema: map[string]*schema.Schema{
"foo": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: func(_ interface{}, _ string) ([]string, []error) {
return nil, []error{errors.New("error")}
},
},
},
},
Err: true,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
provider := &GRPCProviderServer{
provider: tc.P,
}
cfgSchema := schema.InternalMap(tc.P.Schema).CoreConfigSchema()
val := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"foo": "bar"})
val, err := cfgSchema.CoerceValue(val)
if err != nil {
t.Fatal(err)
}
mp, err := msgpack.Marshal(val, cfgSchema.ImpliedType())
if err != nil {
t.Fatal(err)
}
req := &proto.ValidateProviderConfig_Request{
Config: &proto.DynamicValue{Msgpack: mp},
}
resp, err := provider.ValidateProviderConfig(nil, req)
if err != nil {
t.Fatal(err)
}
diags := plugin.ProtoToDiagnostics(resp.Diagnostics)
var warn tfdiags.Diagnostic
for _, d := range diags {
if d.Severity() == tfdiags.Warning {
warn = d
}
}
switch {
case tc.Err:
if !diags.HasErrors() {
t.Fatal("expected error")
}
case !tc.Err:
if diags.HasErrors() {
t.Fatal(diags.Err())
}
case tc.Warn:
if warn == nil {
t.Fatal("expected warning")
}
case !tc.Warn:
if warn != nil {
t.Fatal("unexpected warning", warn)
}
}
})
}
}
func TestProviderValidateResource(t *testing.T) {
cases := []struct {
Name string
P *schema.Provider
Type string
Config map[string]interface{}
Err bool
Warn bool
}{
{
Name: "error",
P: &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"foo": &schema.Resource{
Schema: map[string]*schema.Schema{
"attr": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: func(_ interface{}, _ string) ([]string, []error) {
return nil, []error{errors.New("warn")}
},
},
},
},
},
},
Type: "foo",
Err: true,
},
{
Name: "ok",
P: &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"foo": &schema.Resource{
Schema: map[string]*schema.Schema{
"attr": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
},
},
Config: map[string]interface{}{"attr": "bar"},
Type: "foo",
},
{
Name: "warn",
P: &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"foo": &schema.Resource{
Schema: map[string]*schema.Schema{
"attr": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: func(_ interface{}, _ string) ([]string, []error) {
return []string{"warn"}, nil
},
},
},
},
},
},
Type: "foo",
Config: map[string]interface{}{"attr": "bar"},
Err: false,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
provider := &GRPCProviderServer{
provider: tc.P,
}
cfgSchema := tc.P.ResourcesMap[tc.Type].CoreConfigSchema()
val := hcl2shim.HCL2ValueFromConfigValue(tc.Config)
val, err := cfgSchema.CoerceValue(val)
if err != nil {
t.Fatal(err)
}
mp, err := msgpack.Marshal(val, cfgSchema.ImpliedType())
if err != nil {
t.Fatal(err)
}
req := &proto.ValidateResourceTypeConfig_Request{
TypeName: tc.Type,
Config: &proto.DynamicValue{Msgpack: mp},
}
resp, err := provider.ValidateResourceTypeConfig(nil, req)
if err != nil {
t.Fatal(err)
}
diags := plugin.ProtoToDiagnostics(resp.Diagnostics)
var warn tfdiags.Diagnostic
for _, d := range diags {
if d.Severity() == tfdiags.Warning {
warn = d
}
}
switch {
case tc.Err:
if !diags.HasErrors() {
t.Fatal("expected error")
}
case !tc.Err:
if diags.HasErrors() {
t.Fatal(diags.Err())
}
case tc.Warn:
if warn == nil {
t.Fatal("expected warning")
}
case !tc.Warn:
if warn != nil {
t.Fatal("unexpected warning", warn)
}
}
})
}
}
func TestProviderImportState_default(t *testing.T) {
p := &GRPCProviderServer{
provider: &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"foo": &schema.Resource{
Importer: &schema.ResourceImporter{},
},
},
},
}
req := &proto.ImportResourceState_Request{
TypeName: "foo",
Id: "bar",
}
resp, err := p.ImportResourceState(nil, req)
if err != nil {
t.Fatal(err)
}
diags := plugin.ProtoToDiagnostics(resp.Diagnostics)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
if len(resp.ImportedResources) != 1 {
t.Fatalf("expected 1 import, git %#v", resp.ImportedResources)
}
}
func TestProviderImportState_setsId(t *testing.T) {
var val string
stateFunc := func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
val = d.Id()
return []*schema.ResourceData{d}, nil
}
p := &GRPCProviderServer{
provider: &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"foo": &schema.Resource{
Importer: &schema.ResourceImporter{
State: stateFunc,
},
},
},
},
}
req := &proto.ImportResourceState_Request{
TypeName: "foo",
Id: "bar",
}
resp, err := p.ImportResourceState(nil, req)
if err != nil {
t.Fatal(err)
}
diags := plugin.ProtoToDiagnostics(resp.Diagnostics)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
if len(resp.ImportedResources) != 1 {
t.Fatalf("expected 1 import, git %#v", resp.ImportedResources)
}
if val != "bar" {
t.Fatal("should set id")
}
}
func TestProviderImportState_setsType(t *testing.T) {
var tVal string
stateFunc := func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
d.SetId("foo")
tVal = d.State().Ephemeral.Type
return []*schema.ResourceData{d}, nil
}
p := &GRPCProviderServer{
provider: &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"foo": &schema.Resource{
Importer: &schema.ResourceImporter{
State: stateFunc,
},
},
},
},
}
req := &proto.ImportResourceState_Request{
TypeName: "foo",
Id: "bar",
}
resp, err := p.ImportResourceState(nil, req)
if err != nil {
t.Fatal(err)
}
diags := plugin.ProtoToDiagnostics(resp.Diagnostics)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
if tVal != "foo" {
t.Fatal("should set type")
}
}
func TestProviderStop(t *testing.T) {
var p schema.Provider
if p.Stopped() {
t.Fatal("should not be stopped")
}
// Verify stopch blocks
ch := p.StopContext().Done()
select {
case <-ch:
t.Fatal("should not be stopped")
case <-time.After(10 * time.Millisecond):
}
provider := &GRPCProviderServer{
provider: &p,
}
// Stop it
if _, err := provider.Stop(nil, &proto.Stop_Request{}); err != nil {
t.Fatal(err)
}
// Verify
if !p.Stopped() {
t.Fatal("should be stopped")
}
select {
case <-ch:
case <-time.After(10 * time.Millisecond):
t.Fatal("should be stopped")
}
}
func TestProviderStop_stopFirst(t *testing.T) {
var p schema.Provider
provider := &GRPCProviderServer{
provider: &p,
}
// Stop it
_, err := provider.Stop(nil, &proto.Stop_Request{})
if err != nil {
t.Fatal(err)
}
// Verify
if !p.Stopped() {
t.Fatal("should be stopped")
}
select {
case <-p.StopContext().Done():
case <-time.After(10 * time.Millisecond):
t.Fatal("should be stopped")
}
}
// add the implicit "id" attribute for test resources
func testResource(block *configschema.Block) *configschema.Block {
if block.Attributes == nil {
block.Attributes = make(map[string]*configschema.Attribute)
}
if block.BlockTypes == nil {
block.BlockTypes = make(map[string]*configschema.NestedBlock)
}
if block.Attributes["id"] == nil {
block.Attributes["id"] = &configschema.Attribute{
Type: cty.String,
Optional: true,
Computed: true,
}
}
return block
}

View File

@ -0,0 +1,338 @@
package plugin
import (
"context"
"fmt"
"reflect"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/plugin/proto"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/msgpack"
mockproto "github.com/hashicorp/terraform/plugin/mock_proto"
)
// TestProvisioner functions in this file have been adapted from the
// helper/schema tests.
func noopApply(ctx context.Context) error {
return nil
}
func TestProvisionerValidate(t *testing.T) {
cases := []struct {
Name string
P *schema.Provisioner
Config map[string]interface{}
Err bool
Warns []string
}{
{
Name: "No ApplyFunc",
P: &schema.Provisioner{},
Config: map[string]interface{}{},
Err: true,
},
{
"Basic required field set",
&schema.Provisioner{
Schema: map[string]*schema.Schema{
"foo": &schema.Schema{
Required: true,
Type: schema.TypeString,
},
},
ApplyFunc: noopApply,
},
map[string]interface{}{
"foo": "bar",
},
false,
nil,
},
{
Name: "Warning from property validation",
P: &schema.Provisioner{
Schema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
ws = append(ws, "Simple warning from property validation")
return
},
},
},
ApplyFunc: noopApply,
},
Config: map[string]interface{}{
"foo": "",
},
Err: false,
Warns: []string{"Simple warning from property validation"},
},
{
Name: "No schema",
P: &schema.Provisioner{
Schema: nil,
ApplyFunc: noopApply,
},
Config: map[string]interface{}{},
Err: false,
},
{
Name: "Warning from provisioner ValidateFunc",
P: &schema.Provisioner{
Schema: nil,
ApplyFunc: noopApply,
ValidateFunc: func(*terraform.ResourceConfig) (ws []string, errors []error) {
ws = append(ws, "Simple warning from provisioner ValidateFunc")
return
},
},
Config: map[string]interface{}{},
Err: false,
Warns: []string{"Simple warning from provisioner ValidateFunc"},
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
p := &GRPCProvisionerServer{
provisioner: tc.P,
}
cfgSchema := schema.InternalMap(tc.P.Schema).CoreConfigSchema()
val := hcl2shim.HCL2ValueFromConfigValue(tc.Config)
val, err := cfgSchema.CoerceValue(val)
if err != nil {
t.Fatal(err)
}
mp, err := msgpack.Marshal(val, cfgSchema.ImpliedType())
if err != nil {
t.Fatal(err)
}
req := &proto.ValidateProvisionerConfig_Request{
Config: &proto.DynamicValue{Msgpack: mp},
}
resp, err := p.ValidateProvisionerConfig(nil, req)
if err != nil {
t.Fatal(err)
}
diags := plugin.ProtoToDiagnostics(resp.Diagnostics)
if diags.HasErrors() != tc.Err {
t.Fatal(diags.Err())
}
var ws []string
for _, d := range diags {
if d.Severity() == tfdiags.Warning {
ws = append(ws, d.Description().Summary)
}
}
if (tc.Warns != nil || len(ws) != 0) && !reflect.DeepEqual(ws, tc.Warns) {
t.Fatalf("%d: warnings mismatch, actual: %#v", i, ws)
}
})
}
}
func TestProvisionerApply(t *testing.T) {
cases := []struct {
Name string
P *schema.Provisioner
Conn map[string]interface{}
Config map[string]interface{}
Err bool
}{
{
Name: "Basic config",
P: &schema.Provisioner{
ConnSchema: map[string]*schema.Schema{
"foo": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
Schema: map[string]*schema.Schema{
"foo": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
},
ApplyFunc: func(ctx context.Context) error {
cd := ctx.Value(schema.ProvConnDataKey).(*schema.ResourceData)
d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
if d.Get("foo").(int) != 42 {
return fmt.Errorf("bad config data")
}
if cd.Get("foo").(string) != "bar" {
return fmt.Errorf("bad conn data")
}
return nil
},
},
Conn: map[string]interface{}{
"foo": "bar",
},
Config: map[string]interface{}{
"foo": 42,
},
Err: false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
p := &GRPCProvisionerServer{
provisioner: tc.P,
}
cfgSchema := schema.InternalMap(tc.P.Schema).CoreConfigSchema()
val := hcl2shim.HCL2ValueFromConfigValue(tc.Config)
val, err := cfgSchema.CoerceValue(val)
if err != nil {
t.Fatal(err)
}
cfgMP, err := msgpack.Marshal(val, cfgSchema.ImpliedType())
if err != nil {
t.Fatal(err)
}
connVal := hcl2shim.HCL2ValueFromConfigValue(tc.Conn)
connMP, err := msgpack.Marshal(connVal, cty.Map(cty.String))
if err != nil {
t.Fatal(err)
}
req := &proto.ProvisionResource_Request{
Config: &proto.DynamicValue{Msgpack: cfgMP},
Connection: &proto.DynamicValue{Msgpack: connMP},
}
ctrl := gomock.NewController(t)
srv := mockproto.NewMockProvisioner_ProvisionResourceServer(ctrl)
srv.EXPECT().Send(gomock.Any()).Return(nil)
err = p.ProvisionResource(req, srv)
if err != nil && !tc.Err {
t.Fatal(err)
}
})
}
}
func TestProvisionerStop(t *testing.T) {
p := &GRPCProvisionerServer{
provisioner: &schema.Provisioner{},
}
// Verify stopch blocks
ch := p.provisioner.StopContext().Done()
select {
case <-ch:
t.Fatal("should not be stopped")
case <-time.After(10 * time.Millisecond):
}
// Stop it
resp, err := p.Stop(nil, nil)
if err != nil {
t.Fatal(err)
}
if resp.Error != "" {
t.Fatal(resp.Error)
}
select {
case <-ch:
case <-time.After(10 * time.Millisecond):
t.Fatal("should be stopped")
}
}
func TestProvisionerStop_apply(t *testing.T) {
p := &schema.Provisioner{
ConnSchema: map[string]*schema.Schema{
"foo": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
Schema: map[string]*schema.Schema{
"foo": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
},
ApplyFunc: func(ctx context.Context) error {
<-ctx.Done()
return nil
},
}
s := &GRPCProvisionerServer{
provisioner: p,
}
srv := mockproto.NewMockProvisioner_ProvisionResourceServer(gomock.NewController(t))
srv.EXPECT().Send(gomock.Any()).Return(nil)
// Run the apply in a goroutine
doneCh := make(chan struct{})
go func() {
req := &proto.ProvisionResource_Request{
Config: &proto.DynamicValue{Msgpack: []byte("\201\243foo*")},
Connection: &proto.DynamicValue{Msgpack: []byte("\201\243foo\243bar")},
}
err := s.ProvisionResource(req, srv)
if err != nil {
t.Fatal(err)
}
close(doneCh)
}()
// Should block
select {
case <-doneCh:
t.Fatal("should not be done")
case <-time.After(10 * time.Millisecond):
}
resp, err := s.Stop(nil, nil)
if err != nil {
t.Fatal(err)
}
if resp.Error != "" {
t.Fatal(resp.Error)
}
select {
case <-doneCh:
case <-time.After(10 * time.Millisecond):
t.Fatal("should be done")
}
}

View File

@ -0,0 +1,182 @@
package plugin
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/plugin/proto"
"github.com/zclconf/go-cty/cty"
)
// Test that we can convert configschema to protobuf types and back again.
func TestConvertSchemaBlocks(t *testing.T) {
tests := map[string]struct {
Want *proto.Schema_Block
Block *configschema.Block
}{
"attributes": {
&proto.Schema_Block{
Attributes: []*proto.Schema_Attribute{
{
Name: "computed",
Type: []byte(`["list","bool"]`),
Computed: true,
},
{
Name: "optional",
Type: []byte(`"string"`),
Optional: true,
},
{
Name: "optional_computed",
Type: []byte(`["map","bool"]`),
Optional: true,
Computed: true,
},
{
Name: "required",
Type: []byte(`"number"`),
Required: true,
},
},
},
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"computed": {
Type: cty.List(cty.Bool),
Computed: true,
},
"optional": {
Type: cty.String,
Optional: true,
},
"optional_computed": {
Type: cty.Map(cty.Bool),
Optional: true,
Computed: true,
},
"required": {
Type: cty.Number,
Required: true,
},
},
},
},
"blocks": {
&proto.Schema_Block{
BlockTypes: []*proto.Schema_NestedBlock{
{
TypeName: "list",
Nesting: proto.Schema_NestedBlock_LIST,
Block: &proto.Schema_Block{},
},
{
TypeName: "map",
Nesting: proto.Schema_NestedBlock_MAP,
Block: &proto.Schema_Block{},
},
{
TypeName: "set",
Nesting: proto.Schema_NestedBlock_SET,
Block: &proto.Schema_Block{},
},
{
TypeName: "single",
Nesting: proto.Schema_NestedBlock_SINGLE,
Block: &proto.Schema_Block{
Attributes: []*proto.Schema_Attribute{
{
Name: "foo",
Type: []byte(`"dynamic"`),
Required: true,
},
},
},
},
},
},
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"list": &configschema.NestedBlock{
Nesting: configschema.NestingList,
},
"map": &configschema.NestedBlock{
Nesting: configschema.NestingMap,
},
"set": &configschema.NestedBlock{
Nesting: configschema.NestingSet,
},
"single": &configschema.NestedBlock{
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.DynamicPseudoType,
Required: true,
},
},
},
},
},
},
},
"deep block nesting": {
&proto.Schema_Block{
BlockTypes: []*proto.Schema_NestedBlock{
{
TypeName: "single",
Nesting: proto.Schema_NestedBlock_SINGLE,
Block: &proto.Schema_Block{
BlockTypes: []*proto.Schema_NestedBlock{
{
TypeName: "list",
Nesting: proto.Schema_NestedBlock_LIST,
Block: &proto.Schema_Block{
BlockTypes: []*proto.Schema_NestedBlock{
{
TypeName: "set",
Nesting: proto.Schema_NestedBlock_SET,
Block: &proto.Schema_Block{},
},
},
},
},
},
},
},
},
},
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"single": &configschema.NestedBlock{
Nesting: configschema.NestingSingle,
Block: configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"list": &configschema.NestedBlock{
Nesting: configschema.NestingList,
Block: configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"set": &configschema.NestedBlock{
Nesting: configschema.NestingSet,
},
},
},
},
},
},
},
},
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
converted := protoSchemaBlock(tc.Block)
if !cmp.Equal(converted, tc.Want, typeComparer, equateEmpty) {
t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, equateEmpty))
}
})
}
}