Add `terraform state list` command
This introduces the terraform state list command to list the resources within a state. This is the first of many state management commands to come into 0.7. This is the first command of many to come that is considered a "plumbing" command within Terraform (see "plumbing vs porcelain": http://git.661346.n2.nabble.com/what-are-plumbing-and-porcelain-td2190639.html). As such, this PR also introduces a bunch of groundwork to support plumbing commands. The main changes: - Main command output is changed to split "common" and "uncommon" commands. - mitchellh/cli is updated to support nested subcommands, since terraform state list is a nested subcommand. - terraform.StateFilter is introduced as a way in core to filter/search the state files. This is very basic currently but I expect to make it more advanced as time goes on. - terraform state list command is introduced to list resources in a state. This can take a series of arguments to filter this down. Known issues, or things that aren't done in this PR on purpose: - Unit tests for terraform state list are on the way. Unit tests for the core changes are all there.
This commit is contained in:
parent
8e4da4e2a1
commit
d1b46e99bd
|
@ -962,7 +962,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/mitchellh/cli",
|
"ImportPath": "github.com/mitchellh/cli",
|
||||||
"Rev": "cb6853d606ea4a12a15ac83cc43503df99fd28fb"
|
"Rev": "83f97d41cf100ee5f33944a8815c167d5e4aa272"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/mitchellh/cloudflare-go",
|
"ImportPath": "github.com/mitchellh/cloudflare-go",
|
||||||
|
|
|
@ -326,6 +326,9 @@ func (m *Meta) flagSet(n string) *flag.FlagSet {
|
||||||
}()
|
}()
|
||||||
f.SetOutput(errW)
|
f.SetOutput(errW)
|
||||||
|
|
||||||
|
// Set the default Usage to empty
|
||||||
|
f.Usage = func() {}
|
||||||
|
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StateCommand is a Command implementation that just shows help for
|
||||||
|
// the subcommands nested below it.
|
||||||
|
type StateCommand struct {
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StateCommand) Run(args []string) int {
|
||||||
|
return cli.RunResultHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StateCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: terraform state <subcommand> [options] [args]
|
||||||
|
|
||||||
|
This command has subcommands for advanced state management.
|
||||||
|
|
||||||
|
These subcommands can be used to slice and dice the Terraform state.
|
||||||
|
This is sometimes necessary in advanced cases. For your safety, all
|
||||||
|
state management commands that modify the state create a timestamped
|
||||||
|
backup of the state prior to making modifications.
|
||||||
|
|
||||||
|
The structure and output of the commands is specifically tailored to work
|
||||||
|
well with the common Unix utilities such as grep, awk, etc. We recommend
|
||||||
|
using those tools to perform more advanced state tasks.
|
||||||
|
|
||||||
|
`
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StateCommand) Synopsis() string {
|
||||||
|
return "Advanced state management"
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StateListCommand is a Command implementation that lists the resources
|
||||||
|
// within a state file.
|
||||||
|
type StateListCommand struct {
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StateListCommand) Run(args []string) int {
|
||||||
|
args = c.Meta.process(args, true)
|
||||||
|
|
||||||
|
cmdFlags := c.Meta.flagSet("state list")
|
||||||
|
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
||||||
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
|
return cli.RunResultHelp
|
||||||
|
}
|
||||||
|
args = cmdFlags.Args()
|
||||||
|
|
||||||
|
state, err := c.State()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
|
||||||
|
return cli.RunResultHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
stateReal := state.State()
|
||||||
|
if stateReal == nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf(errStateNotFound))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &terraform.StateFilter{State: stateReal}
|
||||||
|
results, err := filter.Filter(args...)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf(errStateFilter, err))
|
||||||
|
return cli.RunResultHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
if _, ok := result.Value.(*terraform.InstanceState); ok {
|
||||||
|
c.Ui.Output(result.Address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StateListCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: terraform state list [options] [pattern...]
|
||||||
|
|
||||||
|
List resources in the Terraform state.
|
||||||
|
|
||||||
|
This command lists resources in the Terraform state. The pattern argument
|
||||||
|
can be used to filter the resources by resource or module. If no pattern
|
||||||
|
is given, all resources are listed.
|
||||||
|
|
||||||
|
The pattern argument is meant to provide very simple filtering. For
|
||||||
|
advanced filtering, please use tools such as "grep". The output of this
|
||||||
|
command is designed to be friendly for this usage.
|
||||||
|
|
||||||
|
The pattern argument accepts any resource targeting syntax. Please
|
||||||
|
refer to the documentation on resource targeting syntax for more
|
||||||
|
information.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-state=statefile Path to a Terraform state file to use to look
|
||||||
|
up Terraform-managed resources. By default it will
|
||||||
|
use the state "terraform.tfstate" if it exists.
|
||||||
|
|
||||||
|
`
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StateListCommand) Synopsis() string {
|
||||||
|
return "List resources in the state"
|
||||||
|
}
|
||||||
|
|
||||||
|
const errStateFilter = `Error filtering state: %[1]s
|
||||||
|
|
||||||
|
Please ensure that all your addresses are formatted properly.`
|
||||||
|
|
||||||
|
const errStateLoadingState = `Error loading the state: %[1]s
|
||||||
|
|
||||||
|
Please ensure that your Terraform state exists and that you've
|
||||||
|
configured it properly. You can use the "-state" flag to point
|
||||||
|
Terraform at another state file.`
|
||||||
|
|
||||||
|
const errStateNotFound = `No state file was found!
|
||||||
|
|
||||||
|
State management commands require a state file. Run this command
|
||||||
|
in a directory where Terraform has been run or use the -state flag
|
||||||
|
to point the command to a specific state location.`
|
|
@ -0,0 +1,59 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStateList(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
statePath := testStateFile(t, state)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &StateListCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that outputs were displayed
|
||||||
|
expected := strings.TrimSpace(testStateListOutput) + "\n"
|
||||||
|
actual := ui.OutputWriter.String()
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStateList_noState(t *testing.T) {
|
||||||
|
tmp, cwd := testCwd(t)
|
||||||
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &StateListCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
if code := c.Run(args); code != 1 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testStateListOutput = `
|
||||||
|
test_instance.foo
|
||||||
|
`
|
21
commands.go
21
commands.go
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
// Commands is the mapping of all the available Terraform commands.
|
// Commands is the mapping of all the available Terraform commands.
|
||||||
var Commands map[string]cli.CommandFactory
|
var Commands map[string]cli.CommandFactory
|
||||||
|
var PlumbingCommands map[string]struct{}
|
||||||
|
|
||||||
// Ui is the cli.Ui used for communicating to the outside world.
|
// Ui is the cli.Ui used for communicating to the outside world.
|
||||||
var Ui cli.Ui
|
var Ui cli.Ui
|
||||||
|
@ -34,6 +35,10 @@ func init() {
|
||||||
Ui: Ui,
|
Ui: Ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlumbingCommands = map[string]struct{}{
|
||||||
|
"state": struct{}{}, // includes all subcommands
|
||||||
|
}
|
||||||
|
|
||||||
Commands = map[string]cli.CommandFactory{
|
Commands = map[string]cli.CommandFactory{
|
||||||
"apply": func() (cli.Command, error) {
|
"apply": func() (cli.Command, error) {
|
||||||
return &command.ApplyCommand{
|
return &command.ApplyCommand{
|
||||||
|
@ -137,6 +142,22 @@ func init() {
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Plumbing
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
|
||||||
|
"state": func() (cli.Command, error) {
|
||||||
|
return &command.StateCommand{
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
|
"state list": func() (cli.Command, error) {
|
||||||
|
return &command.StateListCommand{
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// helpFunc is a cli.HelpFunc that can is used to output the help for Terraform.
|
||||||
|
func helpFunc(commands map[string]cli.CommandFactory) string {
|
||||||
|
// Determine the maximum key length, and classify based on type
|
||||||
|
porcelain := make(map[string]cli.CommandFactory)
|
||||||
|
plumbing := make(map[string]cli.CommandFactory)
|
||||||
|
maxKeyLen := 0
|
||||||
|
for key, f := range commands {
|
||||||
|
if len(key) > maxKeyLen {
|
||||||
|
maxKeyLen = len(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := PlumbingCommands[key]; ok {
|
||||||
|
plumbing[key] = f
|
||||||
|
} else {
|
||||||
|
porcelain[key] = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("usage: terraform [--version] [--help] <command> [args]\n\n")
|
||||||
|
buf.WriteString(
|
||||||
|
"The available commands for execution are listed below.\n" +
|
||||||
|
"The most common, useful commands are shown first, followed by\n" +
|
||||||
|
"less common or more advanced commands. If you're just getting\n" +
|
||||||
|
"started with Terraform, stick with the common commands. For the\n" +
|
||||||
|
"other commands, please read the help and docs before usage.\n\n")
|
||||||
|
buf.WriteString("Common commands:\n")
|
||||||
|
buf.WriteString(listCommands(porcelain, maxKeyLen))
|
||||||
|
buf.WriteString("\nAll other commands:\n")
|
||||||
|
buf.WriteString(listCommands(plumbing, maxKeyLen))
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// listCommands just lists the commands in the map with the
|
||||||
|
// given maximum key length.
|
||||||
|
func listCommands(commands map[string]cli.CommandFactory, maxKeyLen int) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
// Get the list of keys so we can sort them, and also get the maximum
|
||||||
|
// key length so they can be aligned properly.
|
||||||
|
keys := make([]string, 0, len(commands))
|
||||||
|
for key, _ := range commands {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
commandFunc, ok := commands[key]
|
||||||
|
if !ok {
|
||||||
|
// This should never happen since we JUST built the list of
|
||||||
|
// keys.
|
||||||
|
panic("command not found: " + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
command, err := commandFunc()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] cli: Command '%s' failed to load: %s",
|
||||||
|
key, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
|
||||||
|
buf.WriteString(fmt.Sprintf(" %s %s\n", key, command.Synopsis()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
2
main.go
2
main.go
|
@ -113,7 +113,7 @@ func wrappedMain() int {
|
||||||
cli := &cli.CLI{
|
cli := &cli.CLI{
|
||||||
Args: args,
|
Args: args,
|
||||||
Commands: Commands,
|
Commands: Commands,
|
||||||
HelpFunc: cli.BasicHelpFunc("terraform"),
|
HelpFunc: helpFunc,
|
||||||
HelpWriter: os.Stdout,
|
HelpWriter: os.Stdout,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,35 @@ func (r *ResourceAddress) Copy() *ResourceAddress {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String outputs the address that parses into this address.
|
||||||
|
func (r *ResourceAddress) String() string {
|
||||||
|
var result []string
|
||||||
|
for _, p := range r.Path {
|
||||||
|
result = append(result, "module", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Type != "" {
|
||||||
|
result = append(result, r.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Name != "" {
|
||||||
|
name := r.Name
|
||||||
|
switch r.InstanceType {
|
||||||
|
case TypeDeposed:
|
||||||
|
name += ".deposed"
|
||||||
|
case TypeTainted:
|
||||||
|
name += ".tainted"
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Index >= 0 {
|
||||||
|
name += fmt.Sprintf("[%d]", r.Index)
|
||||||
|
}
|
||||||
|
result = append(result, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(result, ".")
|
||||||
|
}
|
||||||
|
|
||||||
func ParseResourceAddress(s string) (*ResourceAddress, error) {
|
func ParseResourceAddress(s string) (*ResourceAddress, error) {
|
||||||
matches, err := tokenizeResourceAddress(s)
|
matches, err := tokenizeResourceAddress(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -9,109 +9,121 @@ func TestParseResourceAddress(t *testing.T) {
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
Input string
|
Input string
|
||||||
Expected *ResourceAddress
|
Expected *ResourceAddress
|
||||||
|
Output string
|
||||||
}{
|
}{
|
||||||
"implicit primary, no specific index": {
|
"implicit primary, no specific index": {
|
||||||
Input: "aws_instance.foo",
|
"aws_instance.foo",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Type: "aws_instance",
|
Type: "aws_instance",
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
InstanceType: TypePrimary,
|
InstanceType: TypePrimary,
|
||||||
Index: -1,
|
Index: -1,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
"implicit primary, explicit index": {
|
"implicit primary, explicit index": {
|
||||||
Input: "aws_instance.foo[2]",
|
"aws_instance.foo[2]",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Type: "aws_instance",
|
Type: "aws_instance",
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
InstanceType: TypePrimary,
|
InstanceType: TypePrimary,
|
||||||
Index: 2,
|
Index: 2,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
"implicit primary, explicit index over ten": {
|
"implicit primary, explicit index over ten": {
|
||||||
Input: "aws_instance.foo[12]",
|
"aws_instance.foo[12]",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Type: "aws_instance",
|
Type: "aws_instance",
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
InstanceType: TypePrimary,
|
InstanceType: TypePrimary,
|
||||||
Index: 12,
|
Index: 12,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
"explicit primary, explicit index": {
|
"explicit primary, explicit index": {
|
||||||
Input: "aws_instance.foo.primary[2]",
|
"aws_instance.foo.primary[2]",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Type: "aws_instance",
|
Type: "aws_instance",
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
InstanceType: TypePrimary,
|
InstanceType: TypePrimary,
|
||||||
Index: 2,
|
Index: 2,
|
||||||
},
|
},
|
||||||
|
"aws_instance.foo[2]",
|
||||||
},
|
},
|
||||||
"tainted": {
|
"tainted": {
|
||||||
Input: "aws_instance.foo.tainted",
|
"aws_instance.foo.tainted",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Type: "aws_instance",
|
Type: "aws_instance",
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
InstanceType: TypeTainted,
|
InstanceType: TypeTainted,
|
||||||
Index: -1,
|
Index: -1,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
"deposed": {
|
"deposed": {
|
||||||
Input: "aws_instance.foo.deposed",
|
"aws_instance.foo.deposed",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Type: "aws_instance",
|
Type: "aws_instance",
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
InstanceType: TypeDeposed,
|
InstanceType: TypeDeposed,
|
||||||
Index: -1,
|
Index: -1,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
"with a hyphen": {
|
"with a hyphen": {
|
||||||
Input: "aws_instance.foo-bar",
|
"aws_instance.foo-bar",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Type: "aws_instance",
|
Type: "aws_instance",
|
||||||
Name: "foo-bar",
|
Name: "foo-bar",
|
||||||
InstanceType: TypePrimary,
|
InstanceType: TypePrimary,
|
||||||
Index: -1,
|
Index: -1,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
"in a module": {
|
"in a module": {
|
||||||
Input: "module.child.aws_instance.foo",
|
"module.child.aws_instance.foo",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Path: []string{"child"},
|
Path: []string{"child"},
|
||||||
Type: "aws_instance",
|
Type: "aws_instance",
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
InstanceType: TypePrimary,
|
InstanceType: TypePrimary,
|
||||||
Index: -1,
|
Index: -1,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
"nested modules": {
|
"nested modules": {
|
||||||
Input: "module.a.module.b.module.forever.aws_instance.foo",
|
"module.a.module.b.module.forever.aws_instance.foo",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Path: []string{"a", "b", "forever"},
|
Path: []string{"a", "b", "forever"},
|
||||||
Type: "aws_instance",
|
Type: "aws_instance",
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
InstanceType: TypePrimary,
|
InstanceType: TypePrimary,
|
||||||
Index: -1,
|
Index: -1,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
"just a module": {
|
"just a module": {
|
||||||
Input: "module.a",
|
"module.a",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Path: []string{"a"},
|
Path: []string{"a"},
|
||||||
Type: "",
|
Type: "",
|
||||||
Name: "",
|
Name: "",
|
||||||
InstanceType: TypePrimary,
|
InstanceType: TypePrimary,
|
||||||
Index: -1,
|
Index: -1,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
"just a nested module": {
|
"just a nested module": {
|
||||||
Input: "module.a.module.b",
|
"module.a.module.b",
|
||||||
Expected: &ResourceAddress{
|
&ResourceAddress{
|
||||||
Path: []string{"a", "b"},
|
Path: []string{"a", "b"},
|
||||||
Type: "",
|
Type: "",
|
||||||
Name: "",
|
Name: "",
|
||||||
InstanceType: TypePrimary,
|
InstanceType: TypePrimary,
|
||||||
Index: -1,
|
Index: -1,
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +136,14 @@ func TestParseResourceAddress(t *testing.T) {
|
||||||
if !reflect.DeepEqual(out, tc.Expected) {
|
if !reflect.DeepEqual(out, tc.Expected) {
|
||||||
t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out)
|
t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expected := tc.Input
|
||||||
|
if tc.Output != "" {
|
||||||
|
expected = tc.Output
|
||||||
|
}
|
||||||
|
if out.String() != expected {
|
||||||
|
t.Fatalf("bad: %q\n\nexpected: %s\n\ngot: %s", tn, expected, out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,238 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StateFilter is responsible for filtering and searching a state.
|
||||||
|
//
|
||||||
|
// This is a separate struct from State rather than a method on State
|
||||||
|
// because StateFilter might create sidecar data structures to optimize
|
||||||
|
// filtering on the state.
|
||||||
|
//
|
||||||
|
// If you change the State, the filter created is invalid and either
|
||||||
|
// Reset should be called or a new one should be allocated. StateFilter
|
||||||
|
// will not watch State for changes and do this for you. If you filter after
|
||||||
|
// changing the State without calling Reset, the behavior is not defined.
|
||||||
|
type StateFilter struct {
|
||||||
|
State *State
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter takes the addresses specified by fs and finds all the matches.
|
||||||
|
// The values of fs are resource addressing syntax that can be parsed by
|
||||||
|
// ParseResourceAddress.
|
||||||
|
func (f *StateFilter) Filter(fs ...string) ([]*StateFilterResult, error) {
|
||||||
|
// Parse all the addresses
|
||||||
|
as := make([]*ResourceAddress, len(fs))
|
||||||
|
for i, v := range fs {
|
||||||
|
a, err := ParseResourceAddress(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error parsing address '%s': %s", v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
as[i] = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we werent given any filters, then we list all
|
||||||
|
if len(fs) == 0 {
|
||||||
|
as = append(as, &ResourceAddress{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter each of the address. We keep track of this in a map to
|
||||||
|
// strip duplicates.
|
||||||
|
resultSet := make(map[string]*StateFilterResult)
|
||||||
|
for _, a := range as {
|
||||||
|
for _, r := range f.filterSingle(a) {
|
||||||
|
resultSet[r.String()] = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the result list
|
||||||
|
results := make([]*StateFilterResult, 0, len(resultSet))
|
||||||
|
for _, v := range resultSet {
|
||||||
|
results = append(results, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort them and return
|
||||||
|
sort.Sort(StateFilterResultSlice(results))
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult {
|
||||||
|
// The slice to keep track of results
|
||||||
|
var results []*StateFilterResult
|
||||||
|
|
||||||
|
// Go through modules first.
|
||||||
|
modules := make([]*ModuleState, 0, len(f.State.Modules))
|
||||||
|
for _, m := range f.State.Modules {
|
||||||
|
if f.relevant(a, m) {
|
||||||
|
modules = append(modules, m)
|
||||||
|
|
||||||
|
// Only add the module to the results if we haven't specified a type.
|
||||||
|
// We also ignore the root module.
|
||||||
|
if a.Type == "" && len(m.Path) > 1 {
|
||||||
|
results = append(results, &StateFilterResult{
|
||||||
|
Path: m.Path[1:],
|
||||||
|
Address: (&ResourceAddress{Path: m.Path[1:]}).String(),
|
||||||
|
Value: m,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the modules set, go through all the resources within
|
||||||
|
// the modules to find relevant resources.
|
||||||
|
for _, m := range modules {
|
||||||
|
for n, r := range m.Resources {
|
||||||
|
if f.relevant(a, r) {
|
||||||
|
// The name in the state contains valuable information. Parse.
|
||||||
|
key, err := ParseResourceStateKey(n)
|
||||||
|
if err != nil {
|
||||||
|
// If we get an error parsing, then just ignore it
|
||||||
|
// out of the state.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the address for this resource
|
||||||
|
addr := &ResourceAddress{
|
||||||
|
Path: m.Path[1:],
|
||||||
|
Name: key.Name,
|
||||||
|
Type: key.Type,
|
||||||
|
Index: key.Index,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the resource level result
|
||||||
|
results = append(results, &StateFilterResult{
|
||||||
|
Path: addr.Path,
|
||||||
|
Address: addr.String(),
|
||||||
|
Value: r,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add the instances
|
||||||
|
if r.Primary != nil {
|
||||||
|
addr.InstanceType = TypePrimary
|
||||||
|
results = append(results, &StateFilterResult{
|
||||||
|
Path: addr.Path,
|
||||||
|
Address: addr.String(),
|
||||||
|
Value: r.Primary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, instance := range r.Tainted {
|
||||||
|
if f.relevant(a, instance) {
|
||||||
|
addr.InstanceType = TypeTainted
|
||||||
|
results = append(results, &StateFilterResult{
|
||||||
|
Path: addr.Path,
|
||||||
|
Address: addr.String(),
|
||||||
|
Value: instance,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, instance := range r.Deposed {
|
||||||
|
if f.relevant(a, instance) {
|
||||||
|
addr.InstanceType = TypeDeposed
|
||||||
|
results = append(results, &StateFilterResult{
|
||||||
|
Path: addr.Path,
|
||||||
|
Address: addr.String(),
|
||||||
|
Value: instance,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// relevant checks for relevance of this address against the given value.
|
||||||
|
func (f *StateFilter) relevant(addr *ResourceAddress, raw interface{}) bool {
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case *ModuleState:
|
||||||
|
path := v.Path[1:]
|
||||||
|
|
||||||
|
if len(addr.Path) > len(path) {
|
||||||
|
// Longer path in address means there is no way we match.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a prefix match
|
||||||
|
for i, p := range addr.Path {
|
||||||
|
if path[i] != p {
|
||||||
|
// Any mismatches don't match.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
case *ResourceState:
|
||||||
|
if addr.Type == "" {
|
||||||
|
// If we have no resource type, then we're interested in all!
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the type doesn't match we fail immediately
|
||||||
|
if v.Type != addr.Type {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
// If we don't know about it, let's just say no
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateFilterResult is a single result from a filter operation. Filter
|
||||||
|
// can match multiple things within a state (module, resource, instance, etc.)
|
||||||
|
// and this unifies that.
|
||||||
|
type StateFilterResult struct {
|
||||||
|
// Module path of the result
|
||||||
|
Path []string
|
||||||
|
|
||||||
|
// Address is the address that can be used to reference this exact result.
|
||||||
|
Address string
|
||||||
|
|
||||||
|
// Value is the actual value. This must be type switched on. It can be
|
||||||
|
// any data structures that `State` can hold: `ModuleState`,
|
||||||
|
// `ResourceState`, `InstanceState`.
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StateFilterResult) String() string {
|
||||||
|
return fmt.Sprintf("%T: %s", r.Value, r.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StateFilterResult) sortedType() int {
|
||||||
|
switch r.Value.(type) {
|
||||||
|
case *ModuleState:
|
||||||
|
return 0
|
||||||
|
case *ResourceState:
|
||||||
|
return 1
|
||||||
|
case *InstanceState:
|
||||||
|
return 2
|
||||||
|
default:
|
||||||
|
return 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateFilterResultSlice is a slice of results that implements
|
||||||
|
// sort.Interface. The sorting goal is what is most appealing to
|
||||||
|
// human output.
|
||||||
|
type StateFilterResultSlice []*StateFilterResult
|
||||||
|
|
||||||
|
func (s StateFilterResultSlice) Len() int { return len(s) }
|
||||||
|
func (s StateFilterResultSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
func (s StateFilterResultSlice) Less(i, j int) bool {
|
||||||
|
a, b := s[i], s[j]
|
||||||
|
|
||||||
|
// If the addresses are different it is just lexographic sorting
|
||||||
|
if a.Address != b.Address {
|
||||||
|
return a.Address < b.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addresses are the same, which means it matters on the type
|
||||||
|
return a.sortedType() < b.sortedType()
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStateFilterFilter(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
State string
|
||||||
|
Filters []string
|
||||||
|
Expected []string
|
||||||
|
}{
|
||||||
|
"all": {
|
||||||
|
"small.tfstate",
|
||||||
|
[]string{},
|
||||||
|
[]string{
|
||||||
|
"*terraform.ResourceState: aws_key_pair.onprem",
|
||||||
|
"*terraform.InstanceState: aws_key_pair.onprem",
|
||||||
|
"*terraform.ModuleState: module.bootstrap",
|
||||||
|
"*terraform.ResourceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-a",
|
||||||
|
"*terraform.InstanceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-a",
|
||||||
|
"*terraform.ResourceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-ns",
|
||||||
|
"*terraform.InstanceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-ns",
|
||||||
|
"*terraform.ResourceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
"*terraform.InstanceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"module filter": {
|
||||||
|
"complete.tfstate",
|
||||||
|
[]string{"module.bootstrap"},
|
||||||
|
[]string{
|
||||||
|
"*terraform.ModuleState: module.bootstrap",
|
||||||
|
"*terraform.ResourceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-a",
|
||||||
|
"*terraform.InstanceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-a",
|
||||||
|
"*terraform.ResourceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-ns",
|
||||||
|
"*terraform.InstanceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-ns",
|
||||||
|
"*terraform.ResourceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
"*terraform.InstanceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"resource in module": {
|
||||||
|
"complete.tfstate",
|
||||||
|
[]string{"module.bootstrap.aws_route53_zone.oasis-consul-bootstrap"},
|
||||||
|
[]string{
|
||||||
|
"*terraform.ResourceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
"*terraform.InstanceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, tc := range cases {
|
||||||
|
// Load our state
|
||||||
|
f, err := os.Open(filepath.Join("./test-fixtures", "state-filter", tc.State))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%q: err: %s", n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := ReadState(f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%q: err: %s", n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the filter
|
||||||
|
filter := &StateFilter{State: state}
|
||||||
|
|
||||||
|
// Filter!
|
||||||
|
results, err := filter.Filter(tc.Filters...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%q: err: %s", n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := make([]string, len(results))
|
||||||
|
for i, result := range results {
|
||||||
|
actual[i] = result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(actual, tc.Expected) {
|
||||||
|
t.Fatalf("%q: expected, then actual\n\n%#v\n\n%#v", n, tc.Expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,122 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"serial": 12,
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {
|
||||||
|
"public_az1_subnet_id": "subnet-d658bba0",
|
||||||
|
"region": "us-west-2",
|
||||||
|
"vpc_cidr": "10.201.0.0/16",
|
||||||
|
"vpc_id": "vpc-65814701"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"aws_key_pair.onprem": {
|
||||||
|
"type": "aws_key_pair",
|
||||||
|
"primary": {
|
||||||
|
"id": "onprem",
|
||||||
|
"attributes": {
|
||||||
|
"id": "onprem",
|
||||||
|
"key_name": "onprem",
|
||||||
|
"public_key": "foo"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"schema_version": "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root",
|
||||||
|
"bootstrap"
|
||||||
|
],
|
||||||
|
"outputs": {
|
||||||
|
"consul_bootstrap_dns": "consul.bootstrap"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"aws_route53_record.oasis-consul-bootstrap-a": {
|
||||||
|
"type": "aws_route53_record",
|
||||||
|
"depends_on": [
|
||||||
|
"aws_route53_zone.oasis-consul-bootstrap"
|
||||||
|
],
|
||||||
|
"primary": {
|
||||||
|
"id": "Z68734P5178QN_consul.bootstrap_A",
|
||||||
|
"attributes": {
|
||||||
|
"failover": "",
|
||||||
|
"fqdn": "consul.bootstrap",
|
||||||
|
"health_check_id": "",
|
||||||
|
"id": "Z68734P5178QN_consul.bootstrap_A",
|
||||||
|
"name": "consul.bootstrap",
|
||||||
|
"records.#": "6",
|
||||||
|
"records.1148461392": "10.201.3.8",
|
||||||
|
"records.1169574759": "10.201.2.8",
|
||||||
|
"records.1206973758": "10.201.1.8",
|
||||||
|
"records.1275070284": "10.201.2.4",
|
||||||
|
"records.1304587643": "10.201.3.4",
|
||||||
|
"records.1313257749": "10.201.1.4",
|
||||||
|
"set_identifier": "",
|
||||||
|
"ttl": "300",
|
||||||
|
"type": "A",
|
||||||
|
"weight": "-1",
|
||||||
|
"zone_id": "Z68734P5178QN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"aws_route53_record.oasis-consul-bootstrap-ns": {
|
||||||
|
"type": "aws_route53_record",
|
||||||
|
"depends_on": [
|
||||||
|
"aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
"aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
"aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
"aws_route53_zone.oasis-consul-bootstrap",
|
||||||
|
"aws_route53_zone.oasis-consul-bootstrap"
|
||||||
|
],
|
||||||
|
"primary": {
|
||||||
|
"id": "Z68734P5178QN_consul.bootstrap_NS",
|
||||||
|
"attributes": {
|
||||||
|
"failover": "",
|
||||||
|
"fqdn": "consul.bootstrap",
|
||||||
|
"health_check_id": "",
|
||||||
|
"id": "Z68734P5178QN_consul.bootstrap_NS",
|
||||||
|
"name": "consul.bootstrap",
|
||||||
|
"records.#": "4",
|
||||||
|
"records.1796532126": "ns-512.awsdns-00.net.",
|
||||||
|
"records.2728059479": "ns-1536.awsdns-00.co.uk.",
|
||||||
|
"records.4092160370": "ns-1024.awsdns-00.org.",
|
||||||
|
"records.456007465": "ns-0.awsdns-00.com.",
|
||||||
|
"set_identifier": "",
|
||||||
|
"ttl": "30",
|
||||||
|
"type": "NS",
|
||||||
|
"weight": "-1",
|
||||||
|
"zone_id": "Z68734P5178QN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"aws_route53_zone.oasis-consul-bootstrap": {
|
||||||
|
"type": "aws_route53_zone",
|
||||||
|
"primary": {
|
||||||
|
"id": "Z68734P5178QN",
|
||||||
|
"attributes": {
|
||||||
|
"comment": "Used to bootstrap consul dns",
|
||||||
|
"id": "Z68734P5178QN",
|
||||||
|
"name": "consul.bootstrap",
|
||||||
|
"name_servers.#": "4",
|
||||||
|
"name_servers.0": "ns-0.awsdns-00.com.",
|
||||||
|
"name_servers.1": "ns-1024.awsdns-00.org.",
|
||||||
|
"name_servers.2": "ns-1536.awsdns-00.co.uk.",
|
||||||
|
"name_servers.3": "ns-512.awsdns-00.net.",
|
||||||
|
"tags.#": "0",
|
||||||
|
"vpc_id": "vpc-65814701",
|
||||||
|
"vpc_region": "us-west-2",
|
||||||
|
"zone_id": "Z68734P5178QN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -122,20 +122,11 @@ func (c *CLI) Run() (int, error) {
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is an invalid flag, then error
|
|
||||||
if len(c.topFlags) > 0 {
|
|
||||||
c.HelpWriter.Write([]byte(
|
|
||||||
"Invalid flags before the subcommand. If these flags are for\n" +
|
|
||||||
"the subcommand, please put them after the subcommand.\n\n"))
|
|
||||||
c.HelpWriter.Write([]byte(c.HelpFunc(c.Commands) + "\n"))
|
|
||||||
return 1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to get the factory function for creating the command
|
// Attempt to get the factory function for creating the command
|
||||||
// implementation. If the command is invalid or blank, it is an error.
|
// implementation. If the command is invalid or blank, it is an error.
|
||||||
raw, ok := c.commandTree.Get(c.Subcommand())
|
raw, ok := c.commandTree.Get(c.Subcommand())
|
||||||
if !ok {
|
if !ok {
|
||||||
c.HelpWriter.Write([]byte(c.HelpFunc(c.Commands) + "\n"))
|
c.HelpWriter.Write([]byte(c.HelpFunc(c.helpCommands(c.subcommandParent())) + "\n"))
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +141,15 @@ func (c *CLI) Run() (int, error) {
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there is an invalid flag, then error
|
||||||
|
if len(c.topFlags) > 0 {
|
||||||
|
c.HelpWriter.Write([]byte(
|
||||||
|
"Invalid flags before the subcommand. If these flags are for\n" +
|
||||||
|
"the subcommand, please put them after the subcommand.\n\n"))
|
||||||
|
c.commandHelp(command)
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
code := command.Run(c.SubcommandArgs())
|
code := command.Run(c.SubcommandArgs())
|
||||||
if code == RunResultHelp {
|
if code == RunResultHelp {
|
||||||
// Requesting help
|
// Requesting help
|
||||||
|
@ -175,6 +175,27 @@ func (c *CLI) SubcommandArgs() []string {
|
||||||
return c.subcommandArgs
|
return c.subcommandArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subcommandParent returns the parent of this subcommand, if there is one.
|
||||||
|
// If there isn't on, "" is returned.
|
||||||
|
func (c *CLI) subcommandParent() string {
|
||||||
|
// Get the subcommand, if it is "" alread just return
|
||||||
|
sub := c.Subcommand()
|
||||||
|
if sub == "" {
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any trailing spaces and find the last space
|
||||||
|
sub = strings.TrimRight(sub, " ")
|
||||||
|
idx := strings.LastIndex(sub, " ")
|
||||||
|
|
||||||
|
if idx == -1 {
|
||||||
|
// No space means our parent is root
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
func (c *CLI) init() {
|
func (c *CLI) init() {
|
||||||
if c.HelpFunc == nil {
|
if c.HelpFunc == nil {
|
||||||
c.HelpFunc = BasicHelpFunc("app")
|
c.HelpFunc = BasicHelpFunc("app")
|
||||||
|
@ -268,15 +289,14 @@ func (c *CLI) commandHelp(command Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build subcommand list if we have it
|
// Build subcommand list if we have it
|
||||||
var subcommands []map[string]interface{}
|
var subcommandsTpl []map[string]interface{}
|
||||||
if c.commandNested {
|
if c.commandNested {
|
||||||
// Get the matching keys
|
// Get the matching keys
|
||||||
var keys []string
|
subcommands := c.helpCommands(c.Subcommand())
|
||||||
prefix := c.Subcommand() + " "
|
keys := make([]string, 0, len(subcommands))
|
||||||
c.commandTree.WalkPrefix(prefix, func(k string, raw interface{}) bool {
|
for k, _ := range subcommands {
|
||||||
keys = append(keys, k)
|
keys = append(keys, k)
|
||||||
return false
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// Sort the keys
|
// Sort the keys
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
|
@ -290,34 +310,30 @@ func (c *CLI) commandHelp(command Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go through and create their structures
|
// Go through and create their structures
|
||||||
subcommands = make([]map[string]interface{}, len(keys))
|
subcommandsTpl = make([]map[string]interface{}, 0, len(subcommands))
|
||||||
for i, k := range keys {
|
for k, raw := range subcommands {
|
||||||
raw, ok := c.commandTree.Get(k)
|
|
||||||
if !ok {
|
|
||||||
// We just checked that it should be here above. If it is
|
|
||||||
// isn't, there are serious problems.
|
|
||||||
panic("value is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the command
|
// Get the command
|
||||||
sub, err := raw.(CommandFactory)()
|
sub, err := raw()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HelpWriter.Write([]byte(fmt.Sprintf(
|
c.HelpWriter.Write([]byte(fmt.Sprintf(
|
||||||
"Error instantiating %q: %s", k, err)))
|
"Error instantiating %q: %s", k, err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine some info
|
// Find the last space and make sure we only include that last part
|
||||||
name := strings.TrimPrefix(k, prefix)
|
name := k
|
||||||
|
if idx := strings.LastIndex(k, " "); idx > -1 {
|
||||||
|
name = name[idx+1:]
|
||||||
|
}
|
||||||
|
|
||||||
subcommands[i] = map[string]interface{}{
|
subcommandsTpl = append(subcommandsTpl, map[string]interface{}{
|
||||||
"Name": name,
|
"Name": name,
|
||||||
"NameAligned": name + strings.Repeat(" ", longest-len(k)),
|
"NameAligned": name + strings.Repeat(" ", longest-len(k)),
|
||||||
"Help": sub.Help(),
|
"Help": sub.Help(),
|
||||||
"Synopsis": sub.Synopsis(),
|
"Synopsis": sub.Synopsis(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data["Subcommands"] = subcommands
|
data["Subcommands"] = subcommandsTpl
|
||||||
|
|
||||||
// Write
|
// Write
|
||||||
err = t.Execute(c.HelpWriter, data)
|
err = t.Execute(c.HelpWriter, data)
|
||||||
|
@ -330,6 +346,40 @@ func (c *CLI) commandHelp(command Command) {
|
||||||
"Internal error rendering help: %s", err)))
|
"Internal error rendering help: %s", err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helpCommands returns the subcommands for the HelpFunc argument.
|
||||||
|
// This will only contain immediate subcommands.
|
||||||
|
func (c *CLI) helpCommands(prefix string) map[string]CommandFactory {
|
||||||
|
// If our prefix isn't empty, make sure it ends in ' '
|
||||||
|
if prefix != "" && prefix[len(prefix)-1] != ' ' {
|
||||||
|
prefix += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all the subkeys of this command
|
||||||
|
var keys []string
|
||||||
|
c.commandTree.WalkPrefix(prefix, func(k string, raw interface{}) bool {
|
||||||
|
// Ignore any sub-sub keys, i.e. "foo bar baz" when we want "foo bar"
|
||||||
|
if !strings.Contains(k[len(prefix):], " ") {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// For each of the keys return that in the map
|
||||||
|
result := make(map[string]CommandFactory, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
raw, ok := c.commandTree.Get(k)
|
||||||
|
if !ok {
|
||||||
|
// We just got it via WalkPrefix above, so we just panic
|
||||||
|
panic("not found: " + k)
|
||||||
|
}
|
||||||
|
|
||||||
|
result[k] = raw.(CommandFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (c *CLI) processArgs() {
|
func (c *CLI) processArgs() {
|
||||||
for i, arg := range c.Args {
|
for i, arg := range c.Args {
|
||||||
if c.subcommand == "" {
|
if c.subcommand == "" {
|
||||||
|
|
|
@ -6,6 +6,7 @@ body.page-sub{
|
||||||
background-color: $light-black;
|
background-color: $light-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.layout-commands-state,
|
||||||
body.layout-atlas,
|
body.layout-atlas,
|
||||||
body.layout-aws,
|
body.layout-aws,
|
||||||
body.layout-azure,
|
body.layout-azure,
|
||||||
|
|
|
@ -55,4 +55,3 @@ Usage: terraform graph [options] PATH
|
||||||
read this format is GraphViz, but many web services are also available
|
read this format is GraphViz, but many web services are also available
|
||||||
to read this format.
|
to read this format.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
layout: "commands-state"
|
||||||
|
page_title: "Command: state resource addressing"
|
||||||
|
sidebar_current: "docs-state-address"
|
||||||
|
description: |-
|
||||||
|
The `terraform state` command is used for advanced state management.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resource Addressing
|
||||||
|
|
||||||
|
The `terraform state` subcommands make heavy use of resource addressing
|
||||||
|
for targeting and filtering specific resources and modules within the state.
|
||||||
|
|
||||||
|
Resource addressing is a common feature of Terraform that is used in
|
||||||
|
multiple locations. For example, resource addressing syntax is also used for
|
||||||
|
the `-target` flag for apply and plan commands.
|
||||||
|
|
||||||
|
Because resource addressing is unified across Terraform, it is documented
|
||||||
|
in a single place rather than duplicating it in multiple locations. You
|
||||||
|
can find the [resource addressing documentation here](/docs/internals/resource-addressing.html).
|
|
@ -0,0 +1,54 @@
|
||||||
|
---
|
||||||
|
layout: "commands-state"
|
||||||
|
page_title: "Command: state"
|
||||||
|
sidebar_current: "docs-state-index"
|
||||||
|
description: |-
|
||||||
|
The `terraform state` command is used for advanced state management.
|
||||||
|
---
|
||||||
|
|
||||||
|
# State Command
|
||||||
|
|
||||||
|
The `terraform state` command is used for advanced state management.
|
||||||
|
As your Terraform usage becomes more advanced, there are some cases where
|
||||||
|
you may need to modify the [Terraform state](/docs/state/index.html).
|
||||||
|
Rather than modify the state directly, the `terraform state` commands can
|
||||||
|
be used in many cases instead.
|
||||||
|
|
||||||
|
This command is a nested subcommand, meaning that it has further subcommands.
|
||||||
|
These subcommands are listed to the left.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Usage: `terraform state <subcommand> [options] [args]`
|
||||||
|
|
||||||
|
Please click a subcommand to the left for more information.
|
||||||
|
|
||||||
|
## Remote State
|
||||||
|
|
||||||
|
The Terraform state subcommands all work with remote state just as if it
|
||||||
|
was local state. Reads and writes may take longer than normal as each read
|
||||||
|
and each write do a full network roundtrip. Otherwise, backups are still
|
||||||
|
written to disk and the CLI usage is the same as if it were local state.
|
||||||
|
|
||||||
|
## Backups
|
||||||
|
|
||||||
|
All `terraform state` subcommands that modify the state write backup
|
||||||
|
files. The path of these backup file can be controlled with `-backup`.
|
||||||
|
|
||||||
|
Subcommands that are read-only (such as [list](/docs/commands/state/list.html))
|
||||||
|
do not write any backup files since they aren't modifying the state.
|
||||||
|
|
||||||
|
Note that backups for state modification _can not be disabled_. Due to
|
||||||
|
the sensitivity of the state file, Terraform forces every state modification
|
||||||
|
command to write a backup file. You'll have to remove these files manually
|
||||||
|
if you don't want to keep them around.
|
||||||
|
|
||||||
|
## Command-Line Friendly
|
||||||
|
|
||||||
|
The output and command-line structure of the state subcommands is
|
||||||
|
designed to be easy to use with Unix command-line tools such as grep, awk,
|
||||||
|
etc. Consequently, the output is also friendly to the equivalent PowerShell
|
||||||
|
commands within Windows.
|
||||||
|
|
||||||
|
For advanced filtering and modification, we recommend piping Terraform
|
||||||
|
state subcommands together with other command line tools.
|
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
layout: "commands-state"
|
||||||
|
page_title: "Command: state list"
|
||||||
|
sidebar_current: "docs-state-sub-list"
|
||||||
|
description: |-
|
||||||
|
The `terraform init` command is used to initialize a Terraform configuration using another module as a skeleton.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Command: state list
|
||||||
|
|
||||||
|
The `terraform state list` command is used to list resources within a
|
||||||
|
[Terraform state](/docs/state/index.html).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Usage: `terraform state list [options] [pattern...]`
|
||||||
|
|
||||||
|
The command will list all resources in the state file matching the given
|
||||||
|
patterns (if any). If no patterns are given, all resources are listed.
|
||||||
|
|
||||||
|
The resources listed are sorted according to module depth order followed
|
||||||
|
by alphabetical. This means that resources that are in your immediate
|
||||||
|
configuration are listed first, and resources that are more deeply nested
|
||||||
|
within modules are listed last.
|
||||||
|
|
||||||
|
For complex infrastructures, the state can contain thousands of resources.
|
||||||
|
To filter these, provide one or more patterns to the command. Patterns are
|
||||||
|
in [resource addressing format](/docs/commands/state/addressing.html).
|
||||||
|
|
||||||
|
The command-line flags are all optional. The list of available flags are:
|
||||||
|
|
||||||
|
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
|
||||||
|
|
||||||
|
## Example: All Resources
|
||||||
|
|
||||||
|
This example will list all resources, including modules:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ terraform state list
|
||||||
|
aws_instance.foo
|
||||||
|
aws_instance.bar[0]
|
||||||
|
aws_instance.bar[1]
|
||||||
|
module.elb.aws_elb.main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Filtering by Resource
|
||||||
|
|
||||||
|
This example will only list resources for the given name:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ terraform state list aws_instance.bar
|
||||||
|
aws_instance.bar[0]
|
||||||
|
aws_instance.bar[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Filtering by Module
|
||||||
|
|
||||||
|
This example will only list resources in the given module:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ terraform state list module.elb
|
||||||
|
module.elb.aws_elb.main
|
||||||
|
```
|
|
@ -25,6 +25,23 @@ state file with the real infrastructure if the file didn't exist. But currently,
|
||||||
Terraform state is a mixture of both a cache and required configuration and
|
Terraform state is a mixture of both a cache and required configuration and
|
||||||
isn't optional.
|
isn't optional.
|
||||||
|
|
||||||
|
## Inspection and Modification
|
||||||
|
|
||||||
|
While the format of the state files are just JSON, direct file editing
|
||||||
|
of the state is discouraged. Terraform provides the
|
||||||
|
[terraform state](/docs/commands/state/index.html) command to perform
|
||||||
|
basic modifications of the state using the CLI.
|
||||||
|
|
||||||
|
The CLI usage and output of the state commands is structured to be
|
||||||
|
friendly for Unix tools such as grep, awk, etc. Additionally, the CLI
|
||||||
|
insulates users from any format changes within the state itself. The Terraform
|
||||||
|
project will keep the CLI working while the state format underneath it may
|
||||||
|
shift.
|
||||||
|
|
||||||
|
Finally, the CLI manages backups for you automatically. If you make a mistake
|
||||||
|
modifying your state, the state CLI will always have a backup available for
|
||||||
|
you that you can restore.
|
||||||
|
|
||||||
## Format
|
## Format
|
||||||
|
|
||||||
The state is in JSON format and Terraform will promise backwards compatibility
|
The state is in JSON format and Terraform will promise backwards compatibility
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<% wrap_layout :inner do %>
|
||||||
|
<% content_for :sidebar do %>
|
||||||
|
<div class="docs-sidebar hidden-print affix-top" role="complementary">
|
||||||
|
<ul class="nav docs-sidenav">
|
||||||
|
<li<%= sidebar_current("docs-home") %>>
|
||||||
|
<a href="/docs/commands/index.html">« Documentation Home</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-state-index") %>>
|
||||||
|
<a href="/docs/commands/state/index.html">State Command</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-state-address") %>>
|
||||||
|
<a href="/docs/commands/state/addressing.html">Resource Addressing</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current(/^docs-state-sub/) %>>
|
||||||
|
<a href="#">Subcommands</a>
|
||||||
|
<ul class="nav nav-visible">
|
||||||
|
<li<%= sidebar_current("docs-state-sub-list") %>>
|
||||||
|
<a href="/docs/commands/state/list.html">list</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= yield %>
|
||||||
|
<% end %>
|
|
@ -107,6 +107,10 @@
|
||||||
<a href="/docs/commands/show.html">show</a>
|
<a href="/docs/commands/show.html">show</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-commands-state") %>>
|
||||||
|
<a href="/docs/commands/state/index.html">state</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-commands-taint") %>>
|
<li<%= sidebar_current("docs-commands-taint") %>>
|
||||||
<a href="/docs/commands/taint.html">taint</a>
|
<a href="/docs/commands/taint.html">taint</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -269,7 +273,7 @@
|
||||||
<a href="/docs/providers/tls/index.html">TLS</a>
|
<a href="/docs/providers/tls/index.html">TLS</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-providers-triton") %>>
|
<li<%= sidebar_current("docs-providers-triton") %>>
|
||||||
<a href="/docs/providers/triton/index.html">Triton</a>
|
<a href="/docs/providers/triton/index.html">Triton</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue