opentofu/terraform/resource.go
Martin Atkins b0da5b1ce5 core: Remove the last few HIL remnants
We've not been using HIL in the main codepaths since Terraform 0.12, but
some references to it (and some supporting functionality in Terraform)
stuck around due to interactions with types we'd kept around to support
legacy shims.

However, removing the configs.RawConfig field from
terraform.ResourceConfig disconnects that subtree of dependencies from
everything else, allowing us to remove it. This is safe because the only
remaining uses of terraform.ResourceConfig are shims from values that
were already evaluated using the HCL 2 API, and thus they never need
the "just in time" HIL evaluation that ResourceConfig.interpolateForce
used to do.

We also had some HIL references in configs/hcl2shim that were previously
in support of the "terraform 0.12upgrade" command, but the implementation
of that command is now removed.

There was one remaining reference to HIL in a now-unused function in the
helper/schema package, which I removed entirely here.

This then allows us to remove the HIL dependency entirely, and also to
clean up some remaining old remants of the legacy "config" package that
we'd recently moved into the "configs" package pending further pruning.
2020-09-02 15:53:33 -07:00

517 lines
15 KiB
Go

package terraform
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/mitchellh/copystructure"
"github.com/mitchellh/reflectwalk"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/configs/hcl2shim"
)
// Resource is a legacy way to identify a particular resource instance.
//
// New code should use addrs.ResourceInstance instead. This is still here
// only for codepaths that haven't been updated yet.
type Resource struct {
// These are all used by the new EvalNode stuff.
Name string
Type string
CountIndex int
// These aren't really used anymore anywhere, but we keep them around
// since we haven't done a proper cleanup yet.
Id string
Info *InstanceInfo
Config *ResourceConfig
Dependencies []string
Diff *InstanceDiff
Provider ResourceProvider
State *InstanceState
Flags ResourceFlag
}
// NewResource constructs a legacy Resource object from an
// addrs.ResourceInstance value.
//
// This is provided to shim to old codepaths that haven't been updated away
// from this type yet. Since this old type is not able to represent instances
// that have string keys, this function will panic if given a resource address
// that has a string key.
func NewResource(addr addrs.ResourceInstance) *Resource {
ret := &Resource{
Name: addr.Resource.Name,
Type: addr.Resource.Type,
}
if addr.Key != addrs.NoKey {
switch tk := addr.Key.(type) {
case addrs.IntKey:
ret.CountIndex = int(tk)
default:
panic(fmt.Errorf("resource instance with key %#v is not supported", addr.Key))
}
}
return ret
}
// ResourceKind specifies what kind of instance we're working with, whether
// its a primary instance, a tainted instance, or an orphan.
type ResourceFlag byte
// InstanceInfo is used to hold information about the instance and/or
// resource being modified.
type InstanceInfo struct {
// Id is a unique name to represent this instance. This is not related
// to InstanceState.ID in any way.
Id string
// ModulePath is the complete path of the module containing this
// instance.
ModulePath []string
// Type is the resource type of this instance
Type string
// uniqueExtra is an internal field that can be populated to supply
// extra metadata that is used to identify a unique instance in
// the graph walk. This will be appended to HumanID when uniqueId
// is called.
uniqueExtra string
}
// NewInstanceInfo constructs an InstanceInfo from an addrs.AbsResourceInstance.
//
// InstanceInfo is a legacy type, and uses of it should be gradually replaced
// by direct use of addrs.AbsResource or addrs.AbsResourceInstance as
// appropriate.
//
// The legacy InstanceInfo type cannot represent module instances with instance
// keys, so this function will panic if given such a path. Uses of this type
// should all be removed or replaced before implementing "count" and "for_each"
// arguments on modules in order to avoid such panics.
//
// This legacy type also cannot represent resource instances with string
// instance keys. It will panic if the given key is not either NoKey or an
// IntKey.
func NewInstanceInfo(addr addrs.AbsResourceInstance) *InstanceInfo {
// We need an old-style []string module path for InstanceInfo.
path := make([]string, len(addr.Module))
for i, step := range addr.Module {
if step.InstanceKey != addrs.NoKey {
panic("NewInstanceInfo cannot convert module instance with key")
}
path[i] = step.Name
}
// This is a funny old meaning of "id" that is no longer current. It should
// not be used for anything users might see. Note that it does not include
// a representation of the resource mode, and so it's impossible to
// determine from an InstanceInfo alone whether it is a managed or data
// resource that is being referred to.
id := fmt.Sprintf("%s.%s", addr.Resource.Resource.Type, addr.Resource.Resource.Name)
if addr.Resource.Resource.Mode == addrs.DataResourceMode {
id = "data." + id
}
if addr.Resource.Key != addrs.NoKey {
switch k := addr.Resource.Key.(type) {
case addrs.IntKey:
id = id + fmt.Sprintf(".%d", int(k))
default:
panic(fmt.Sprintf("NewInstanceInfo cannot convert resource instance with %T instance key", addr.Resource.Key))
}
}
return &InstanceInfo{
Id: id,
ModulePath: path,
Type: addr.Resource.Resource.Type,
}
}
// ResourceAddress returns the address of the resource that the receiver is describing.
func (i *InstanceInfo) ResourceAddress() *ResourceAddress {
// GROSS: for tainted and deposed instances, their status gets appended
// to i.Id to create a unique id for the graph node. Historically these
// ids were displayed to the user, so it's designed to be human-readable:
// "aws_instance.bar.0 (deposed #0)"
//
// So here we detect such suffixes and try to interpret them back to
// their original meaning so we can then produce a ResourceAddress
// with a suitable InstanceType.
id := i.Id
instanceType := TypeInvalid
if idx := strings.Index(id, " ("); idx != -1 {
remain := id[idx:]
id = id[:idx]
switch {
case strings.Contains(remain, "tainted"):
instanceType = TypeTainted
case strings.Contains(remain, "deposed"):
instanceType = TypeDeposed
}
}
addr, err := parseResourceAddressInternal(id)
if err != nil {
// should never happen, since that would indicate a bug in the
// code that constructed this InstanceInfo.
panic(fmt.Errorf("InstanceInfo has invalid Id %s", id))
}
if len(i.ModulePath) > 1 {
addr.Path = i.ModulePath[1:] // trim off "root" prefix, which is implied
}
if instanceType != TypeInvalid {
addr.InstanceTypeSet = true
addr.InstanceType = instanceType
}
return addr
}
// ResourceConfig is a legacy type that was formerly used to represent
// interpolatable configuration blocks. It is now only used to shim to old
// APIs that still use this type, via NewResourceConfigShimmed.
type ResourceConfig struct {
ComputedKeys []string
Raw map[string]interface{}
Config map[string]interface{}
}
// NewResourceConfigRaw constructs a ResourceConfig whose content is exactly
// the given value.
//
// The given value may contain hcl2shim.UnknownVariableValue to signal that
// something is computed, but it must not contain unprocessed interpolation
// sequences as we might've seen in Terraform v0.11 and prior.
func NewResourceConfigRaw(raw map[string]interface{}) *ResourceConfig {
v := hcl2shim.HCL2ValueFromConfigValue(raw)
// This is a little weird but we round-trip the value through the hcl2shim
// package here for two reasons: firstly, because that reduces the risk
// of it including something unlike what NewResourceConfigShimmed would
// produce, and secondly because it creates a copy of "raw" just in case
// something is relying on the fact that in the old world the raw and
// config maps were always distinct, and thus you could in principle mutate
// one without affecting the other. (I sure hope nobody was doing that, though!)
cfg := hcl2shim.ConfigValueFromHCL2(v).(map[string]interface{})
return &ResourceConfig{
Raw: raw,
Config: cfg,
ComputedKeys: newResourceConfigShimmedComputedKeys(v, ""),
}
}
// NewResourceConfigShimmed wraps a cty.Value of object type in a legacy
// ResourceConfig object, so that it can be passed to older APIs that expect
// this wrapping.
//
// The returned ResourceConfig is already interpolated and cannot be
// re-interpolated. It is, therefore, useful only to functions that expect
// an already-populated ResourceConfig which they then treat as read-only.
//
// If the given value is not of an object type that conforms to the given
// schema then this function will panic.
func NewResourceConfigShimmed(val cty.Value, schema *configschema.Block) *ResourceConfig {
if !val.Type().IsObjectType() {
panic(fmt.Errorf("NewResourceConfigShimmed given %#v; an object type is required", val.Type()))
}
ret := &ResourceConfig{}
legacyVal := hcl2shim.ConfigValueFromHCL2Block(val, schema)
if legacyVal != nil {
ret.Config = legacyVal
// Now we need to walk through our structure and find any unknown values,
// producing the separate list ComputedKeys to represent these. We use the
// schema here so that we can preserve the expected invariant
// that an attribute is always either wholly known or wholly unknown, while
// a child block can be partially unknown.
ret.ComputedKeys = newResourceConfigShimmedComputedKeys(val, "")
} else {
ret.Config = make(map[string]interface{})
}
ret.Raw = ret.Config
return ret
}
// Record the any config values in ComputedKeys. This field had been unused in
// helper/schema, but in the new protocol we're using this so that the SDK can
// now handle having an unknown collection. The legacy diff code doesn't
// properly handle the unknown, because it can't be expressed in the same way
// between the config and diff.
func newResourceConfigShimmedComputedKeys(val cty.Value, path string) []string {
var ret []string
ty := val.Type()
if val.IsNull() {
return ret
}
if !val.IsKnown() {
// we shouldn't have an entirely unknown resource, but prevent empty
// strings just in case
if len(path) > 0 {
ret = append(ret, path)
}
return ret
}
if path != "" {
path += "."
}
switch {
case ty.IsListType(), ty.IsTupleType(), ty.IsSetType():
i := 0
for it := val.ElementIterator(); it.Next(); i++ {
_, subVal := it.Element()
keys := newResourceConfigShimmedComputedKeys(subVal, fmt.Sprintf("%s%d", path, i))
ret = append(ret, keys...)
}
case ty.IsMapType(), ty.IsObjectType():
for it := val.ElementIterator(); it.Next(); {
subK, subVal := it.Element()
keys := newResourceConfigShimmedComputedKeys(subVal, fmt.Sprintf("%s%s", path, subK.AsString()))
ret = append(ret, keys...)
}
}
return ret
}
// DeepCopy performs a deep copy of the configuration. This makes it safe
// to modify any of the structures that are part of the resource config without
// affecting the original configuration.
func (c *ResourceConfig) DeepCopy() *ResourceConfig {
// DeepCopying a nil should return a nil to avoid panics
if c == nil {
return nil
}
// Copy, this will copy all the exported attributes
copy, err := copystructure.Config{Lock: true}.Copy(c)
if err != nil {
panic(err)
}
// Force the type
result := copy.(*ResourceConfig)
return result
}
// Equal checks the equality of two resource configs.
func (c *ResourceConfig) Equal(c2 *ResourceConfig) bool {
// If either are nil, then they're only equal if they're both nil
if c == nil || c2 == nil {
return c == c2
}
// Sort the computed keys so they're deterministic
sort.Strings(c.ComputedKeys)
sort.Strings(c2.ComputedKeys)
// Two resource configs if their exported properties are equal.
// We don't compare "raw" because it is never used again after
// initialization and for all intents and purposes they are equal
// if the exported properties are equal.
check := [][2]interface{}{
{c.ComputedKeys, c2.ComputedKeys},
{c.Raw, c2.Raw},
{c.Config, c2.Config},
}
for _, pair := range check {
if !reflect.DeepEqual(pair[0], pair[1]) {
return false
}
}
return true
}
// CheckSet checks that the given list of configuration keys is
// properly set. If not, errors are returned for each unset key.
//
// This is useful to be called in the Validate method of a ResourceProvider.
func (c *ResourceConfig) CheckSet(keys []string) []error {
var errs []error
for _, k := range keys {
if !c.IsSet(k) {
errs = append(errs, fmt.Errorf("%s must be set", k))
}
}
return errs
}
// Get looks up a configuration value by key and returns the value.
//
// The second return value is true if the get was successful. Get will
// return the raw value if the key is computed, so you should pair this
// with IsComputed.
func (c *ResourceConfig) Get(k string) (interface{}, bool) {
// We aim to get a value from the configuration. If it is computed,
// then we return the pure raw value.
source := c.Config
if c.IsComputed(k) {
source = c.Raw
}
return c.get(k, source)
}
// GetRaw looks up a configuration value by key and returns the value,
// from the raw, uninterpolated config.
//
// The second return value is true if the get was successful. Get will
// not succeed if the value is being computed.
func (c *ResourceConfig) GetRaw(k string) (interface{}, bool) {
return c.get(k, c.Raw)
}
// IsComputed returns whether the given key is computed or not.
func (c *ResourceConfig) IsComputed(k string) bool {
// The next thing we do is check the config if we get a computed
// value out of it.
v, ok := c.get(k, c.Config)
if !ok {
return false
}
// If value is nil, then it isn't computed
if v == nil {
return false
}
// Test if the value contains an unknown value
var w unknownCheckWalker
if err := reflectwalk.Walk(v, &w); err != nil {
panic(err)
}
return w.Unknown
}
// IsSet checks if the key in the configuration is set. A key is set if
// it has a value or the value is being computed (is unknown currently).
//
// This function should be used rather than checking the keys of the
// raw configuration itself, since a key may be omitted from the raw
// configuration if it is being computed.
func (c *ResourceConfig) IsSet(k string) bool {
if c == nil {
return false
}
if c.IsComputed(k) {
return true
}
if _, ok := c.Get(k); ok {
return true
}
return false
}
func (c *ResourceConfig) get(
k string, raw map[string]interface{}) (interface{}, bool) {
parts := strings.Split(k, ".")
if len(parts) == 1 && parts[0] == "" {
parts = nil
}
var current interface{} = raw
var previous interface{} = nil
for i, part := range parts {
if current == nil {
return nil, false
}
cv := reflect.ValueOf(current)
switch cv.Kind() {
case reflect.Map:
previous = current
v := cv.MapIndex(reflect.ValueOf(part))
if !v.IsValid() {
if i > 0 && i != (len(parts)-1) {
tryKey := strings.Join(parts[i:], ".")
v := cv.MapIndex(reflect.ValueOf(tryKey))
if !v.IsValid() {
return nil, false
}
return v.Interface(), true
}
return nil, false
}
current = v.Interface()
case reflect.Slice:
previous = current
if part == "#" {
// If any value in a list is computed, this whole thing
// is computed and we can't read any part of it.
for i := 0; i < cv.Len(); i++ {
if v := cv.Index(i).Interface(); v == hcl2shim.UnknownVariableValue {
return v, true
}
}
current = cv.Len()
} else {
i, err := strconv.ParseInt(part, 0, 0)
if err != nil {
return nil, false
}
if int(i) < 0 || int(i) >= cv.Len() {
return nil, false
}
current = cv.Index(int(i)).Interface()
}
case reflect.String:
// This happens when map keys contain "." and have a common
// prefix so were split as path components above.
actualKey := strings.Join(parts[i-1:], ".")
if prevMap, ok := previous.(map[string]interface{}); ok {
v, ok := prevMap[actualKey]
return v, ok
}
return nil, false
default:
panic(fmt.Sprintf("Unknown kind: %s", cv.Kind()))
}
}
return current, true
}
// unknownCheckWalker
type unknownCheckWalker struct {
Unknown bool
}
func (w *unknownCheckWalker) Primitive(v reflect.Value) error {
if v.Interface() == hcl2shim.UnknownVariableValue {
w.Unknown = true
}
return nil
}