diff --git a/internal/command/arguments/state_list.go b/internal/command/arguments/state_list.go new file mode 100644 index 0000000000..d6aaaf73b5 --- /dev/null +++ b/internal/command/arguments/state_list.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StateList represents the command-line arguments for the state list command. +type StateList struct { + // StatePath is an optional path to a state file, overriding the default. + StatePath string + + // ID filters the results to include only instances whose resource types + // have an attribute named "id" whose value equals this string. + ID string + + // Addrs are optional resource or module addresses used to filter the + // listed instances. + Addrs []string +} + +// ParseStateList processes CLI arguments, returning a StateList value and +// diagnostics. If errors are encountered, a StateList value is still returned +// representing the best effort interpretation of the arguments. +func ParseStateList(args []string) (*StateList, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + list := &StateList{} + + var statePath, id string + cmdFlags := defaultFlagSet("state list") + cmdFlags.StringVar(&statePath, "state", "", "path") + cmdFlags.StringVar(&id, "id", "", "Restrict output to paths with a resource having the specified ID.") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + list.StatePath = statePath + list.ID = id + list.Addrs = cmdFlags.Args() + + return list, diags +} diff --git a/internal/command/arguments/state_list_test.go b/internal/command/arguments/state_list_test.go new file mode 100644 index 0000000000..422b6efb63 --- /dev/null +++ b/internal/command/arguments/state_list_test.go @@ -0,0 +1,118 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseStateList_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StateList + }{ + "defaults": { + nil, + &StateList{ + StatePath: "", + ID: "", + Addrs: nil, + }, + }, + "state path": { + []string{"-state=foobar.tfstate"}, + &StateList{ + StatePath: "foobar.tfstate", + ID: "", + Addrs: nil, + }, + }, + "id filter": { + []string{"-id=bar"}, + &StateList{ + StatePath: "", + ID: "bar", + Addrs: nil, + }, + }, + "with addresses": { + []string{"module.example", "aws_instance.foo"}, + &StateList{ + StatePath: "", + ID: "", + Addrs: []string{"module.example", "aws_instance.foo"}, + }, + }, + "all options": { + []string{"-state=foobar.tfstate", "-id=bar", "module.example"}, + &StateList{ + StatePath: "foobar.tfstate", + ID: "bar", + Addrs: []string{"module.example"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseStateList(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if got.StatePath != tc.want.StatePath { + t.Fatalf("unexpected StatePath\n got: %q\nwant: %q", got.StatePath, tc.want.StatePath) + } + if got.ID != tc.want.ID { + t.Fatalf("unexpected ID\n got: %q\nwant: %q", got.ID, tc.want.ID) + } + if len(got.Addrs) != len(tc.want.Addrs) { + t.Fatalf("unexpected Addrs length\n got: %d\nwant: %d", len(got.Addrs), len(tc.want.Addrs)) + } + for i := range got.Addrs { + if got.Addrs[i] != tc.want.Addrs[i] { + t.Fatalf("unexpected Addrs[%d]\n got: %q\nwant: %q", i, got.Addrs[i], tc.want.Addrs[i]) + } + } + }) + } +} + +func TestParseStateList_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StateList + wantDiags tfdiags.Diagnostics + }{ + "unknown flag": { + []string{"-boop"}, + &StateList{ + StatePath: "", + ID: "", + Addrs: nil, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -boop", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseStateList(tc.args) + if got.StatePath != tc.want.StatePath { + t.Fatalf("unexpected StatePath\n got: %q\nwant: %q", got.StatePath, tc.want.StatePath) + } + if got.ID != tc.want.ID { + t.Fatalf("unexpected ID\n got: %q\nwant: %q", got.ID, tc.want.ID) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/state_list.go b/internal/command/state_list.go index 0bbd89970f..8eaefbe3b8 100644 --- a/internal/command/state_list.go +++ b/internal/command/state_list.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/states" @@ -21,19 +20,14 @@ type StateListCommand struct { } func (c *StateListCommand) Run(args []string) int { - args = c.Meta.process(args) - var statePath string - cmdFlags := c.Meta.defaultFlagSet("state list") - cmdFlags.StringVar(&statePath, "state", "", "path") - lookupId := cmdFlags.String("id", "", "Restrict output to paths with a resource having the specified ID.") - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) - return cli.RunResultHelp + parsedArgs, diags := arguments.ParseStateList(c.Meta.process(args)) + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 } - args = cmdFlags.Args() - if statePath != "" { - c.Meta.statePath = statePath + if parsedArgs.StatePath != "" { + c.Meta.statePath = parsedArgs.StatePath } // Load the backend @@ -69,10 +63,10 @@ func (c *StateListCommand) Run(args []string) int { } var addrs []addrs.AbsResourceInstance - if len(args) == 0 { + if len(parsedArgs.Addrs) == 0 { addrs, diags = c.lookupAllResourceInstanceAddrs(state) } else { - addrs, diags = c.lookupResourceInstanceAddrs(state, args...) + addrs, diags = c.lookupResourceInstanceAddrs(state, parsedArgs.Addrs...) } if diags.HasErrors() { c.showDiagnostics(diags) @@ -81,7 +75,7 @@ func (c *StateListCommand) Run(args []string) int { for _, addr := range addrs { if is := state.ResourceInstance(addr); is != nil { - if *lookupId == "" || *lookupId == states.LegacyInstanceObjectID(is.Current) { + if parsedArgs.ID == "" || parsedArgs.ID == states.LegacyInstanceObjectID(is.Current) { c.Ui.Output(addr.String()) } }