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 +}