Skip to content

Commit 595cd80

Browse files
authored
CLI command for listing state identities (#36705)
1 parent 9bbe34d commit 595cd80

File tree

8 files changed

+824
-0
lines changed

8 files changed

+824
-0
lines changed

Diff for: commands.go

+6
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,12 @@ func initCommands(
378378
}, nil
379379
},
380380

381+
"state identities": func() (cli.Command, error) {
382+
return &command.StateIdentitiesCommand{
383+
Meta: meta,
384+
}, nil
385+
},
386+
381387
"state rm": func() (cli.Command, error) {
382388
return &command.StateRmCommand{
383389
StateMeta: command.StateMeta{

Diff for: internal/command/command_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,53 @@ func testState() *states.State {
322322
}).DeepCopy()
323323
}
324324

325+
func testStateWithIdentity() *states.State {
326+
return states.BuildState(func(s *states.SyncState) {
327+
s.SetResourceInstanceCurrent(
328+
addrs.Resource{
329+
Mode: addrs.ManagedResourceMode,
330+
Type: "test_instance",
331+
Name: "foo",
332+
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
333+
&states.ResourceInstanceObjectSrc{
334+
// The weird whitespace here is reflective of how this would
335+
// get written out in a real state file, due to the indentation
336+
// of all of the containing wrapping objects and arrays.
337+
AttrsJSON: []byte("{\n \"id\": \"foo\"\n }"),
338+
Status: states.ObjectReady,
339+
Dependencies: []addrs.ConfigResource{},
340+
IdentitySchemaVersion: 0,
341+
IdentityJSON: []byte("{\n \"id\": \"my-foo-id\"\n }"),
342+
},
343+
addrs.AbsProviderConfig{
344+
Provider: addrs.NewDefaultProvider("test"),
345+
Module: addrs.RootModule,
346+
},
347+
)
348+
s.SetResourceInstanceCurrent(
349+
addrs.Resource{
350+
Mode: addrs.ManagedResourceMode,
351+
Type: "test_instance",
352+
Name: "bar",
353+
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
354+
&states.ResourceInstanceObjectSrc{
355+
AttrsJSON: []byte("{\n \"id\": \"bar\"\n }"),
356+
Status: states.ObjectReady,
357+
Dependencies: []addrs.ConfigResource{},
358+
IdentitySchemaVersion: 0,
359+
IdentityJSON: []byte("{\n \"id\": \"my-bar-id\"\n }"),
360+
},
361+
addrs.AbsProviderConfig{
362+
Provider: addrs.NewDefaultProvider("test"),
363+
Module: addrs.RootModule,
364+
},
365+
)
366+
// DeepCopy is used here to ensure our synthetic state matches exactly
367+
// with a state that will have been copied during the command
368+
// operation, and all fields have been copied correctly.
369+
}).DeepCopy()
370+
}
371+
325372
// writeStateForTesting is a helper that writes the given naked state to the
326373
// given writer, generating a stub *statefile.File wrapper which is then
327374
// immediately discarded.

Diff for: internal/command/state_identities.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package command
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/hashicorp/cli"
12+
"github.com/hashicorp/terraform/internal/addrs"
13+
"github.com/hashicorp/terraform/internal/states"
14+
"github.com/hashicorp/terraform/internal/tfdiags"
15+
)
16+
17+
// StateIdentitiesCommand is a Command implementation that lists the resource identities
18+
// within a state file.
19+
type StateIdentitiesCommand struct {
20+
Meta
21+
StateMeta
22+
}
23+
24+
func (c *StateIdentitiesCommand) Run(args []string) int {
25+
args = c.Meta.process(args)
26+
var statePath string
27+
var jsonOutput bool
28+
cmdFlags := c.Meta.defaultFlagSet("state identities")
29+
cmdFlags.StringVar(&statePath, "state", "", "path")
30+
cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output")
31+
lookupId := cmdFlags.String("id", "", "Restrict output to paths with a resource having the specified ID.")
32+
if err := cmdFlags.Parse(args); err != nil {
33+
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
34+
return cli.RunResultHelp
35+
}
36+
args = cmdFlags.Args()
37+
38+
if !jsonOutput {
39+
c.Ui.Error(
40+
"The `terraform state identities` command requires the `-json` flag.\n")
41+
cmdFlags.Usage()
42+
return 1
43+
}
44+
45+
if statePath != "" {
46+
c.Meta.statePath = statePath
47+
}
48+
49+
// Load the backend
50+
b, backendDiags := c.Backend(nil)
51+
if backendDiags.HasErrors() {
52+
c.showDiagnostics(backendDiags)
53+
return 1
54+
}
55+
56+
// This is a read-only command
57+
c.ignoreRemoteVersionConflict(b)
58+
59+
// Get the state
60+
env, err := c.Workspace()
61+
if err != nil {
62+
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
63+
return 1
64+
}
65+
stateMgr, err := b.StateMgr(env)
66+
if err != nil {
67+
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
68+
return 1
69+
}
70+
if err := stateMgr.RefreshState(); err != nil {
71+
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
72+
return 1
73+
}
74+
75+
state := stateMgr.State()
76+
if state == nil {
77+
c.Ui.Error(errStateNotFound)
78+
return 1
79+
}
80+
81+
var addrs []addrs.AbsResourceInstance
82+
var diags tfdiags.Diagnostics
83+
if len(args) == 0 {
84+
addrs, diags = c.lookupAllResourceInstanceAddrs(state)
85+
} else {
86+
addrs, diags = c.lookupResourceInstanceAddrs(state, args...)
87+
}
88+
if diags.HasErrors() {
89+
c.showDiagnostics(diags)
90+
return 1
91+
}
92+
93+
output := make(map[string]any)
94+
for _, addr := range addrs {
95+
// If the resource exists but identity is nil, skip it, as it is not required to be present
96+
if is := state.ResourceInstance(addr); is != nil && is.Current.IdentityJSON != nil {
97+
if *lookupId == "" || *lookupId == states.LegacyInstanceObjectID(is.Current) {
98+
var rawIdentity map[string]any
99+
if err := json.Unmarshal(is.Current.IdentityJSON, &rawIdentity); err != nil {
100+
c.Ui.Error(fmt.Sprintf("Failed to unmarshal identity JSON: %s", err))
101+
return 1
102+
}
103+
output[addr.String()] = rawIdentity
104+
}
105+
}
106+
}
107+
108+
outputJSON, err := json.MarshalIndent(output, "", " ")
109+
if err != nil {
110+
c.Ui.Error(fmt.Sprintf("Failed to marshal output JSON: %s", err))
111+
return 1
112+
}
113+
114+
c.Ui.Output(string(outputJSON))
115+
c.showDiagnostics(diags)
116+
117+
return 0
118+
}
119+
120+
func (c *StateIdentitiesCommand) Help() string {
121+
helpText := `
122+
Usage: terraform [global options] state identities [options] -json [address...]
123+
124+
List the json format of the identities of resources in the Terraform state.
125+
126+
This command lists the identities of resource instances in the Terraform state in json format.
127+
The address argument can be used to filter the instances by resource or module. If
128+
no pattern is given, identities for all resource instances are listed.
129+
130+
The addresses must either be module addresses or absolute resource
131+
addresses, such as:
132+
aws_instance.example
133+
module.example
134+
module.example.module.child
135+
module.example.aws_instance.example
136+
137+
An error will be returned if any of the resources or modules given as
138+
filter addresses do not exist in the state.
139+
140+
Options:
141+
142+
-state=statefile Path to a Terraform state file to use to look
143+
up Terraform-managed resources. By default, Terraform
144+
will consult the state of the currently-selected
145+
workspace.
146+
147+
-id=ID Filters the results to include only instances whose
148+
resource types have an attribute named "id" whose value
149+
equals the given id string.
150+
151+
`
152+
return strings.TrimSpace(helpText)
153+
}
154+
155+
func (c *StateIdentitiesCommand) Synopsis() string {
156+
return "List the identities of resources in the state"
157+
}

0 commit comments

Comments
 (0)