diff --git a/internal/stacks/stackstate/statekeys/components.go b/internal/stacks/stackstate/statekeys/components.go new file mode 100644 index 0000000000..6f020f21e2 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/components.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +type ComponentInstance struct { + ComponentInstanceAddr stackaddrs.AbsComponentInstance +} + +func parseComponentInstance(s string) (Key, error) { + addrRaw, ok := finalKeyField(s) + if !ok { + return nil, fmt.Errorf("unsupported extra field in component instance key") + } + addr, diags := stackaddrs.ParseAbsComponentInstanceStr(addrRaw) + if diags.HasErrors() { + return nil, fmt.Errorf("component instance key has invalid component instance address %q", addrRaw) + } + return ComponentInstance{ + ComponentInstanceAddr: addr, + }, nil +} + +func (k ComponentInstance) KeyType() KeyType { + return ComponentInstanceType +} + +func (k ComponentInstance) rawSuffix() string { + return k.ComponentInstanceAddr.String() +} diff --git a/internal/stacks/stackstate/statekeys/doc.go b/internal/stacks/stackstate/statekeys/doc.go new file mode 100644 index 0000000000..856a1d2f0d --- /dev/null +++ b/internal/stacks/stackstate/statekeys/doc.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package statekeys contains the definitions for the various different kinds +// of tracking key we use (or have historically used) for objects in a stack +// state. +// +// Stack state is a mutable data structure whose storage strategy is delegated +// to whatever is calling into Terraform Core. To allow Terraform Core to +// emit updates to that data structure piecemeal, rather than having to return +// the whole dataset over and over, we use tracking keys for each +// separately-updatable element of the state that are opaque to the caller but +// meaningful to Terraform Core. +// +// Callers are expected to use simple character-for-character string matching +// to compare these to recognize whether an update is describing an entirely +// new object or a replacement for ane existing object, and so the main +// requirement is that the content of these keys remains consistent across +// Terraform Core releases. However, from Terraform Core's perspective we +// also use these keys to carry some metadata about what is being tracked +// so we can avoid redundantly storing the same information in both the key +// and in the associated stored object. +// +// The keys defined in this package are in principle valid for use both as +// raw state keys and as external description keys, but some of them are used +// only for one or the other since the raw and external description forms +// don't necessarily have the same level of detail. +package statekeys diff --git a/internal/stacks/stackstate/statekeys/key.go b/internal/stacks/stackstate/statekeys/key.go new file mode 100644 index 0000000000..6b647de189 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +// Key is implemented by types that can be used as state keys. +type Key interface { + // KeyType returns the [KeyType] used for keys belonging to a particular + // implementation of [Key]. + KeyType() KeyType + + // rawSuffix returns additional characters that should appear after the + // key type portion of the final raw key. + // + // This is unexported both to help prevent accidental misuse (external + // callers MUST use [String] to obtain the correct string representation], + // and to prevent implementations of this interface from other packages. + // This package is the sole authority on state keys. + rawSuffix() string +} + +// String returns the string representation of the given key, ready to be used +// in the RPC API representation of a [stackstate.AppliedChange] object. +func String(k Key) string { + if k == nil { + panic("called statekeys.String with nil Key") + } + return string(k.KeyType()) + k.rawSuffix() +} + +// RecognizedType returns true if the given key has a [KeyType] that's known +// to the current version of this package, or false otherwise. +// +// If RecognizedType returns false, use the key's KeyType method to obtain +// the unrecognized type and then use its UnrecognizedKeyHandling method +// to determine the appropriate handling for the unrecognized key type. +func RecognizedType(k Key) bool { + if k == nil { + panic("called statekeys.RecognizedType with nil Key") + } + _, unrecognized := k.(Unrecognized) + return !unrecognized +} + +// Unrecognized is a fallback [Key] implementation used when a given +// key has an unrecognized type. +// +// Unrecognized keys are round-trippable in that the RawKey method will return +// the same string that was originally parsed. Use +// KeyType.UnrecognizedKeyHandling to determine how Terraform Core should +// respond to the key having an unrecognized type. +type Unrecognized struct { + // ApparentKeyType is a [KeyType] representation of the type portion of the + // unrecognized key. Unlike most other [KeyType] values, this one + // will presumably not match any of the [KeyType] constants defined + // elsewhere in this package. + ApparentKeyType KeyType + + // Remainder is a verbatim copy of whatever appeared after the type + // in the given key string. This is preserved only for round-tripping + // purposes and so should be treated as opaque. + remainder string +} + +// KeyType returns the value from the ApparentKeyType field, which will +// presumably not match any of the [KeyType] constants in this package +// (because otherwise we would've used a different implementation of [Key]). +func (k Unrecognized) KeyType() KeyType { + return k.ApparentKeyType +} + +func (k Unrecognized) rawSuffix() string { + return k.remainder +} diff --git a/internal/stacks/stackstate/statekeys/key_build.go b/internal/stacks/stackstate/statekeys/key_build.go new file mode 100644 index 0000000000..4b6fc0a7bd --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key_build.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "strings" +) + +// rawKeyBuilder is a helper for building multi-field keys in the format +// that's expected by [cutKeyField]. +// +// The zero value of rawKeyBuilder is ready to use. +type rawKeyBuilder struct { + b strings.Builder + w bool +} + +// AppendField appends the given string to the key-in-progress as an additional +// field. +// +// The given string must not contain any unquoted commas, because comma is the +// field delimiter. If given an invalid field value this function will panic. +func (b *rawKeyBuilder) AppendField(s string) { + if keyDelimiterIdx(s) != -1 { + panic("key field contains the field delimiter") + } + if b.w { + b.b.WriteByte(',') + } + b.w = true + b.b.WriteString(s) +} + +// Raw returns the assembled raw key string. +func (b *rawKeyBuilder) Raw() string { + return b.b.String() +} diff --git a/internal/stacks/stackstate/statekeys/key_parse.go b/internal/stacks/stackstate/statekeys/key_parse.go new file mode 100644 index 0000000000..f7a820afe4 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key_parse.go @@ -0,0 +1,102 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" +) + +// Parse attempts to parse the given string as a state key, and returns the +// result if successful. +// +// A returned error means that the given string is syntactically invalid, +// which could mean either that it doesn't meet the basic requirements for +// any state key, or that it has a recognized key type but the remainder is +// not valid for that type. +// +// Parse DOES NOT return an error for a syntactically-valid key of an +// unrecognized type. Instead, it returns an [UnrecognizedKey] value which +// callers can detect using [RecognizedType], which will return false for +// a key of an unrecognized type. +func Parse(raw string) (Key, error) { + if len(raw) < 4 { + // All state keys must have at least four characters, since that's + // how long a key prefix is. + return nil, fmt.Errorf("too short to be a valid state key") + } + keyType := KeyType(raw[:4]) + remain := raw[4:] + parser := keyParsers[keyType] + if parser == nil { + if !isPlausibleRawKeyType(string(keyType)) { + return nil, fmt.Errorf("invalid key type prefix %q", keyType) + } + return Unrecognized{ + ApparentKeyType: keyType, + remainder: remain, + }, nil + } + return parser(remain) +} + +var keyParsers = map[KeyType]func(string) (Key, error){ + ResourceInstanceObjectType: parseResourceInstanceObject, + ComponentInstanceType: parseComponentInstance, +} + +// cutKeyField is a key parsing helper for key types that consist of +// multiple fields concatenated together. +// +// cutKeyField returns the raw string content of the next field, and +// also returns any remaining text after the field delimeter which +// could therefore be used in a subsequent call to cutKeyField. +// +// The field delimiter is a comma, but the parser ignores any comma +// that appears to be inside a pair of double-quote characters (") +// so that it's safe to include an address with a string-based instance key +// (which could potentially contain a literal comma) and get back that same +// address as a single field. +// +// If the given string does not contain any delimiters, the result is the +// same string verbatim and an empty "remain" result. +func cutKeyField(raw string) (field, remain string) { + i := keyDelimiterIdx(raw) + if i == -1 { + return raw, "" + } + return raw[:i], raw[i+1:] +} + +// finalKeyField returns the given string and true if it doesn't contain a key +// field delimiter, or "", false if the string does have a delimiter. +func finalKeyField(raw string) (string, bool) { + i := keyDelimiterIdx(raw) + if i != -1 { + return "", false + } + return raw, true +} + +// keyDelimiterIdx finds the index of the first delimiter in the given +// string, or returns -1 if there is no delimiter in the string. +func keyDelimiterIdx(raw string) int { + inQuotes := false + escape := false + for i, c := range raw { + if c == ',' && !inQuotes { + return i + } + if c == '\\' { + escape = true + continue + } + if c == '"' && !escape { + inQuotes = !inQuotes + } + escape = false + } + // If we fall out here then the entire string seems to be + // a single field, with no delimiters. + return -1 +} diff --git a/internal/stacks/stackstate/statekeys/key_parse_test.go b/internal/stacks/stackstate/statekeys/key_parse_test.go new file mode 100644 index 0000000000..180f9abf7f --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key_parse_test.go @@ -0,0 +1,353 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/states" +) + +func TestParse(t *testing.T) { + tests := []struct { + Input string + Want Key + WantErr string + + WantUnrecognizedHandling UnrecognizedKeyHandling + }{ + { + Input: "", + WantErr: `too short to be a valid state key`, + }, + { + Input: "a", + WantErr: `too short to be a valid state key`, + }, + { + Input: "aa", + WantErr: `too short to be a valid state key`, + }, + { + Input: "aaa", + WantErr: `too short to be a valid state key`, + }, + { + Input: "aaa!", // this is a suitable length but contains an invalid character + WantErr: `invalid key type prefix "aaa!"`, + }, + { + Input: "aaaa", + Want: Unrecognized{ + ApparentKeyType: KeyType("aaaa"), + remainder: "", + }, + WantUnrecognizedHandling: DiscardIfUnrecognized, + }, + { + Input: "AAAA", + Want: Unrecognized{ + ApparentKeyType: KeyType("AAAA"), + remainder: "", + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + Input: "aaaA", + Want: Unrecognized{ + ApparentKeyType: KeyType("aaaA"), + remainder: "", + }, + WantUnrecognizedHandling: PreserveIfUnrecognized, + }, + + // Resource instance object keys + { + Input: "RSRC", + WantErr: `resource instance object key has invalid component instance address ""`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,cur", + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + }, + }, + }, + DeposedKey: states.NotDeposed, + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + // Commas inside quoted instance keys are not treated as + // delimiters. + Input: `RSRCcomponent.foo["a,a"],aws_instance.bar["c,c"],cur`, + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey("a,a"), + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + Key: addrs.StringKey("c,c"), + }, + }, + }, + DeposedKey: states.NotDeposed, + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + // Commas inside quoted instance keys are not treated as + // delimiters even when there's quote-escaping hazards. + Input: `RSRCcomponent.foo["a\",a"],aws_instance.bar["c\",c"],cur`, + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey(`a",a`), + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + Key: addrs.StringKey(`c",c`), + }, + }, + }, + DeposedKey: states.NotDeposed, + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + Input: `RSRCstack.beep["a"].component.foo["b"],module.boop[1].aws_instance.bar[2],cur`, + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("beep", addrs.StringKey("a")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey("b"), + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance.Child("boop", addrs.IntKey(1)), + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + Key: addrs.IntKey(2), + }, + }, + }, + DeposedKey: states.NotDeposed, + }, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,facecafe", + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + }, + }, + }, + DeposedKey: states.DeposedKey("facecafe"), + }, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,beef", // deposed key is invalid because it's not long enough + WantErr: `resource instance object key has invalid deposed key "beef"`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,tootcafe", // deposed key is invalid because it isn't all hex digits + WantErr: `resource instance object key has invalid deposed key "tootcafe"`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,FACECAFE", // deposed key is invalid because it uses uppercase hex digits + WantErr: `resource instance object key has invalid deposed key "FACECAFE"`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,", // last field must either be "cur" or a deposed key + WantErr: `resource instance object key has invalid deposed key ""`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,cur,", + WantErr: `unsupported extra field in resource instance object key`, + }, + + // Component instance keys + { + Input: "CMPT", + WantErr: `component instance key has invalid component instance address ""`, + }, + { + Input: "CMPTcomponent.foo", + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + }, + }, + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + Input: `CMPTcomponent.foo["baz"]`, + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey("baz"), + }, + }, + }, + }, + { + Input: `CMPTstack.boop.component.foo["baz"]`, + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("boop", addrs.NoKey), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey("baz"), + }, + }, + }, + }, + { + Input: `CMPTcomponent.foo["b,b"]`, + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey(`b,b`), + }, + }, + }, + }, + { + Input: `CMPTcomponent.foo["b\",b"]`, + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey(`b",b`), + }, + }, + }, + }, + { + Input: "CMPTcomponent.foo,", + WantErr: `unsupported extra field in component instance key`, + }, + } + + cmpOpts := cmp.AllowUnexported(Unrecognized{}) + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + got, err := Parse(test.Input) + + if diff := cmp.Diff(test.Want, got, cmpOpts); diff != "" { + t.Errorf("wrong result for: %s\n%s", test.Input, diff) + } + + if test.WantErr == "" { + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + // Any valid key should round-trip back to what we were given. + if got != nil { + gotAsStr := String(got) + if gotAsStr != test.Input { + t.Errorf("valid key of type %T did not round-trip\ngot: %s\nwant: %s", got, gotAsStr, test.Input) + } + } else if err == nil { + t.Error("Parse returned nil Key and nil error") + } + } else { + if err == nil { + t.Errorf("unexpected success\nwant error: %s", test.WantErr) + } else { + if got, want := err.Error(), test.WantErr; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } + } + } + }) + } +} diff --git a/internal/stacks/stackstate/statekeys/key_type.go b/internal/stacks/stackstate/statekeys/key_type.go new file mode 100644 index 0000000000..c7eb22aec8 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key_type.go @@ -0,0 +1,98 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" +) + +// A KeyType represents a particular type of state key, which is typically +// associated with a particular kind of object that can be represented in +// stack state. +// +// Each KeyType consists of four ASCII letters which are intended to be +// somewhat mnemonic (at least for the more commonly-appearing ones) +// but are not intended for end-user consumption, because state storage +// keys are to be considered opaque by anything other than Terraform Core. +// +// There are some additional semantics encoded in the case of some of the +// letters, to help keep the encoding relatively compact: +// - If the first letter is uppercase then that means the key type is +// "mandatory", while if it's lowercase then the key type is "ignorable". +// Terraform Core will raise an error during state decoding if it encounters +// a mandatory key type that it isn't familiar with, but it will silently +// allow unrecognized key types that are ignorable. +// - For key types that are ignorable, if the _last_ letter is lowercase +// then the key type is "discarded", while if it's uppercase then the +// key type is "preserved". When Terraform Core encounters an unrecognized +// key type that is both ignorable and "discarded" then it will proactively +// emit an event to delete that unrecognized object from the state. +// If the key type is "preserved" then Terraform Core will just ignore it +// and let the existing object with that key continue to exist in the +// state. +// +// These behaviors are intended as a lightweight way to achieve some +// forward-compatibility by allowing an older version of Terraform Core to, +// when it's safe to do so, silently discard or preserve objects that were +// presumably added by a later version of Terraform. When we add new key types +// in future we should consider which of the three unrecognized key handling +// methods is most appropriate, preferring one of the two "ignorable" modes +// if possible but using a "mandatory" key type if ignoring a particular +// object could cause an older version of Terraform Core to misinterpret +// the overall meaning of the prior state. +type KeyType string + +const ( + ResourceInstanceObjectType KeyType = "RSRC" + ComponentInstanceType KeyType = "CMPT" +) + +// UnrecognizedKeyHandling returns an indication of which of the three possible +// actions should be taken if the receiver is an unrecognized key type. +// +// It only really makes sense to use this method for a [KeyType] included in +// an [UnrecognizedKey] value. +func (kt KeyType) UnrecognizedKeyHandling() UnrecognizedKeyHandling { + first := kt[0] + last := kt[3] + switch { + case first >= 'A' && first <= 'Z': + return FailIfUnrecognized + case last >= 'A' && last <= 'Z': + return PreserveIfUnrecognized + default: + return DiscardIfUnrecognized + } +} + +func (kt KeyType) GoString() string { + return fmt.Sprintf("statekeys.KeyType(%q)", kt) +} + +func isPlausibleRawKeyType(s string) bool { + if len(s) != 4 { + return false + } + // All of the characters must be ASCII letters + for _, c := range s { + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + return false + } + } + return true +} + +// UnrecognizedKeyHandling models the three different ways an unrecognized +// key type can be handled when decoding prior state. +// +// See the documentation for [KeyType] for more information. +type UnrecognizedKeyHandling rune + +//go:generate go run golang.org/x/tools/cmd/stringer -type UnrecognizedKeyHandling + +const ( + FailIfUnrecognized UnrecognizedKeyHandling = 'F' + PreserveIfUnrecognized UnrecognizedKeyHandling = 'P' + DiscardIfUnrecognized UnrecognizedKeyHandling = 'D' +) diff --git a/internal/stacks/stackstate/statekeys/resources.go b/internal/stacks/stackstate/statekeys/resources.go new file mode 100644 index 0000000000..66bfbc3132 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/resources.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/states" +) + +// ResourceInstanceObject represents state keys for resource instance objects. +type ResourceInstanceObject struct { + ResourceInstance stackaddrs.AbsResourceInstance + DeposedKey states.DeposedKey +} + +func parseResourceInstanceObject(s string) (Key, error) { + componentInstAddrRaw, s := cutKeyField(s) + resourceInstAddrRaw, s := cutKeyField(s) + deposedRaw, ok := finalKeyField(s) + if !ok { + return nil, fmt.Errorf("unsupported extra field in resource instance object key") + } + componentInstAddr, diags := stackaddrs.ParseAbsComponentInstanceStr(componentInstAddrRaw) + if diags.HasErrors() { + return nil, fmt.Errorf("resource instance object key has invalid component instance address %q", componentInstAddrRaw) + } + resourceInstAddr, diags := addrs.ParseAbsResourceInstanceStr(resourceInstAddrRaw) + if diags.HasErrors() { + return nil, fmt.Errorf("resource instance object key has invalid resource instance address %q", resourceInstAddrRaw) + } + var deposedKey states.DeposedKey + if deposedRaw != "cur" { + var err error + deposedKey, err = states.ParseDeposedKey(deposedRaw) + if err != nil { + return nil, fmt.Errorf("resource instance object key has invalid deposed key %q", deposedRaw) + } + } else { + deposedKey = states.NotDeposed + } + return ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: componentInstAddr, + Item: resourceInstAddr, + }, + DeposedKey: deposedKey, + }, nil +} + +func (k ResourceInstanceObject) KeyType() KeyType { + return ResourceInstanceObjectType +} + +func (k ResourceInstanceObject) rawSuffix() string { + var b rawKeyBuilder + b.AppendField(k.ResourceInstance.Component.String()) + b.AppendField(k.ResourceInstance.Item.String()) + if k.DeposedKey != states.NotDeposed { + // A valid deposed key is always eight hex digits, and never + // contains a comma so we can write it unquoted. + b.AppendField(string(k.DeposedKey)) + } else { + b.AppendField("cur") // short for "current" + } + return b.Raw() +} diff --git a/internal/stacks/stackstate/statekeys/unrecognizedkeyhandling_string.go b/internal/stacks/stackstate/statekeys/unrecognizedkeyhandling_string.go new file mode 100644 index 0000000000..59c7ec892b --- /dev/null +++ b/internal/stacks/stackstate/statekeys/unrecognizedkeyhandling_string.go @@ -0,0 +1,33 @@ +// Code generated by "stringer -type UnrecognizedKeyHandling"; DO NOT EDIT. + +package statekeys + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[FailIfUnrecognized-70] + _ = x[PreserveIfUnrecognized-80] + _ = x[DiscardIfUnrecognized-68] +} + +const ( + _UnrecognizedKeyHandling_name_0 = "DiscardIfUnrecognized" + _UnrecognizedKeyHandling_name_1 = "FailIfUnrecognized" + _UnrecognizedKeyHandling_name_2 = "PreserveIfUnrecognized" +) + +func (i UnrecognizedKeyHandling) String() string { + switch { + case i == 68: + return _UnrecognizedKeyHandling_name_0 + case i == 70: + return _UnrecognizedKeyHandling_name_1 + case i == 80: + return _UnrecognizedKeyHandling_name_2 + default: + return "UnrecognizedKeyHandling(" + strconv.FormatInt(int64(i), 10) + ")" + } +}