| // Copyright (c) HashiCorp, Inc. |
| // 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 |
| } |