mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-18 18:29:44 -05:00
287 lines
9.7 KiB
Go
287 lines
9.7 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package backendbase
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
)
|
|
|
|
// SDKLikeData offers an approximation of the legack SDK "ResourceData" API
|
|
// as a stopgap measure to help migrate all of the remote state backend
|
|
// implementations away from the legacy SDK.
|
|
//
|
|
// It's designed to wrap an object returned by [Base.PrepareConfig] which
|
|
// should therefore already have a fixed, known data type. Therefore the
|
|
// methods assume that the caller already knows what type each attribute
|
|
// should have and will panic if a caller asks for an incompatible type.
|
|
type SDKLikeData struct {
|
|
v cty.Value
|
|
}
|
|
|
|
func NewSDKLikeData(v cty.Value) SDKLikeData {
|
|
return SDKLikeData{v}
|
|
}
|
|
|
|
// String extracts a string attribute from a configuration object
|
|
// in a similar way to how the legacy SDK would interpret an attribute
|
|
// of type schema.TypeString, or panics if the wrapped object isn't of a
|
|
// suitable type.
|
|
func (d SDKLikeData) String(attrPath string) string {
|
|
v := d.GetAttr(attrPath, cty.String)
|
|
if v.IsNull() {
|
|
return ""
|
|
}
|
|
return v.AsString()
|
|
}
|
|
|
|
// Int extracts a string attribute from a configuration object
|
|
// in a similar way to how the legacy SDK would interpret an attribute
|
|
// of type schema.TypeInt, or panics if the wrapped object isn't of a
|
|
// suitable type.
|
|
//
|
|
// Since the Terraform language does not have an integers-only type, this
|
|
// can fail dynamically (returning an error) if the given value has a
|
|
// fractional component.
|
|
func (d SDKLikeData) Int64(attrPath string) (int64, error) {
|
|
// Legacy SDK used strconv.ParseInt to interpret values, so we'll
|
|
// follow its lead here for maximal compatibility.
|
|
v := d.GetAttr(attrPath, cty.String)
|
|
if v.IsNull() {
|
|
return 0, nil
|
|
}
|
|
return strconv.ParseInt(v.AsString(), 0, 0)
|
|
}
|
|
|
|
// Bool extracts a string attribute from a configuration object
|
|
// in a similar way to how the legacy SDK would interpret an attribute
|
|
// of type schema.TypeBool, or panics if the wrapped object isn't of a
|
|
// suitable type.
|
|
func (d SDKLikeData) Bool(attrPath string) bool {
|
|
// Legacy SDK used strconv.ParseBool to interpret values, but it
|
|
// did so only after the configuration was interpreted by HCL and
|
|
// thus HCL's more constrained definition of bool still "won",
|
|
// and we follow that tradition here.
|
|
v := d.GetAttr(attrPath, cty.Bool)
|
|
if v.IsNull() {
|
|
return false
|
|
}
|
|
return v.True()
|
|
}
|
|
|
|
// GetAttr is just a thin wrapper around [cty.Path.Apply] that accepts
|
|
// a legacy-SDK-like dot-separated string as attribute path, instead of
|
|
// a [cty.Path] directly.
|
|
//
|
|
// It uses [SDKLikePath] to interpret the given path, and so the limitations
|
|
// of that function apply equally to this function.
|
|
//
|
|
// This function will panic if asked to extract a path that isn't compatible
|
|
// with the object type of the enclosed value.
|
|
func (d SDKLikeData) GetAttr(attrPath string, wantType cty.Type) cty.Value {
|
|
path := SDKLikePath(attrPath)
|
|
v, err := path.Apply(d.v)
|
|
if err != nil {
|
|
panic("invalid attribute path: " + err.Error())
|
|
}
|
|
v, err = convert.Convert(v, wantType)
|
|
if err != nil {
|
|
panic("incorrect attribute type: " + err.Error())
|
|
}
|
|
return v
|
|
}
|
|
|
|
// SDKLikePath interprets a subset of the legacy SDK attribute path syntax --
|
|
// identifiers separated by dots -- into a cty.Path.
|
|
//
|
|
// This is designed only for migrating historical remote system backends that
|
|
// were originally written using the SDK, and so it's limited only to the
|
|
// simple cases they use. It's not suitable for the more complex legacy SDK
|
|
// uses made by Terraform providers.
|
|
func SDKLikePath(rawPath string) cty.Path {
|
|
var ret cty.Path
|
|
remain := rawPath
|
|
for {
|
|
dot := strings.IndexByte(remain, '.')
|
|
last := false
|
|
if dot == -1 {
|
|
dot = len(remain)
|
|
last = true
|
|
}
|
|
|
|
attrName := remain[:dot]
|
|
ret = append(ret, cty.GetAttrStep{Name: attrName})
|
|
if last {
|
|
return ret
|
|
}
|
|
remain = remain[dot+1:]
|
|
}
|
|
}
|
|
|
|
// SDKLikeEnvDefault emulates an SDK-style "EnvDefaultFunc" by taking the
|
|
// result of [SDKLikeData.String] and a series of environment variable names.
|
|
//
|
|
// If the given string is already non-empty then it just returns it directly.
|
|
// Otherwise it returns the value of the first environment variable that has
|
|
// a non-empty value. If everything turns out empty, the result is an empty
|
|
// string.
|
|
func SDKLikeEnvDefault(v string, envNames ...string) string {
|
|
if v == "" {
|
|
for _, envName := range envNames {
|
|
v = os.Getenv(envName)
|
|
if v != "" {
|
|
return v
|
|
}
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// SDKLikeRequiredWithEnvDefault is a convenience wrapper around
|
|
// [SDKLikeEnvDefault] which returns an error if the result is still the
|
|
// empty string even after trying all of the fallback environment variables.
|
|
//
|
|
// This wrapper requires an additional argument specifying the attribute name
|
|
// just because that becomes part of the returned error message.
|
|
func SDKLikeRequiredWithEnvDefault(attrPath string, v string, envNames ...string) (string, error) {
|
|
ret := SDKLikeEnvDefault(v, envNames...)
|
|
if ret == "" {
|
|
return "", fmt.Errorf("attribute %q is required", attrPath)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// SDKLikeDefaults captures legacy-SDK-like default values to help fill the
|
|
// gap in abstraction level between the legacy SDK and Terraform's own
|
|
// configuration schema model.
|
|
type SDKLikeDefaults map[string]SDKLikeDefault
|
|
|
|
type SDKLikeDefault struct {
|
|
EnvVars []string
|
|
Fallback string
|
|
|
|
// Required is for situations where an argument is optional to set
|
|
// in the configuration but _must_ eventually be set through the
|
|
// combination of the configuration and the environment variables
|
|
// in this object.
|
|
//
|
|
// It doesn't make sense to set Fallback non-empty when this flag is
|
|
// set, because an attribute with a non-empty fallback is always
|
|
// effectively present.
|
|
Required bool
|
|
}
|
|
|
|
// ApplyTo is a convenience helper that allows inserting default
|
|
// values from environment variables into many different string attributes of
|
|
// an object value all at once, approximating what the legacy SDK would've
|
|
// done when the schema included an "EnvDefaultFunc".
|
|
//
|
|
// Like all of the "SDK-like" helpers. this expects that the base object has
|
|
// already been coerced into the correct type for a backend's schema and
|
|
// so this will panic if any of the keys in envVars do not match existing
|
|
// attributes in base, and if the value in any of those attributes is not
|
|
// of a cty primitive type.
|
|
func (d SDKLikeDefaults) ApplyTo(base cty.Value) (cty.Value, error) {
|
|
attrTypes := base.Type().AttributeTypes()
|
|
retAttrs := make(map[string]cty.Value, len(attrTypes))
|
|
for attrName, ty := range attrTypes {
|
|
defs, hasDefs := d[attrName]
|
|
givenVal := base.GetAttr(attrName)
|
|
if !hasDefs {
|
|
// Just pass through verbatim any attributes that are not
|
|
// accounted for in our defaults.
|
|
retAttrs[attrName] = givenVal
|
|
continue
|
|
}
|
|
|
|
// The legacy SDK shims convert all values into strings (for flatmap)
|
|
// and then do their work in terms of that, so we'll follow suit here.
|
|
vStr, err := convert.Convert(givenVal, cty.String)
|
|
if err != nil {
|
|
panic("cannot apply environment variable defaults for " + ty.GoString())
|
|
}
|
|
|
|
rawStr := ""
|
|
if !vStr.IsNull() {
|
|
rawStr = vStr.AsString()
|
|
}
|
|
|
|
if rawStr == "" {
|
|
for _, envName := range defs.EnvVars {
|
|
rawStr = os.Getenv(envName)
|
|
if rawStr != "" {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if rawStr == "" {
|
|
rawStr = defs.Fallback
|
|
}
|
|
if defs.Required && rawStr == "" {
|
|
return cty.NilVal, fmt.Errorf("argument %q is required", attrName)
|
|
}
|
|
|
|
// As a special case, if we still have an empty string and the original
|
|
// value was null then we'll preserve the null. This is a compromise,
|
|
// assuming that SDKLikeData knows how to treat a null value as a
|
|
// zero value anyway and if we preserve the null then the recipient
|
|
// of this result can still use the cty.Value result directly to
|
|
// distinguish between the value being set explicitly to empty in
|
|
// the config vs. being entirely unset.
|
|
if rawStr == "" && givenVal.IsNull() {
|
|
retAttrs[attrName] = givenVal
|
|
continue
|
|
}
|
|
|
|
// By the time we get here, rawStr should be empty only if the original
|
|
// value was unset and all of the fallback environment variables were
|
|
// also unset. Otherwise, rawStr contains a string representation of
|
|
// a value that we now need to convert back to the type that was
|
|
// originally wanted.
|
|
switch ty {
|
|
case cty.String:
|
|
retAttrs[attrName] = cty.StringVal(rawStr)
|
|
case cty.Bool:
|
|
if rawStr == "" {
|
|
rawStr = "false"
|
|
}
|
|
|
|
// Legacy SDK uses strconv.ParseBool and therefore tolerates a
|
|
// variety of different string representations of true and false,
|
|
// so we'll do the same here. The config itself can't use those
|
|
// alternate forms because HCL's definition of bool prevails there,
|
|
// but the environment variables can use any of these forms.
|
|
bv, err := strconv.ParseBool(rawStr)
|
|
if err != nil {
|
|
return cty.NilVal, fmt.Errorf("invalid value for %q: %s", attrName, err)
|
|
}
|
|
retAttrs[attrName] = cty.BoolVal(bv)
|
|
case cty.Number:
|
|
if rawStr == "" {
|
|
rawStr = "0"
|
|
}
|
|
|
|
// This case is a little trickier because cty.Number could be
|
|
// representing either an integer or a float, which each have
|
|
// different interpretations in the legacy SDK. Therefore we'll
|
|
// try integer first and use its result if successful, but then
|
|
// try float as a fallback if not.
|
|
if iv, err := strconv.ParseInt(rawStr, 0, 0); err == nil {
|
|
retAttrs[attrName] = cty.NumberIntVal(iv)
|
|
} else if fv, err := strconv.ParseFloat(rawStr, 64); err == nil {
|
|
retAttrs[attrName] = cty.NumberFloatVal(fv)
|
|
} else {
|
|
return cty.NilVal, fmt.Errorf("invalid value for %q: must be a number", attrName)
|
|
}
|
|
default:
|
|
panic("cannot apply environment variable defaults for " + ty.GoString())
|
|
}
|
|
}
|
|
return cty.ObjectVal(retAttrs), nil
|
|
}
|