| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| 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 { |
| detail := fmt.Sprintf("There is no backend type named %q.", backendType) |
| if msg, removed := backendInit.RemovedBackends[backendType]; removed { |
| detail = msg |
| } |
| |
| diags = diags.Append(tfdiags.AttributeValue( |
| tfdiags.Error, |
| "Invalid backend configuration", |
| detail, |
| 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) |
| } |