| // 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) |
| } |
| if test.WantUnrecognizedHandling != UnrecognizedKeyHandling(0) { |
| if got, want := got.KeyType().UnrecognizedKeyHandling(), test.WantUnrecognizedHandling; got != want { |
| t.Errorf("unexpected UnrecognizedKeyHandling\ngot: %s\nwant: %s", got, want) |
| } |
| } |
| } 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) |
| } |
| } |
| } |
| }) |
| } |
| } |