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

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

View File

@ -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"
}
}`

View File

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

View File

@ -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) {