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:
parent
1ec792cabc
commit
63dcdbe948
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package plugin
|
||||
|
||||
import "github.com/hashicorp/terraform/plugin/proto"
|
||||
|
||||
var _ proto.ProvisionerServer = (*GRPCProvisionerServer)(nil)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue