Skip to content

Commit

Permalink
core: terraform.ResourceProvider.GetSchema method
Browse files Browse the repository at this point in the history
In order to parse provider, resource and data source configuration from
HCL2 config files, we need to know the relevant configuration schema.
This new method allows Terraform Core to request these from a provider.

This is a breaking change to this interface, so all of its implementers
in this package are updated too. This includes concrete implementations
of the new method in helper/schema that use the schema conversion code
added in an earlier commit to produce a configschema.Block automatically.

Plugins compiled against prior versions of helper/schema will not have
support for this method, and so calls to them will fail. Callers of
this new method will therefore need to sniff for support using the
SchemaAvailable field added to both ResourceType and DataSource.

This careful handling will need to persist until next time we increment
the plugin protocol version, at which point we can make the breaking
change of requiring this information to be available.
  • Loading branch information
apparentlymart committed Oct 17, 2017
1 parent ccb328c commit 183833a
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 10 deletions.
5 changes: 4 additions & 1 deletion helper/schema/core_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import (
// panic or produce an invalid result if given an invalid schemaMap.
func (m schemaMap) CoreConfigSchema() *configschema.Block {
if len(m) == 0 {
return nil
// We return an actual (empty) object here, rather than a nil,
// because a nil result would mean that we don't have a schema at
// all, rather than that we have an empty one.
return &configschema.Block{}
}

ret := &configschema.Block{
Expand Down
2 changes: 1 addition & 1 deletion helper/schema/core_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestSchemaMapCoreConfigSchema(t *testing.T) {
}{
"empty": {
map[string]*Schema{},
nil,
&configschema.Block{},
},
"primitives": {
map[string]*Schema{
Expand Down
32 changes: 32 additions & 0 deletions helper/schema/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/configschema"
"github.com/hashicorp/terraform/terraform"
)

Expand Down Expand Up @@ -185,6 +186,29 @@ func (p *Provider) TestReset() error {
return nil
}

// GetSchema implementation of terraform.ResourceProvider interface
func (p *Provider) GetSchema(req *terraform.ProviderSchemaRequest) (*terraform.ProviderSchema, error) {
resourceTypes := map[string]*configschema.Block{}
dataSources := map[string]*configschema.Block{}

for _, name := range req.ResourceTypes {
if r, exists := p.ResourcesMap[name]; exists {
resourceTypes[name] = r.CoreConfigSchema()
}
}
for _, name := range req.DataSources {
if r, exists := p.DataSourcesMap[name]; exists {
dataSources[name] = r.CoreConfigSchema()
}
}

return &terraform.ProviderSchema{
Provider: schemaMap(p.Schema).CoreConfigSchema(),
ResourceTypes: resourceTypes,
DataSources: dataSources,
}, nil
}

// Input implementation of terraform.ResourceProvider interface.
func (p *Provider) Input(
input terraform.UIInput,
Expand Down Expand Up @@ -305,6 +329,10 @@ func (p *Provider) Resources() []terraform.ResourceType {
result = append(result, terraform.ResourceType{
Name: k,
Importable: resource.Importer != nil,

// Indicates that a provider is compiled against a new enough
// version of core to support the GetSchema method.
SchemaAvailable: true,
})
}

Expand Down Expand Up @@ -410,6 +438,10 @@ func (p *Provider) DataSources() []terraform.DataSource {
for _, k := range keys {
result = append(result, terraform.DataSource{
Name: k,

// Indicates that a provider is compiled against a new enough
// version of core to support the GetSchema method.
SchemaAvailable: true,
})
}

Expand Down
96 changes: 89 additions & 7 deletions helper/schema/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,96 @@ import (
"testing"
"time"

"github.com/davecgh/go-spew/spew"
"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/configschema"
"github.com/hashicorp/terraform/terraform"
)

func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = new(Provider)
}

func TestProviderGetSchema(t *testing.T) {
// This functionality is already broadly tested in core_schema_test.go,
// so this is just to ensure that the call passes through correctly.
p := &Provider{
Schema: map[string]*Schema{
"bar": {
Type: TypeString,
Required: true,
},
},
ResourcesMap: map[string]*Resource{
"foo": &Resource{
Schema: map[string]*Schema{
"bar": {
Type: TypeString,
Required: true,
},
},
},
},
DataSourcesMap: map[string]*Resource{
"baz": &Resource{
Schema: map[string]*Schema{
"bur": {
Type: TypeString,
Required: true,
},
},
},
},
}

want := &terraform.ProviderSchema{
Provider: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": &configschema.Attribute{
Type: cty.String,
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
},
ResourceTypes: map[string]*configschema.Block{
"foo": &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": &configschema.Attribute{
Type: cty.String,
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
},
},
DataSources: map[string]*configschema.Block{
"baz": &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bur": &configschema.Attribute{
Type: cty.String,
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
},
},
}
got, err := p.GetSchema(&terraform.ProviderSchemaRequest{
ResourceTypes: []string{"foo", "bar"},
DataSources: []string{"baz", "bar"},
})
if err != nil {
t.Fatalf("unexpected error %s", err)
}

if !reflect.DeepEqual(got, want) {
t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want))
}
}

func TestProviderConfigure(t *testing.T) {
cases := []struct {
P *Provider
Expand Down Expand Up @@ -104,8 +186,8 @@ func TestProviderResources(t *testing.T) {
},
},
Result: []terraform.ResourceType{
terraform.ResourceType{Name: "bar"},
terraform.ResourceType{Name: "foo"},
terraform.ResourceType{Name: "bar", SchemaAvailable: true},
terraform.ResourceType{Name: "foo", SchemaAvailable: true},
},
},

Expand All @@ -118,9 +200,9 @@ func TestProviderResources(t *testing.T) {
},
},
Result: []terraform.ResourceType{
terraform.ResourceType{Name: "bar", Importable: true},
terraform.ResourceType{Name: "baz"},
terraform.ResourceType{Name: "foo"},
terraform.ResourceType{Name: "bar", Importable: true, SchemaAvailable: true},
terraform.ResourceType{Name: "baz", SchemaAvailable: true},
terraform.ResourceType{Name: "foo", SchemaAvailable: true},
},
},
}
Expand Down Expand Up @@ -151,8 +233,8 @@ func TestProviderDataSources(t *testing.T) {
},
},
Result: []terraform.DataSource{
terraform.DataSource{Name: "bar"},
terraform.DataSource{Name: "foo"},
terraform.DataSource{Name: "bar", SchemaAvailable: true},
terraform.DataSource{Name: "foo", SchemaAvailable: true},
},
},
}
Expand Down
39 changes: 39 additions & 0 deletions plugin/resource_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ func (p *ResourceProvider) Stop() error {
return err
}

func (p *ResourceProvider) GetSchema(req *terraform.ProviderSchemaRequest) (*terraform.ProviderSchema, error) {
var result ResourceProviderGetSchemaResponse
args := &ResourceProviderGetSchemaArgs{
Req: req,
}

err := p.Client.Call("Plugin.GetSchema", args, &result)
if err != nil {
return nil, err
}

if result.Error != nil {
err = result.Error
}

return result.Schema, err
}

func (p *ResourceProvider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
Expand Down Expand Up @@ -312,6 +330,15 @@ type ResourceProviderStopResponse struct {
Error *plugin.BasicError
}

type ResourceProviderGetSchemaArgs struct {
Req *terraform.ProviderSchemaRequest
}

type ResourceProviderGetSchemaResponse struct {
Schema *terraform.ProviderSchema
Error *plugin.BasicError
}

type ResourceProviderConfigureResponse struct {
Error *plugin.BasicError
}
Expand Down Expand Up @@ -418,6 +445,18 @@ func (s *ResourceProviderServer) Stop(
return nil
}

func (s *ResourceProviderServer) GetSchema(
args *ResourceProviderGetSchemaArgs,
result *ResourceProviderGetSchemaResponse,
) error {
schema, err := s.Provider.GetSchema(args.Req)
result.Schema = schema
if err != nil {
result.Error = plugin.NewBasicError(err)
}
return nil
}

func (s *ResourceProviderServer) Input(
args *ResourceProviderInputArgs,
reply *ResourceProviderInputResponse) error {
Expand Down
23 changes: 23 additions & 0 deletions terraform/resource_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ type ResourceProvider interface {
* Functions related to the provider
*********************************************************************/

// ProviderSchema returns the config schema for the main provider
// configuration, as would appear in a "provider" block in the
// configuration files.
//
// Currently not all providers support schema. Callers must therefore
// first call Resources and DataSources and ensure that at least one
// resource or data source has the SchemaAvailable flag set.
GetSchema(*ProviderSchemaRequest) (*ProviderSchema, error)

// Input is called to ask the provider to ask the user for input
// for completing the configuration if necesarry.
//
Expand Down Expand Up @@ -183,11 +192,25 @@ type ResourceProviderCloser interface {
type ResourceType struct {
Name string // Name of the resource, example "instance" (no provider prefix)
Importable bool // Whether this resource supports importing

// SchemaAvailable is set if the provider supports the ProviderSchema,
// ResourceTypeSchema and DataSourceSchema methods. Although it is
// included on each resource type, it's actually a provider-wide setting
// that's smuggled here only because that avoids a breaking change to
// the plugin protocol.
SchemaAvailable bool
}

// DataSource is a data source that a resource provider implements.
type DataSource struct {
Name string

// SchemaAvailable is set if the provider supports the ProviderSchema,
// ResourceTypeSchema and DataSourceSchema methods. Although it is
// included on each resource type, it's actually a provider-wide setting
// that's smuggled here only because that avoids a breaking change to
// the plugin protocol.
SchemaAvailable bool
}

// ResourceProviderResolver is an interface implemented by objects that are
Expand Down
17 changes: 16 additions & 1 deletion terraform/resource_provider_mock.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package terraform

import "sync"
import (
"sync"
)

// MockResourceProvider implements ResourceProvider but mocks out all the
// calls for testing purposes.
Expand All @@ -12,6 +14,10 @@ type MockResourceProvider struct {

CloseCalled bool
CloseError error
GetSchemaCalled bool
GetSchemaRequest *ProviderSchemaRequest
GetSchemaReturn *ProviderSchema
GetSchemaReturnError error
InputCalled bool
InputInput UIInput
InputConfig *ResourceConfig
Expand Down Expand Up @@ -92,6 +98,15 @@ func (p *MockResourceProvider) Close() error {
return p.CloseError
}

func (p *MockResourceProvider) GetSchema(req *ProviderSchemaRequest) (*ProviderSchema, error) {
p.Lock()
defer p.Unlock()

p.GetSchemaCalled = true
p.GetSchemaRequest = req
return p.GetSchemaReturn, p.GetSchemaReturnError
}

func (p *MockResourceProvider) Input(
input UIInput, c *ResourceConfig) (*ResourceConfig, error) {
p.Lock()
Expand Down
34 changes: 34 additions & 0 deletions terraform/schemas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package terraform

import (
"github.com/hashicorp/terraform/config/configschema"
)

type Schemas struct {
Providers ProviderSchemas
}

// ProviderSchemas is a map from provider names to provider schemas.
//
// The names in this map are the direct plugin name (e.g. "aws") rather than
// any alias name (e.g. "aws.foo"), since.
type ProviderSchemas map[string]*ProviderSchema

// ProviderSchema represents the schema for a provider's own configuration
// and the configuration for some or all of its resources and data sources.
//
// The completeness of this structure depends on how it was constructed.
// When constructed for a configuration, it will generally include only
// resource types and data sources used by that configuration.
type ProviderSchema struct {
Provider *configschema.Block
ResourceTypes map[string]*configschema.Block
DataSources map[string]*configschema.Block
}

// ProviderSchemaRequest is used to describe to a ResourceProvider which
// aspects of schema are required, when calling the GetSchema method.
type ProviderSchemaRequest struct {
ResourceTypes []string
DataSources []string
}

0 comments on commit 183833a

Please sign in to comment.