terraform/internal/builtin/providers/terraform/data_source_state.go

259 lines
7.6 KiB
Go

package terraform
import (
"fmt"
"log"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/backend/remote"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
backendInit "github.com/hashicorp/terraform/internal/backend/init"
)
func dataSourceRemoteStateGetSchema() providers.Schema {
return providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"backend": {
Type: cty.String,
Description: "The remote backend to use, e.g. `remote` or `http`.",
DescriptionKind: configschema.StringMarkdown,
Required: true,
},
"config": {
Type: cty.DynamicPseudoType,
Description: "The configuration of the remote backend. " +
"Although this is optional, most backends require " +
"some configuration.\n\n" +
"The object can use any arguments that would be valid " +
"in the equivalent `terraform { backend \"<TYPE>\" { ... } }` " +
"block.",
DescriptionKind: configschema.StringMarkdown,
Optional: true,
},
"defaults": {
Type: cty.DynamicPseudoType,
Description: "Default values for outputs, in case " +
"the state file is empty or lacks a required output.",
DescriptionKind: configschema.StringMarkdown,
Optional: true,
},
"outputs": {
Type: cty.DynamicPseudoType,
Description: "An object containing every root-level " +
"output in the remote state.",
DescriptionKind: configschema.StringMarkdown,
Computed: true,
},
"workspace": {
Type: cty.String,
Description: "The Terraform workspace to use, if " +
"the backend supports workspaces.",
DescriptionKind: configschema.StringMarkdown,
Optional: true,
},
},
},
}
}
func dataSourceRemoteStateValidate(cfg cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
// Getting the backend implicitly validates the configuration for it,
// but we can only do that if it's all known already.
if cfg.GetAttr("config").IsWhollyKnown() && cfg.GetAttr("backend").IsKnown() {
_, _, moreDiags := getBackend(cfg)
diags = diags.Append(moreDiags)
} else {
// Otherwise we'll just type-check the config object itself.
configTy := cfg.GetAttr("config").Type()
if configTy != cty.DynamicPseudoType && !(configTy.IsObjectType() || configTy.IsMapType()) {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid backend configuration",
"The configuration must be an object value.",
cty.GetAttrPath("config"),
))
}
}
{
defaultsTy := cfg.GetAttr("defaults").Type()
if defaultsTy != cty.DynamicPseudoType && !(defaultsTy.IsObjectType() || defaultsTy.IsMapType()) {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid default values",
"Defaults must be given in an object value.",
cty.GetAttrPath("defaults"),
))
}
}
return diags
}
func dataSourceRemoteStateRead(d cty.Value) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
b, cfg, moreDiags := getBackend(d)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return cty.NilVal, diags
}
configureDiags := b.Configure(cfg)
if configureDiags.HasErrors() {
diags = diags.Append(configureDiags.Err())
return cty.NilVal, diags
}
newState := make(map[string]cty.Value)
newState["backend"] = d.GetAttr("backend")
newState["config"] = d.GetAttr("config")
workspaceVal := d.GetAttr("workspace")
// This attribute is not computed, so we always have to store the state
// value, even if we implicitly use a default.
newState["workspace"] = workspaceVal
workspaceName := backend.DefaultStateName
if !workspaceVal.IsNull() {
workspaceName = workspaceVal.AsString()
}
state, err := b.StateMgr(workspaceName)
if err != nil {
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 {
diags = diags.Append(err)
return cty.NilVal, diags
}
outputs := make(map[string]cty.Value)
if defaultsVal := d.GetAttr("defaults"); !defaultsVal.IsNull() {
newState["defaults"] = defaultsVal
it := defaultsVal.ElementIterator()
for it.Next() {
k, v := it.Element()
outputs[k.AsString()] = v
}
} else {
newState["defaults"] = cty.NullVal(cty.DynamicPseudoType)
}
remoteState := state.State()
if remoteState == nil {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Unable to find remote state",
"No stored state was found for the given workspace in the given backend.",
cty.Path(nil).GetAttr("workspace"),
))
newState["outputs"] = cty.EmptyObjectVal
return cty.ObjectVal(newState), diags
}
mod := remoteState.RootModule()
if mod != nil { // should always have a root module in any valid state
for k, os := range mod.OutputValues {
outputs[k] = os.Value
}
}
newState["outputs"] = cty.ObjectVal(outputs)
return cty.ObjectVal(newState), diags
}
func getBackend(cfg cty.Value) (backend.Backend, cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
backendType := cfg.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 := getBackendFactory(backendType)
if f == nil {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid backend configuration",
fmt.Sprintf("There is no backend type named %q.", backendType),
cty.Path(nil).GetAttr("backend"),
))
return nil, cty.NilVal, diags
}
b := f()
config := cfg.GetAttr("config")
if config.IsNull() {
// We'll treat this as an empty configuration and see if the backend's
// schema and validation code will accept it.
config = cty.EmptyObjectVal
}
if config.Type().IsMapType() { // The code below expects an object type, so we'll convert
config = cty.ObjectVal(config.AsValueMap())
}
schema := b.ConfigSchema()
// Try to coerce the provided value into the desired configuration type.
configVal, err := schema.CoerceValue(config)
if err != nil {
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 nil, cty.NilVal, diags
}
newVal, validateDiags := b.PrepareConfig(configVal)
diags = diags.Append(validateDiags)
if validateDiags.HasErrors() {
return nil, cty.NilVal, diags
}
// If this is the enhanced remote backend, we want to disable the version
// check, because this is a read-only operation
if rb, ok := b.(*remote.Remote); ok {
rb.IgnoreVersionConflict()
}
return b, newVal, diags
}
// overrideBackendFactories allows test cases to control the set of available
// backends to allow for more self-contained tests. This should never be set
// in non-test code.
var overrideBackendFactories map[string]backend.InitFn
func getBackendFactory(backendType string) backend.InitFn {
if len(overrideBackendFactories) > 0 {
// Tests may override the set of backend factories.
return overrideBackendFactories[backendType]
}
return backendInit.Backend(backendType)
}