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:
Mitchell Hashimoto 2016-03-22 10:41:02 -07:00 committed by James Nugent
parent 8e4da4e2a1
commit d1b46e99bd
23 changed files with 2405 additions and 57 deletions

2
Godeps/Godeps.json generated
View File

@ -962,7 +962,7 @@
},
{
"ImportPath": "github.com/mitchellh/cli",
"Rev": "cb6853d606ea4a12a15ac83cc43503df99fd28fb"
"Rev": "83f97d41cf100ee5f33944a8815c167d5e4aa272"
},
{
"ImportPath": "github.com/mitchellh/cloudflare-go",

View File

@ -326,6 +326,9 @@ func (m *Meta) flagSet(n string) *flag.FlagSet {
}()
f.SetOutput(errW)
// Set the default Usage to empty
f.Usage = func() {}
return f
}

40
command/state_command.go Normal file
View File

@ -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"
}

101
command/state_list.go Normal file
View File

@ -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.`

View File

@ -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
`

View File

@ -10,6 +10,7 @@ import (
// Commands is the mapping of all the available Terraform commands.
var Commands map[string]cli.CommandFactory
var PlumbingCommands map[string]struct{}
// Ui is the cli.Ui used for communicating to the outside world.
var Ui cli.Ui
@ -34,6 +35,10 @@ func init() {
Ui: Ui,
}
PlumbingCommands = map[string]struct{}{
"state": struct{}{}, // includes all subcommands
}
Commands = map[string]cli.CommandFactory{
"apply": func() (cli.Command, error) {
return &command.ApplyCommand{
@ -137,6 +142,22 @@ func init() {
Meta: meta,
}, 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
},
}
}

79
help.go Normal file
View File

@ -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()
}

View File

@ -113,7 +113,7 @@ func wrappedMain() int {
cli := &cli.CLI{
Args: args,
Commands: Commands,
HelpFunc: cli.BasicHelpFunc("terraform"),
HelpFunc: helpFunc,
HelpWriter: os.Stdout,
}

View File

@ -38,6 +38,35 @@ func (r *ResourceAddress) Copy() *ResourceAddress {
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) {
matches, err := tokenizeResourceAddress(s)
if err != nil {

View File

@ -9,109 +9,121 @@ func TestParseResourceAddress(t *testing.T) {
cases := map[string]struct {
Input string
Expected *ResourceAddress
Output string
}{
"implicit primary, no specific index": {
Input: "aws_instance.foo",
Expected: &ResourceAddress{
"aws_instance.foo",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"implicit primary, explicit index": {
Input: "aws_instance.foo[2]",
Expected: &ResourceAddress{
"aws_instance.foo[2]",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 2,
},
"",
},
"implicit primary, explicit index over ten": {
Input: "aws_instance.foo[12]",
Expected: &ResourceAddress{
"aws_instance.foo[12]",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 12,
},
"",
},
"explicit primary, explicit index": {
Input: "aws_instance.foo.primary[2]",
Expected: &ResourceAddress{
"aws_instance.foo.primary[2]",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 2,
},
"aws_instance.foo[2]",
},
"tainted": {
Input: "aws_instance.foo.tainted",
Expected: &ResourceAddress{
"aws_instance.foo.tainted",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypeTainted,
Index: -1,
},
"",
},
"deposed": {
Input: "aws_instance.foo.deposed",
Expected: &ResourceAddress{
"aws_instance.foo.deposed",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypeDeposed,
Index: -1,
},
"",
},
"with a hyphen": {
Input: "aws_instance.foo-bar",
Expected: &ResourceAddress{
"aws_instance.foo-bar",
&ResourceAddress{
Type: "aws_instance",
Name: "foo-bar",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"in a module": {
Input: "module.child.aws_instance.foo",
Expected: &ResourceAddress{
"module.child.aws_instance.foo",
&ResourceAddress{
Path: []string{"child"},
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"nested modules": {
Input: "module.a.module.b.module.forever.aws_instance.foo",
Expected: &ResourceAddress{
"module.a.module.b.module.forever.aws_instance.foo",
&ResourceAddress{
Path: []string{"a", "b", "forever"},
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"just a module": {
Input: "module.a",
Expected: &ResourceAddress{
"module.a",
&ResourceAddress{
Path: []string{"a"},
Type: "",
Name: "",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"just a nested module": {
Input: "module.a.module.b",
Expected: &ResourceAddress{
"module.a.module.b",
&ResourceAddress{
Path: []string{"a", "b"},
Type: "",
Name: "",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
}
@ -124,6 +136,14 @@ func TestParseResourceAddress(t *testing.T) {
if !reflect.DeepEqual(out, tc.Expected) {
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)
}
}
}

238
terraform/state_filter.go Normal file
View File

@ -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()
}

View File

@ -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

View File

@ -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"
}
}
}
}
}
]
}

View File

@ -122,20 +122,11 @@ func (c *CLI) Run() (int, error) {
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
// implementation. If the command is invalid or blank, it is an error.
raw, ok := c.commandTree.Get(c.Subcommand())
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
}
@ -150,6 +141,15 @@ func (c *CLI) Run() (int, error) {
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())
if code == RunResultHelp {
// Requesting help
@ -175,6 +175,27 @@ func (c *CLI) SubcommandArgs() []string {
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() {
if c.HelpFunc == nil {
c.HelpFunc = BasicHelpFunc("app")
@ -268,15 +289,14 @@ func (c *CLI) commandHelp(command Command) {
}
// Build subcommand list if we have it
var subcommands []map[string]interface{}
var subcommandsTpl []map[string]interface{}
if c.commandNested {
// Get the matching keys
var keys []string
prefix := c.Subcommand() + " "
c.commandTree.WalkPrefix(prefix, func(k string, raw interface{}) bool {
subcommands := c.helpCommands(c.Subcommand())
keys := make([]string, 0, len(subcommands))
for k, _ := range subcommands {
keys = append(keys, k)
return false
})
}
// Sort the keys
sort.Strings(keys)
@ -290,34 +310,30 @@ func (c *CLI) commandHelp(command Command) {
}
// Go through and create their structures
subcommands = make([]map[string]interface{}, len(keys))
for i, k := range keys {
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")
}
subcommandsTpl = make([]map[string]interface{}, 0, len(subcommands))
for k, raw := range subcommands {
// Get the command
sub, err := raw.(CommandFactory)()
sub, err := raw()
if err != nil {
c.HelpWriter.Write([]byte(fmt.Sprintf(
"Error instantiating %q: %s", k, err)))
}
// Determine some info
name := strings.TrimPrefix(k, prefix)
// Find the last space and make sure we only include that last part
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,
"NameAligned": name + strings.Repeat(" ", longest-len(k)),
"Help": sub.Help(),
"Synopsis": sub.Synopsis(),
}
})
}
}
data["Subcommands"] = subcommands
data["Subcommands"] = subcommandsTpl
// Write
err = t.Execute(c.HelpWriter, data)
@ -330,6 +346,40 @@ func (c *CLI) commandHelp(command Command) {
"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() {
for i, arg := range c.Args {
if c.subcommand == "" {

View File

@ -6,6 +6,7 @@ body.page-sub{
background-color: $light-black;
}
body.layout-commands-state,
body.layout-atlas,
body.layout-aws,
body.layout-azure,

View File

@ -55,4 +55,3 @@ Usage: terraform graph [options] PATH
read this format is GraphViz, but many web services are also available
to read this format.
```

View File

@ -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).

View File

@ -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.

View File

@ -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
```

View File

@ -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
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
The state is in JSON format and Terraform will promise backwards compatibility

View File

@ -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">&laquo; 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 %>

View File

@ -107,6 +107,10 @@
<a href="/docs/commands/show.html">show</a>
</li>
<li<%= sidebar_current("docs-commands-state") %>>
<a href="/docs/commands/state/index.html">state</a>
</li>
<li<%= sidebar_current("docs-commands-taint") %>>
<a href="/docs/commands/taint.html">taint</a>
</li>
@ -269,7 +273,7 @@
<a href="/docs/providers/tls/index.html">TLS</a>
</li>
<li<%= sidebar_current("docs-providers-triton") %>>
<li<%= sidebar_current("docs-providers-triton") %>>
<a href="/docs/providers/triton/index.html">Triton</a>
</li>