Skip to content

Commit

Permalink
core: New ResourceProvider methods for data resources
Browse files Browse the repository at this point in the history
This is a breaking change to the ResourceProvider interface that adds the
new operations relating to data sources.

DataSources, ValidateDataSource, ReadDataDiff and ReadDataApply are the
data source equivalents of Resources, Validate, Diff and Apply (respectively)
for managed resources.

The diff/apply model seems at first glance a rather strange workflow for
read-only resources, but implementing data resources in this way allows them
to fit cleanly into the standard plan/apply lifecycle in cases where the
configuration contains computed arguments and thus the read must be deferred
until apply time.

Along with breaking the interface, we also fix up the plugin client/server
and helper/schema implementations of it, which are all of the callers
used when provider plugins use helper/schema. This would be a breaking
change for any provider plugin that directly implements the provider
interface, but no known plugins do this and it is not recommended.

At the helper/schema layer the implementer sees ReadDataApply as a "Read",
as opposed to "Create" or "Update" as in the managed resource Apply
implementation. The planning mechanics are handled entirely within
helper/schema, so that complexity is hidden from the provider implementation
itself.
  • Loading branch information
apparentlymart committed May 14, 2016
1 parent 718cdda commit 0e0e3d7
Show file tree
Hide file tree
Showing 7 changed files with 551 additions and 47 deletions.
78 changes: 77 additions & 1 deletion helper/schema/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ type Provider struct {
// Diff, etc. to the proper resource.
ResourcesMap map[string]*Resource

// DataSourcesMap is the collection of available data sources that
// this provider implements, with a Resource instance defining
// the schema and Read operation of each.
//
// Resource instances for data sources must have a Read function
// and must *not* implement Create, Update or Delete.
DataSourcesMap map[string]*Resource

// ConfigureFunc is a function for configuring the provider. If the
// provider doesn't need to be configured, this can be omitted.
//
Expand Down Expand Up @@ -68,7 +76,19 @@ func (p *Provider) InternalValidate() error {

for k, r := range p.ResourcesMap {
if err := r.InternalValidate(nil); err != nil {
return fmt.Errorf("%s: %s", k, err)
return fmt.Errorf("resource %s: %s", k, err)
}
}

for k, r := range p.DataSourcesMap {
if err := r.InternalValidate(nil); err != nil {
return fmt.Errorf("data source %s: %s", k, err)
}

if r.Create != nil || r.Update != nil || r.Delete != nil {
return fmt.Errorf(
"data source %s: must not have Create, Update or Delete", k,
)
}
}

Expand Down Expand Up @@ -262,3 +282,59 @@ func (p *Provider) ImportState(

return states, nil
}

// ValidateDataSource implementation of terraform.ResourceProvider interface.
func (p *Provider) ValidateDataSource(
t string, c *terraform.ResourceConfig) ([]string, []error) {
r, ok := p.DataSourcesMap[t]
if !ok {
return nil, []error{fmt.Errorf(
"Provider doesn't support data source: %s", t)}
}

return r.Validate(c)
}

// ReadDataDiff implementation of terraform.ResourceProvider interface.
func (p *Provider) ReadDataDiff(
info *terraform.InstanceInfo,
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {

r, ok := p.DataSourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown data source: %s", info.Type)
}

return r.Diff(nil, c)
}

// RefreshData implementation of terraform.ResourceProvider interface.
func (p *Provider) ReadDataApply(
info *terraform.InstanceInfo,
d *terraform.InstanceDiff) (*terraform.InstanceState, error) {

r, ok := p.DataSourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown data source: %s", info.Type)
}

return r.ReadDataApply(d, p.meta)
}

// DataSources implementation of terraform.ResourceProvider interface.
func (p *Provider) DataSources() []terraform.DataSource {
keys := make([]string, 0, len(p.DataSourcesMap))
for k, _ := range p.DataSourcesMap {
keys = append(keys, k)
}
sort.Strings(keys)

result := make([]terraform.DataSource, 0, len(keys))
for _, k := range keys {
result = append(result, terraform.DataSource{
Name: k,
})
}

return result
}
32 changes: 32 additions & 0 deletions helper/schema/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,38 @@ func TestProviderResources(t *testing.T) {
}
}

func TestProviderDataSources(t *testing.T) {
cases := []struct {
P *Provider
Result []terraform.DataSource
}{
{
P: &Provider{},
Result: []terraform.DataSource{},
},

{
P: &Provider{
DataSourcesMap: map[string]*Resource{
"foo": nil,
"bar": nil,
},
},
Result: []terraform.DataSource{
terraform.DataSource{Name: "bar"},
terraform.DataSource{Name: "foo"},
},
},
}

for i, tc := range cases {
actual := tc.P.DataSources()
if !reflect.DeepEqual(actual, tc.Result) {
t.Fatalf("%d: got %#v; want %#v", i, actual, tc.Result)
}
}
}

func TestProviderValidate(t *testing.T) {
cases := []struct {
P *Provider
Expand Down
27 changes: 27 additions & 0 deletions helper/schema/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,33 @@ func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return schemaMap(r.Schema).Validate(c)
}

// ReadDataApply loads the data for a data source, given a diff that
// describes the configuration arguments and desired computed attributes.
func (r *Resource) ReadDataApply(
d *terraform.InstanceDiff,
meta interface{},
) (*terraform.InstanceState, error) {

// Data sources are always built completely from scratch
// on each read, so the source state is always nil.
data, err := schemaMap(r.Schema).Data(nil, d)
if err != nil {
return nil, err
}

err = r.Read(data, meta)
state := data.State()
if state != nil && state.ID == "" {
// Data sources can set an ID if they want, but they aren't
// required to; we'll provide a placeholder if they don't,
// to preserve the invariant that all resources have non-empty
// ids.
state.ID = "-"
}

return r.recordCurrentSchemaVersion(state), err
}

// Refresh refreshes the state of the resource.
func (r *Resource) Refresh(
s *terraform.InstanceState,
Expand Down
140 changes: 140 additions & 0 deletions plugin/resource_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,30 @@ func (p *ResourceProvider) Diff(
return resp.Diff, err
}

func (p *ResourceProvider) ValidateDataSource(
t string, c *terraform.ResourceConfig) ([]string, []error) {
var resp ResourceProviderValidateResourceResponse
args := ResourceProviderValidateResourceArgs{
Config: c,
Type: t,
}

err := p.Client.Call("Plugin.ValidateDataSource", &args, &resp)
if err != nil {
return nil, []error{err}
}

var errs []error
if len(resp.Errors) > 0 {
errs = make([]error, len(resp.Errors))
for i, err := range resp.Errors {
errs[i] = err
}
}

return resp.Warnings, errs
}

func (p *ResourceProvider) Refresh(
info *terraform.InstanceInfo,
s *terraform.InstanceState) (*terraform.InstanceState, error) {
Expand Down Expand Up @@ -208,6 +232,58 @@ func (p *ResourceProvider) Resources() []terraform.ResourceType {
return result
}

func (p *ResourceProvider) ReadDataDiff(
info *terraform.InstanceInfo,
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
var resp ResourceProviderReadDataDiffResponse
args := &ResourceProviderReadDataDiffArgs{
Info: info,
Config: c,
}

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

return resp.Diff, err
}

func (p *ResourceProvider) ReadDataApply(
info *terraform.InstanceInfo,
d *terraform.InstanceDiff) (*terraform.InstanceState, error) {
var resp ResourceProviderReadDataApplyResponse
args := &ResourceProviderReadDataApplyArgs{
Info: info,
Diff: d,
}

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

return resp.State, err
}

func (p *ResourceProvider) DataSources() []terraform.DataSource {
var result []terraform.DataSource

err := p.Client.Call("Plugin.DataSources", new(interface{}), &result)
if err != nil {
// TODO: panic, log, what?
return nil
}

return result
}

func (p *ResourceProvider) Close() error {
return p.Client.Close()
}
Expand Down Expand Up @@ -275,6 +351,26 @@ type ResourceProviderImportStateResponse struct {
Error *plugin.BasicError
}

type ResourceProviderReadDataApplyArgs struct {
Info *terraform.InstanceInfo
Diff *terraform.InstanceDiff
}

type ResourceProviderReadDataApplyResponse struct {
State *terraform.InstanceState
Error *plugin.BasicError
}

type ResourceProviderReadDataDiffArgs struct {
Info *terraform.InstanceInfo
Config *terraform.ResourceConfig
}

type ResourceProviderReadDataDiffResponse struct {
Diff *terraform.InstanceDiff
Error *plugin.BasicError
}

type ResourceProviderValidateArgs struct {
Config *terraform.ResourceConfig
}
Expand Down Expand Up @@ -408,3 +504,47 @@ func (s *ResourceProviderServer) Resources(
*result = s.Provider.Resources()
return nil
}

func (s *ResourceProviderServer) ValidateDataSource(
args *ResourceProviderValidateResourceArgs,
reply *ResourceProviderValidateResourceResponse) error {
warns, errs := s.Provider.ValidateDataSource(args.Type, args.Config)
berrs := make([]*plugin.BasicError, len(errs))
for i, err := range errs {
berrs[i] = plugin.NewBasicError(err)
}
*reply = ResourceProviderValidateResourceResponse{
Warnings: warns,
Errors: berrs,
}
return nil
}

func (s *ResourceProviderServer) ReadDataDiff(
args *ResourceProviderReadDataDiffArgs,
result *ResourceProviderReadDataDiffResponse) error {
diff, err := s.Provider.ReadDataDiff(args.Info, args.Config)
*result = ResourceProviderReadDataDiffResponse{
Diff: diff,
Error: plugin.NewBasicError(err),
}
return nil
}

func (s *ResourceProviderServer) ReadDataApply(
args *ResourceProviderReadDataApplyArgs,
result *ResourceProviderReadDataApplyResponse) error {
newState, err := s.Provider.ReadDataApply(args.Info, args.Diff)
*result = ResourceProviderReadDataApplyResponse{
State: newState,
Error: plugin.NewBasicError(err),
}
return nil
}

func (s *ResourceProviderServer) DataSources(
nothing interface{},
result *[]terraform.DataSource) error {
*result = s.Provider.DataSources()
return nil
}
Loading

0 comments on commit 0e0e3d7

Please sign in to comment.