From 183833affc456db915f1edfc4d1e871816d73db8 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 13 Oct 2017 18:43:08 -0700 Subject: [PATCH] core: terraform.ResourceProvider.GetSchema method 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. --- helper/schema/core_schema.go | 5 +- helper/schema/core_schema_test.go | 2 +- helper/schema/provider.go | 32 ++++++++++ helper/schema/provider_test.go | 96 ++++++++++++++++++++++++++--- plugin/resource_provider.go | 39 ++++++++++++ terraform/resource_provider.go | 23 +++++++ terraform/resource_provider_mock.go | 17 ++++- terraform/schemas.go | 34 ++++++++++ 8 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 terraform/schemas.go diff --git a/helper/schema/core_schema.go b/helper/schema/core_schema.go index c1c7d8e51..bf952f663 100644 --- a/helper/schema/core_schema.go +++ b/helper/schema/core_schema.go @@ -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{ diff --git a/helper/schema/core_schema_test.go b/helper/schema/core_schema_test.go index 63e76c988..08f8dbd9a 100644 --- a/helper/schema/core_schema_test.go +++ b/helper/schema/core_schema_test.go @@ -18,7 +18,7 @@ func TestSchemaMapCoreConfigSchema(t *testing.T) { }{ "empty": { map[string]*Schema{}, - nil, + &configschema.Block{}, }, "primitives": { map[string]*Schema{ diff --git a/helper/schema/provider.go b/helper/schema/provider.go index fb28b4151..30b6b9e3d 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -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" ) @@ -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, @@ -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, }) } @@ -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, }) } diff --git a/helper/schema/provider_test.go b/helper/schema/provider_test.go index 0243be938..959a88a34 100644 --- a/helper/schema/provider_test.go +++ b/helper/schema/provider_test.go @@ -6,7 +6,11 @@ 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" ) @@ -14,6 +18,84 @@ 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 @@ -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}, }, }, @@ -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}, }, }, } @@ -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}, }, }, } diff --git a/plugin/resource_provider.go b/plugin/resource_provider.go index 473f78601..d6a433c4e 100644 --- a/plugin/resource_provider.go +++ b/plugin/resource_provider.go @@ -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) { @@ -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 } @@ -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 { diff --git a/terraform/resource_provider.go b/terraform/resource_provider.go index 7d78f67ef..93fd14fc8 100644 --- a/terraform/resource_provider.go +++ b/terraform/resource_provider.go @@ -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. // @@ -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 diff --git a/terraform/resource_provider_mock.go b/terraform/resource_provider_mock.go index 95f8c56a2..73cde0ccb 100644 --- a/terraform/resource_provider_mock.go +++ b/terraform/resource_provider_mock.go @@ -1,6 +1,8 @@ package terraform -import "sync" +import ( + "sync" +) // MockResourceProvider implements ResourceProvider but mocks out all the // calls for testing purposes. @@ -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 @@ -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() diff --git a/terraform/schemas.go b/terraform/schemas.go new file mode 100644 index 000000000..ec46efcf7 --- /dev/null +++ b/terraform/schemas.go @@ -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 +}