builtin/providers: terraform remote state datasource (#18446)

* builtin/providers: implement terraform remote state datasource as providers.Interface

* append and return diags separately (to match the idiomatic usage
elsewhere in Terraform)
* diagnostic summary style improvements
* update tests to pass config to schema.CoerceValue
* trust that the schema will be enforced and there is no need to check
that a given attribute exists
* added dataSourceRemoteStateGetSchema() (effectively replacing a
function that was inappropriately removed) for consistency with other
terraform providers
* builtin/provider terraform test: added InternalValidate() test for dataSourceRemoteStateGetSchema
This commit is contained in:
Kristin Laemmert 2018-07-17 14:55:56 -07:00 committed by Martin Atkins
parent 05936df0e7
commit 52ae93cf97
4 changed files with 343 additions and 332 deletions

View File

@ -3,68 +3,52 @@ package terraform
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
backendinit "github.com/hashicorp/terraform/backend/init" backendinit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/config/hcl2shim" "github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
) )
func dataSourceRemoteState() *schema.Resource { func dataSourceRemoteStateGetSchema() providers.Schema {
return &schema.Resource{ return providers.Schema{
Read: dataSourceRemoteStateRead, Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
Schema: map[string]*schema.Schema{
"backend": { "backend": {
Type: schema.TypeString, Type: cty.String,
Required: true, 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": { "config": {
Type: schema.TypeMap, Type: cty.DynamicPseudoType,
Optional: true, Optional: true,
}, },
"defaults": { "defaults": {
Type: schema.TypeMap, Type: cty.DynamicPseudoType,
Optional: true, Optional: true,
}, },
"outputs": {
"environment": { Type: cty.DynamicPseudoType,
Type: schema.TypeString, Computed: true,
Optional: true,
Default: backend.DefaultStateName,
Deprecated: "Terraform environments are now called workspaces. Please use the workspace key instead.",
}, },
"workspace": { "workspace": {
Type: schema.TypeString, Type: cty.String,
Optional: true, Optional: true,
Default: backend.DefaultStateName,
}, },
"__has_dynamic_attributes": {
Type: schema.TypeString,
Optional: true,
}, },
}, },
} }
} }
func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error { func dataSourceRemoteStateRead(d *cty.Value) (cty.Value, tfdiags.Diagnostics) {
backendType := d.Get("backend").(string) var diags tfdiags.Diagnostics
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 // Don't break people using the old _local syntax - but note warning above
if backendType == "_local" { if backendType == "_local" {
@ -76,67 +60,92 @@ func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] Initializing remote state backend: %s", backendType) log.Printf("[DEBUG] Initializing remote state backend: %s", backendType)
f := backendinit.Backend(backendType) f := backendinit.Backend(backendType)
if f == nil { 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() b := f()
schema := b.ConfigSchema() config := d.GetAttr("config")
rawConfig := d.Get("config") newState["config"] = config
configVal := hcl2shim.HCL2ValueFromConfigValue(rawConfig)
schema := b.ConfigSchema()
// Try to coerce the provided value into the desired configuration type. // Try to coerce the provided value into the desired configuration type.
configVal, err := schema.CoerceValue(configVal) configVal, err := schema.CoerceValue(config)
if err != nil { 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) validateDiags := b.ValidateConfig(configVal)
diags = diags.Append(validateDiags)
if validateDiags.HasErrors() { if validateDiags.HasErrors() {
return validateDiags.Err() return cty.NilVal, diags
} }
configureDiags := b.Configure(configVal) configureDiags := b.Configure(configVal)
if configureDiags.HasErrors() { if configureDiags.HasErrors() {
return configureDiags.Err() diags = diags.Append(configureDiags.Err())
return cty.NilVal, diags
} }
// environment is deprecated in favour of workspace. var name string
// If both keys are set workspace should win.
name := d.Get("environment").(string) if workspaceVal := d.GetAttr("workspace"); !workspaceVal.IsNull() {
if ws, ok := d.GetOk("workspace"); ok && ws != backend.DefaultStateName { newState["workspace"] = workspaceVal
name = ws.(string) ws := workspaceVal.AsString()
if ws != backend.DefaultStateName {
name = ws
}
} }
state, err := b.State(name) state, err := b.State(name)
if err != nil { 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 { 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{}) if defaultsVal := d.GetAttr("defaults"); !defaultsVal.IsNull() {
for key, val := range defaults { newState["defaults"] = defaultsVal
outputMap[key] = val it := defaultsVal.ElementIterator()
for it.Next() {
k, v := it.Element()
outputs[k.AsString()] = v
}
} }
remoteState := state.State() remoteState := state.State()
if remoteState.Empty() { if remoteState.Empty() {
log.Println("[DEBUG] empty remote state") log.Println("[DEBUG] empty remote state")
} else { } else {
for key, val := range remoteState.RootModule().Outputs { for k, os := range remoteState.RootModule().Outputs {
if val.Value != nil { outputs[k] = hcl2shim.HCL2ValueFromConfigValue(os.Value)
outputMap[key] = val.Value
}
} }
} }
mappedOutputs := remoteStateFlatten(outputMap) newState["outputs"] = cty.ObjectVal(outputs)
for key, val := range mappedOutputs { return cty.ObjectVal(newState), diags
d.UnsafeSetFieldRaw(key, val)
}
return nil
} }

View File

@ -1,225 +1,135 @@
package terraform package terraform
import ( import (
"fmt"
"testing" "testing"
backendInit "github.com/hashicorp/terraform/backend/init" "github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
) )
func TestResource(t *testing.T) {
if err := dataSourceRemoteStateGetSchema().Block.InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestState_basic(t *testing.T) { func TestState_basic(t *testing.T) {
resource.UnitTest(t, resource.TestCase{ var tests = []struct {
PreCheck: func() { testAccPreCheck(t) }, Config cty.Value
Providers: testAccProviders, Want cty.Value
Steps: []resource.TestStep{ Err bool
{ }{
Config: testAccState_basic, { // basic test
Check: resource.ComposeTestCheckFunc( cty.ObjectVal(map[string]cty.Value{
testAccCheckStateValue( "backend": cty.StringVal("local"),
"data.terraform_remote_state.foo", "foo", "bar"), "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,
}, },
{ // 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,
}, },
}) { // null outputs
} cty.ObjectVal(map[string]cty.Value{
"backend": cty.StringVal("local"),
func TestState_backends(t *testing.T) { "config": cty.ObjectVal(map[string]cty.Value{
backendInit.Set("_ds_test", backendInit.Backend("local")) "path": cty.StringVal("./test-fixtures/null_outputs.tfstate"),
defer backendInit.Set("_ds_test", nil) }),
}),
resource.UnitTest(t, resource.TestCase{ cty.ObjectVal(map[string]cty.Value{
PreCheck: func() { testAccPreCheck(t) }, "backend": cty.StringVal("local"),
Providers: testAccProviders, "config": cty.ObjectVal(map[string]cty.Value{
Steps: []resource.TestStep{ "path": cty.StringVal("./test-fixtures/null_outputs.tfstate"),
{ }),
Config: testAccState_backend, "outputs": cty.ObjectVal(map[string]cty.Value{
Check: resource.ComposeTestCheckFunc( "map": cty.NullVal(cty.DynamicPseudoType),
testAccCheckStateValue( "list": cty.NullVal(cty.DynamicPseudoType),
"data.terraform_remote_state.foo", "foo", "bar"), }),
), }),
false,
}, },
{ // 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,
}, },
}) }
for _, test := range tests {
schema := dataSourceRemoteStateGetSchema().Block
config, err := schema.CoerceValue(test.Config)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
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)
} }
func TestState_complexOutputs(t *testing.T) { if !got.RawEquals(test.Want) {
resource.UnitTest(t, resource.TestCase{ t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
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"),
),
},
},
})
}
// 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,
},
},
})
}
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)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}
v := rs.Primary.Attributes[name]
if v != value {
return fmt.Errorf(
"Value for %s is %s, not %s", name, v, value)
}
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"
}
}`

View File

@ -1,21 +1,124 @@
package terraform package terraform
import ( import (
"github.com/hashicorp/terraform/helper/schema" "fmt"
"github.com/hashicorp/terraform/terraform" "log"
"github.com/hashicorp/terraform/providers"
) )
// Provider returns a terraform.ResourceProvider. // Provider is an implementation of providers.Interface
func Provider() terraform.ResourceProvider { type Provider struct {
return &schema.Provider{ // Provider is the schema for the provider itself.
ResourcesMap: map[string]*schema.Resource{ Schema providers.Schema
"terraform_remote_state": schema.DataSourceResourceShim(
"terraform_remote_state", // DataSources maps the data source name to that data source's schema.
dataSourceRemoteState(), DataSources map[string]providers.Schema
), }
},
DataSourcesMap: map[string]*schema.Resource{ // NewProvider returns a new terraform provider
"terraform_remote_state": dataSourceRemoteState(), 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
}

View File

@ -3,32 +3,21 @@ package terraform
import ( import (
"testing" "testing"
backendInit "github.com/hashicorp/terraform/backend/init" "github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
) )
var testAccProviders map[string]terraform.ResourceProvider var testAccProviders map[string]*Provider
var testAccProvider *schema.Provider var testAccProvider *Provider
func init() { func init() {
// Initialize the backends testAccProvider = NewProvider()
backendInit.Init(nil) testAccProviders = map[string]*Provider{
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"terraform": testAccProvider, "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) { func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider() var _ providers.Interface = NewProvider()
} }
func testAccPreCheck(t *testing.T) { func testAccPreCheck(t *testing.T) {