diff --git a/builtin/providers/terraform/data_source_state.go b/builtin/providers/terraform/data_source_state.go index 536f8b2d4..5fe0cf8cd 100644 --- a/builtin/providers/terraform/data_source_state.go +++ b/builtin/providers/terraform/data_source_state.go @@ -1,142 +1,151 @@ package terraform import ( - "fmt" - "log" - "time" + "fmt" + "log" - "github.com/hashicorp/terraform/backend" - backendinit "github.com/hashicorp/terraform/backend/init" - "github.com/hashicorp/terraform/config/hcl2shim" - "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/tfdiags" + "github.com/hashicorp/terraform/backend" + backendinit "github.com/hashicorp/terraform/backend/init" + "github.com/hashicorp/terraform/config/hcl2shim" + "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/providers" + "github.com/hashicorp/terraform/tfdiags" + "github.com/zclconf/go-cty/cty" ) -func dataSourceRemoteState() *schema.Resource { - return &schema.Resource{ - Read: dataSourceRemoteStateRead, - - Schema: map[string]*schema.Schema{ - "backend": { - Type: schema.TypeString, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - if vStr, ok := v.(string); ok && vStr == "_local" { - ws = append(ws, "Use of the %q backend is now officially "+ - "supported as %q. Please update your configuration to ensure "+ - "compatibility with future versions of Terraform.", - "_local", "local") - } - - return - }, - }, - - "config": { - Type: schema.TypeMap, - Optional: true, - }, - - "defaults": { - Type: schema.TypeMap, - Optional: true, - }, - - "environment": { - Type: schema.TypeString, - Optional: true, - Default: backend.DefaultStateName, - Deprecated: "Terraform environments are now called workspaces. Please use the workspace key instead.", - }, - - "workspace": { - Type: schema.TypeString, - Optional: true, - Default: backend.DefaultStateName, - }, - - "__has_dynamic_attributes": { - Type: schema.TypeString, - Optional: true, - }, - }, - } +func dataSourceRemoteStateGetSchema() providers.Schema { + return providers.Schema{ + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "backend": { + Type: cty.String, + Required: true, + }, + "config": { + Type: cty.DynamicPseudoType, + Optional: true, + }, + "defaults": { + Type: cty.DynamicPseudoType, + Optional: true, + }, + "outputs": { + Type: cty.DynamicPseudoType, + Computed: true, + }, + "workspace": { + Type: cty.String, + Optional: true, + }, + }, + }, + } } -func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error { - backendType := d.Get("backend").(string) +func dataSourceRemoteStateRead(d *cty.Value) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics - // Don't break people using the old _local syntax - but note warning above - if backendType == "_local" { - log.Println(`[INFO] Switching old (unsupported) backend "_local" to "local"`) - backendType = "local" + newState := make(map[string]cty.Value) + newState["backend"] = d.GetAttr("backend") + + backendType := d.GetAttr("backend").AsString() + + // Don't break people using the old _local syntax - but note warning above + if backendType == "_local" { + log.Println(`[INFO] Switching old (unsupported) backend "_local" to "local"`) + backendType = "local" } // Create the client to access our remote state log.Printf("[DEBUG] Initializing remote state backend: %s", backendType) f := backendinit.Backend(backendType) if f == nil { - return fmt.Errorf("Unknown backend type: %s", backendType) + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid backend configuration", + fmt.Sprintf("Unknown backend type: %s", backendType), + cty.Path(nil).GetAttr("backend"), + )) + return cty.NilVal, diags } b := f() - schema := b.ConfigSchema() - rawConfig := d.Get("config") - configVal := hcl2shim.HCL2ValueFromConfigValue(rawConfig) + config := d.GetAttr("config") + newState["config"] = config + schema := b.ConfigSchema() // Try to coerce the provided value into the desired configuration type. - configVal, err := schema.CoerceValue(configVal) + configVal, err := schema.CoerceValue(config) if err != nil { - return fmt.Errorf("invalid %s backend configuration: %s", backendType, tfdiags.FormatError(err)) + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid backend configuration", + fmt.Sprintf("The given configuration is not valid for backend %q: %s.", backendType, + tfdiags.FormatError(err)), + cty.Path(nil).GetAttr("config"), + )) + return cty.NilVal, diags } + validateDiags := b.ValidateConfig(configVal) + diags = diags.Append(validateDiags) if validateDiags.HasErrors() { - return validateDiags.Err() + return cty.NilVal, diags } + configureDiags := b.Configure(configVal) if configureDiags.HasErrors() { - return configureDiags.Err() + diags = diags.Append(configureDiags.Err()) + return cty.NilVal, diags } - // environment is deprecated in favour of workspace. - // If both keys are set workspace should win. - name := d.Get("environment").(string) - if ws, ok := d.GetOk("workspace"); ok && ws != backend.DefaultStateName { - name = ws.(string) + var name string + + if workspaceVal := d.GetAttr("workspace"); !workspaceVal.IsNull() { + newState["workspace"] = workspaceVal + ws := workspaceVal.AsString() + if ws != backend.DefaultStateName { + name = ws + } } state, err := b.State(name) if err != nil { - return fmt.Errorf("error loading the remote state: %s", err) + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Error loading state error", + fmt.Sprintf("error loading the remote state: %s", err), + cty.Path(nil).GetAttr("backend"), + )) + return cty.NilVal, diags } + if err := state.RefreshState(); err != nil { - return err + diags = diags.Append(err) + return cty.NilVal, diags } - d.SetId(time.Now().UTC().String()) - outputMap := make(map[string]interface{}) + outputs := make(map[string]cty.Value) - defaults := d.Get("defaults").(map[string]interface{}) - for key, val := range defaults { - outputMap[key] = val + if defaultsVal := d.GetAttr("defaults"); !defaultsVal.IsNull() { + newState["defaults"] = defaultsVal + it := defaultsVal.ElementIterator() + for it.Next() { + k, v := it.Element() + outputs[k.AsString()] = v + } } remoteState := state.State() if remoteState.Empty() { - log.Println("[DEBUG] empty remote state") + log.Println("[DEBUG] empty remote state") } else { - for key, val := range remoteState.RootModule().Outputs { - if val.Value != nil { - outputMap[key] = val.Value - } - } + for k, os := range remoteState.RootModule().Outputs { + outputs[k] = hcl2shim.HCL2ValueFromConfigValue(os.Value) + } } - mappedOutputs := remoteStateFlatten(outputMap) + newState["outputs"] = cty.ObjectVal(outputs) - for key, val := range mappedOutputs { - d.UnsafeSetFieldRaw(key, val) - } - - return nil + return cty.ObjectVal(newState), diags } diff --git a/builtin/providers/terraform/data_source_state_test.go b/builtin/providers/terraform/data_source_state_test.go index 3bc39ecc9..142b818bb 100644 --- a/builtin/providers/terraform/data_source_state_test.go +++ b/builtin/providers/terraform/data_source_state_test.go @@ -1,225 +1,135 @@ package terraform import ( - "fmt" "testing" - backendInit "github.com/hashicorp/terraform/backend/init" - "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/terraform" + "github.com/zclconf/go-cty/cty" ) +func TestResource(t *testing.T) { + if err := dataSourceRemoteStateGetSchema().Block.InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + func TestState_basic(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccState_basic, - Check: resource.ComposeTestCheckFunc( - testAccCheckStateValue( - "data.terraform_remote_state.foo", "foo", "bar"), - ), - }, + var tests = []struct { + Config cty.Value + Want cty.Value + Err bool + }{ + { // basic test + cty.ObjectVal(map[string]cty.Value{ + "backend": cty.StringVal("local"), + "config": cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal("./test-fixtures/basic.tfstate"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "backend": cty.StringVal("local"), + "config": cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal("./test-fixtures/basic.tfstate"), + }), + "outputs": cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), + }), + false, }, - }) -} - -func TestState_backends(t *testing.T) { - backendInit.Set("_ds_test", backendInit.Backend("local")) - defer backendInit.Set("_ds_test", nil) - - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccState_backend, - Check: resource.ComposeTestCheckFunc( - testAccCheckStateValue( - "data.terraform_remote_state.foo", "foo", "bar"), - ), - }, + { // complex outputs + cty.ObjectVal(map[string]cty.Value{ + "backend": cty.StringVal("local"), + "config": cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal("./test-fixtures/complex_outputs.tfstate"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "backend": cty.StringVal("local"), + "config": cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal("./test-fixtures/complex_outputs.tfstate"), + }), + "outputs": cty.ObjectVal(map[string]cty.Value{ + "computed_map": cty.ObjectVal(map[string]cty.Value{ + "key1": cty.StringVal("value1"), + }), + "computed_set": cty.TupleVal([]cty.Value{ + cty.StringVal("setval1"), + cty.StringVal("setval2"), + }), + "map": cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("test"), + "test": cty.StringVal("test"), + }), + "set": cty.TupleVal([]cty.Value{ + cty.StringVal("test1"), + cty.StringVal("test2"), + }), + }), + }), + false, }, - }) -} - -func TestState_complexOutputs(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccState_complexOutputs, - Check: resource.ComposeTestCheckFunc( - testAccCheckStateValue("terraform_remote_state.foo", "backend", "local"), - // This (adding the hash) should be reverted when merged into 0.12. - // testAccCheckStateValue("terraform_remote_state.foo", "config.path", "./test-fixtures/complex_outputs.tfstate"), - testAccCheckStateValue("terraform_remote_state.foo", "config.1590222752.path", "./test-fixtures/complex_outputs.tfstate"), - testAccCheckStateValue("terraform_remote_state.foo", "computed_set.#", "2"), - testAccCheckStateValue("terraform_remote_state.foo", `map.%`, "2"), - testAccCheckStateValue("terraform_remote_state.foo", `map.key`, "test"), - ), - }, + { // null outputs + cty.ObjectVal(map[string]cty.Value{ + "backend": cty.StringVal("local"), + "config": cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal("./test-fixtures/null_outputs.tfstate"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "backend": cty.StringVal("local"), + "config": cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal("./test-fixtures/null_outputs.tfstate"), + }), + "outputs": cty.ObjectVal(map[string]cty.Value{ + "map": cty.NullVal(cty.DynamicPseudoType), + "list": cty.NullVal(cty.DynamicPseudoType), + }), + }), + false, }, - }) -} - -// outputs should never have a null value, but don't crash if we ever encounter -// them. -func TestState_nullOutputs(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccState_nullOutputs, - }, + { // defaults + cty.ObjectVal(map[string]cty.Value{ + "backend": cty.StringVal("local"), + "config": cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal("./test-fixtures/empty.tfstate"), + }), + "defaults": cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "backend": cty.StringVal("local"), + "config": cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal("./test-fixtures/empty.tfstate"), + }), + "defaults": cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), + "outputs": cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), + }), + false, }, - }) -} - -func TestEmptyState_defaults(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccEmptyState_defaults, - Check: resource.ComposeTestCheckFunc( - testAccCheckStateValue( - "data.terraform_remote_state.foo", "foo", "bar"), - ), - }, - }, - }) -} - -func TestState_defaults(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccEmptyState_defaults, - Check: resource.ComposeTestCheckFunc( - testAccCheckStateValue( - "data.terraform_remote_state.foo", "foo", "bar"), - ), - }, - }, - }) -} - -func testAccCheckStateValue(id, name, value string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) + } + for _, test := range tests { + schema := dataSourceRemoteStateGetSchema().Block + config, err := schema.CoerceValue(test.Config) + if err != nil { + t.Fatalf("unexpected error: %s", err) } - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") + got, diags := dataSourceRemoteStateRead(&config) + + if test.Err { + if !diags.HasErrors() { + t.Fatal("succeeded; want error") + } + } else if diags.HasErrors() { + t.Fatalf("unexpected error: %s", err) } - v := rs.Primary.Attributes[name] - if v != value { - return fmt.Errorf( - "Value for %s is %s, not %s", name, v, value) + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) } - - return nil } } - -// make sure that the deprecated environment field isn't overridden by the -// default value for workspace. -func TestState_deprecatedEnvironment(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccState_deprecatedEnvironment, - Check: resource.ComposeTestCheckFunc( - testAccCheckStateValue( - // if the workspace default value overrides the - // environment, this will get the foo value from the - // default state. - "data.terraform_remote_state.foo", "foo", ""), - ), - }, - }, - }) -} - -const testAccState_basic = ` -data "terraform_remote_state" "foo" { - backend = "local" - - config { - path = "./test-fixtures/basic.tfstate" - } -}` - -const testAccState_backend = ` -data "terraform_remote_state" "foo" { - backend = "_ds_test" - - config { - path = "./test-fixtures/basic.tfstate" - } -}` - -const testAccState_complexOutputs = ` -resource "terraform_remote_state" "foo" { - backend = "local" - - config { - path = "./test-fixtures/complex_outputs.tfstate" - } -}` - -const testAccState_nullOutputs = ` -resource "terraform_remote_state" "foo" { - backend = "local" - - config { - path = "./test-fixtures/null_outputs.tfstate" - } -}` - -const testAccEmptyState_defaults = ` -data "terraform_remote_state" "foo" { - backend = "local" - - config { - path = "./test-fixtures/empty.tfstate" - } - - defaults { - foo = "bar" - } -}` - -const testAccState_defaults = ` -data "terraform_remote_state" "foo" { - backend = "local" - - config { - path = "./test-fixtures/basic.tfstate" - } - - defaults { - foo = "not bar" - } -}` - -const testAccState_deprecatedEnvironment = ` -data "terraform_remote_state" "foo" { - backend = "local" - environment = "deprecated" - - config { - path = "./test-fixtures/basic.tfstate" - } -}` diff --git a/builtin/providers/terraform/provider.go b/builtin/providers/terraform/provider.go index 1bdf2c1d0..405f3f248 100644 --- a/builtin/providers/terraform/provider.go +++ b/builtin/providers/terraform/provider.go @@ -1,21 +1,124 @@ package terraform import ( - "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/terraform" + "fmt" + "log" + + "github.com/hashicorp/terraform/providers" ) -// Provider returns a terraform.ResourceProvider. -func Provider() terraform.ResourceProvider { - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "terraform_remote_state": schema.DataSourceResourceShim( - "terraform_remote_state", - dataSourceRemoteState(), - ), - }, - DataSourcesMap: map[string]*schema.Resource{ - "terraform_remote_state": dataSourceRemoteState(), +// Provider is an implementation of providers.Interface +type Provider struct { + // Provider is the schema for the provider itself. + Schema providers.Schema + + // DataSources maps the data source name to that data source's schema. + DataSources map[string]providers.Schema +} + +// NewProvider returns a new terraform provider +func NewProvider() *Provider { + return &Provider{} +} + +// GetSchema returns the complete schema for the provider. +func (p *Provider) GetSchema() providers.GetSchemaResponse { + return providers.GetSchemaResponse{ + DataSources: map[string]providers.Schema{ + "terraform_remote_state": dataSourceRemoteStateGetSchema(), }, } } + +// ValidateProviderConfig is used to validate the configuration values. +func (p *Provider) ValidateProviderConfig(providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { + // At this moment there is nothing to configure for the terraform provider, + // so we will happily return without taking any action + var res providers.ValidateProviderConfigResponse + return res +} + +// ValidateDataSourceConfig is used to validate the data source configuration values. +func (p *Provider) ValidateDataSourceConfig(providers.ValidateDataSourceConfigRequest) providers.ValidateDataSourceConfigResponse { + // FIXME: move the backend configuration validate call that's currently + // inside the read method into here so that we can catch provider configuration + // errors in terraform validate as well as during terraform plan. + var res providers.ValidateDataSourceConfigResponse + return res +} + +// Configure configures and initializes the provider. +func (p *Provider) Configure(providers.ConfigureRequest) providers.ConfigureResponse { + // At this moment there is nothing to configure for the terraform provider, + // so we will happily return without taking any action + var res providers.ConfigureResponse + return res +} + +// ReadDataSource returns the data source's current state. +func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + // call function + var res providers.ReadDataSourceResponse + + // This should not happen + if req.TypeName != "terraform_remote_state" { + res.Diagnostics.Append(fmt.Errorf("Error: unsupported data source %s", req.TypeName)) + return res + } + + newState, diags := dataSourceRemoteStateRead(&req.Config) + + res.State = newState + res.Diagnostics = diags + + return res +} + +// Stop is called when the provider should halt any in-flight actions. +func (p *Provider) Stop() error { + log.Println("[DEBUG] terraform provider cannot Stop") + return nil +} + +// All the Resource-specific functions are below. +// The terraform provider supplies a single data source, `terraform_remote_state` +// and no resources. + +// UpgradeResourceState is called when the state loader encounters an +// instance state whose schema version is less than the one reported by the +// currently-used version of the corresponding provider, and the upgraded +// result is used for any further processing. +func (p *Provider) UpgradeResourceState(providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { + panic("unimplemented - terraform_remote_state has no resources") +} + +// ReadResource refreshes a resource and returns its current state. +func (p *Provider) ReadResource(providers.ReadResourceRequest) providers.ReadResourceResponse { + panic("unimplemented - terraform_remote_state has no resources") +} + +// PlanResourceChange takes the current state and proposed state of a +// resource, and returns the planned final state. +func (p *Provider) PlanResourceChange(providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + panic("unimplemented - terraform_remote_state has no resources") +} + +// ApplyResourceChange takes the planned state for a resource, which may +// yet contain unknown computed values, and applies the changes returning +// the final state. +func (p *Provider) ApplyResourceChange(providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + panic("unimplemented - terraform_remote_state has no resources") +} + +// ImportResourceState requests that the given resource be imported. +func (p *Provider) ImportResourceState(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + panic("unimplemented - terraform_remote_state has no resources") +} + +// ValidateResourceTypeConfig is used to to validate the resource configuration values. +func (p *Provider) ValidateResourceTypeConfig(providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse { + // At this moment there is nothing to configure for the terraform provider, + // so we will happily return without taking any action + var res providers.ValidateResourceTypeConfigResponse + return res +} diff --git a/builtin/providers/terraform/provider_test.go b/builtin/providers/terraform/provider_test.go index 0ba389b5c..c2eaf3fd0 100644 --- a/builtin/providers/terraform/provider_test.go +++ b/builtin/providers/terraform/provider_test.go @@ -3,32 +3,21 @@ package terraform import ( "testing" - backendInit "github.com/hashicorp/terraform/backend/init" - "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/providers" ) -var testAccProviders map[string]terraform.ResourceProvider -var testAccProvider *schema.Provider +var testAccProviders map[string]*Provider +var testAccProvider *Provider func init() { - // Initialize the backends - backendInit.Init(nil) - - testAccProvider = Provider().(*schema.Provider) - testAccProviders = map[string]terraform.ResourceProvider{ + testAccProvider = NewProvider() + testAccProviders = map[string]*Provider{ "terraform": testAccProvider, } } -func TestProvider(t *testing.T) { - if err := Provider().(*schema.Provider).InternalValidate(); err != nil { - t.Fatalf("err: %s", err) - } -} - func TestProvider_impl(t *testing.T) { - var _ terraform.ResourceProvider = Provider() + var _ providers.Interface = NewProvider() } func testAccPreCheck(t *testing.T) {