From 251e5c6f8748a0d452efa9cebe85435bb2973998 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jan 2017 20:49:31 -0800 Subject: [PATCH] helper/schema: framework for Backends --- helper/schema/backend.go | 94 ++++++++++++++++++++++++++++ helper/schema/backend_test.go | 111 ++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 helper/schema/backend.go create mode 100644 helper/schema/backend_test.go diff --git a/helper/schema/backend.go b/helper/schema/backend.go new file mode 100644 index 000000000..33fe2c190 --- /dev/null +++ b/helper/schema/backend.go @@ -0,0 +1,94 @@ +package schema + +import ( + "context" + + "github.com/hashicorp/terraform/terraform" +) + +// Backend represents a partial backend.Backend implementation and simplifies +// the creation of configuration loading and validation. +// +// Unlike other schema structs such as Provider, this struct is meant to be +// embedded within your actual implementation. It provides implementations +// only for Input and Configure and gives you a method for accessing the +// configuration in the form of a ResourceData that you're expected to call +// from the other implementation funcs. +type Backend struct { + // Schema is the schema for the configuration of this backend. If this + // Backend has no configuration this can be omitted. + Schema map[string]*Schema + + // ConfigureFunc is called to configure the backend. Use the + // FromContext* methods to extract information from the context. + // This can be nil, in which case nothing will be called but the + // config will still be stored. + ConfigureFunc func(context.Context) error + + config *ResourceData +} + +const ( + backendConfigKey = iota +) + +// FromContextBackendConfig extracts a ResourceData with the configuration +// from the context. This should only be called by Backend functions. +func FromContextBackendConfig(ctx context.Context) *ResourceData { + return ctx.Value(backendConfigKey).(*ResourceData) +} + +func (b *Backend) Input( + input terraform.UIInput, + c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { + if b == nil { + return c, nil + } + + return schemaMap(b.Schema).Input(input, c) +} + +func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) { + if b == nil { + return nil, nil + } + + return schemaMap(b.Schema).Validate(c) +} + +func (b *Backend) Configure(c *terraform.ResourceConfig) error { + if b == nil { + return nil + } + + sm := schemaMap(b.Schema) + + // Get a ResourceData for this configuration. To do this, we actually + // generate an intermediary "diff" although that is never exposed. + diff, err := sm.Diff(nil, c) + if err != nil { + return err + } + + data, err := sm.Data(nil, diff) + if err != nil { + return err + } + b.config = data + + if b.ConfigureFunc != nil { + err = b.ConfigureFunc(context.WithValue( + context.Background(), backendConfigKey, data)) + if err != nil { + return err + } + } + + return nil +} + +// Config returns the configuration. This is available after Configure is +// called. +func (b *Backend) Config() *ResourceData { + return b.config +} diff --git a/helper/schema/backend_test.go b/helper/schema/backend_test.go new file mode 100644 index 000000000..aae89cbf4 --- /dev/null +++ b/helper/schema/backend_test.go @@ -0,0 +1,111 @@ +package schema + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" +) + +func TestBackendValidate(t *testing.T) { + cases := []struct { + Name string + B *Backend + Config map[string]interface{} + Err bool + }{ + { + "Basic required field", + &Backend{ + Schema: map[string]*Schema{ + "foo": &Schema{ + Required: true, + Type: TypeString, + }, + }, + }, + nil, + true, + }, + + { + "Basic required field set", + &Backend{ + Schema: map[string]*Schema{ + "foo": &Schema{ + Required: true, + Type: TypeString, + }, + }, + }, + map[string]interface{}{ + "foo": "bar", + }, + false, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { + c, err := config.NewRawConfig(tc.Config) + if err != nil { + t.Fatalf("err: %s", err) + } + + _, es := tc.B.Validate(terraform.NewResourceConfig(c)) + if len(es) > 0 != tc.Err { + t.Fatalf("%d: %#v", i, es) + } + }) + } +} + +func TestBackendConfigure(t *testing.T) { + cases := []struct { + Name string + B *Backend + Config map[string]interface{} + Err bool + }{ + { + "Basic config", + &Backend{ + Schema: map[string]*Schema{ + "foo": &Schema{ + Type: TypeInt, + Optional: true, + }, + }, + + ConfigureFunc: func(ctx context.Context) error { + d := FromContextBackendConfig(ctx) + if d.Get("foo").(int) != 42 { + return fmt.Errorf("bad config data") + } + + return nil + }, + }, + map[string]interface{}{ + "foo": 42, + }, + false, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { + c, err := config.NewRawConfig(tc.Config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = tc.B.Configure(terraform.NewResourceConfig(c)) + if err != nil != tc.Err { + t.Fatalf("%d: %s", i, err) + } + }) + } +}