diff --git a/command/format/state.go b/command/format/state.go index 779642772..6b3bd378f 100644 --- a/command/format/state.go +++ b/command/format/state.go @@ -7,12 +7,11 @@ import ( "strings" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/plans" - - "github.com/mitchellh/colorstring" - "github.com/hashicorp/terraform/states" - // "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/colorstring" ) // StateOpts are the options for formatting a state. @@ -20,6 +19,9 @@ type StateOpts struct { // State is the state to format. This is required. State *states.State + // Schemas are used to decode attributes. This is required. + Schemas *terraform.Schemas + // Color is the colorizer. This is optional. Color *colorstring.Colorize } @@ -30,6 +32,10 @@ func State(opts *StateOpts) string { panic("colorize not given") } + if opts.Schemas == nil { + panic("schemas not given") + } + s := opts.State if len(s.Modules) == 0 { return "The state file is empty. No resources are represented." @@ -45,7 +51,7 @@ func State(opts *StateOpts) string { // Format all the modules for _, m := range s.Modules { - formatStateModule(&buf, m, opts) + formatStateModule(p, m, opts.Schemas) } // Write the outputs for the root module @@ -53,7 +59,7 @@ func State(opts *StateOpts) string { if m.OutputValues != nil { if len(m.OutputValues) > 0 { - buf.WriteString("\nOutputs:\n\n") + p.buf.WriteString("\nOutputs:\n\n") } // Sort the outputs @@ -66,17 +72,17 @@ func State(opts *StateOpts) string { // Output each output k/v pair for _, k := range ks { v := m.OutputValues[k] - buf.WriteString(fmt.Sprintf("%s = ", k)) + p.buf.WriteString(fmt.Sprintf("%s = ", k)) p.writeValue(v.Value, plans.NoOp, 0) } } - return opts.Color.Color(strings.TrimSpace(buf.String())) + return opts.Color.Color(strings.TrimSpace(p.buf.String())) } func formatStateModule( - buf *bytes.Buffer, m *states.Module, opts *StateOpts) { + p blockBodyDiffPrinter, m *states.Module, schemas *terraform.Schemas) { var moduleName string if !m.Addr.IsRoot() { @@ -92,75 +98,64 @@ func formatStateModule( sort.Strings(names) // Go through each resource and begin building up the output. - for _, k := range names { - name := k - if moduleName != "" { - name = moduleName + "." + name + for _, key := range names { + taintStr := "" + instances := m.Resources[key].Instances + for k, v := range instances { + name := key + if moduleName != "" { + name = moduleName + "." + name + } + + addr := m.Resources[key].Addr + if v.Current.Status == 'T' { + taintStr = "(tainted)" + } + p.buf.WriteString(fmt.Sprintf("# %s: %s\n", addr.Instance(k), taintStr)) + taintStr = "" + + var schema *configschema.Block + provider := m.Resources[key].ProviderConfig.ProviderConfig.StringCompact() + + switch addr.Mode { + case addrs.ManagedResourceMode: + p.buf.WriteString(fmt.Sprintf( + "resource %q %q {\n", + addr.Type, + addr.Name, + )) + schema = schemas.Providers[provider].ResourceTypes[addr.Type] + case addrs.DataResourceMode: + p.buf.WriteString(fmt.Sprintf( + "data %q %q {\n", + addr.Type, + addr.Name, + )) + schema = schemas.Providers[provider].DataSources[addr.Type] + default: + // should never happen, since the above is exhaustive + p.buf.WriteString(addr.String()) + } + + val, err := v.Current.Decode(schema.ImpliedType()) + + if err != nil { + fmt.Println(err.Error()) + break + } + for name := range schema.Attributes { + attr := ctyGetAttrMaybeNull(val.Value, name) + if !attr.IsNull() { + p.buf.WriteString(fmt.Sprintf(" %s = ", name)) + attr := ctyGetAttrMaybeNull(val.Value, name) + p.writeValue(attr, plans.NoOp, 4) + p.buf.WriteString("\n") + } + } + p.buf.WriteString("}\n\n") } - - addr := m.Resources[k].Addr - switch addr.Mode { - case addrs.ManagedResourceMode: - buf.WriteString(fmt.Sprintf( - "resource %q %q", - addr.Type, - addr.Name, - )) - case addrs.DataResourceMode: - buf.WriteString(fmt.Sprintf( - "data %q %q ", - addr.Type, - addr.Name, - )) - default: - // should never happen, since the above is exhaustive - buf.WriteString(addr.String()) - } - - buf.WriteString(" { attrs go here! }\n") - } - - // rs := m.Resources[k] - // is := rs.Instance - // var id string - // if is != nil { - // id = is - // } - // if id == "" { - // id = "" - // } - - // taintStr := "" - // // if rs.Primary != nil && rs.Primary.Tainted { - // // taintStr = " (tainted)" - // // } - - // buf.WriteString(fmt.Sprintf("%s:%s\n", name, taintStr)) - // buf.WriteString(fmt.Sprintf(" id = %s\n", id)) - - // if is != nil { - // // Sort the attributes - // attrKeys := make([]string, 0, len(is.Attributes)) - // for ak, _ := range is.Attributes { - // // Skip the id attribute since we just show the id directly - // if ak == "id" { - // continue - // } - - // attrKeys = append(attrKeys, ak) - // } - // sort.Strings(attrKeys) - - // // Output each attribute - // for _, ak := range attrKeys { - // av := is.Attributes[ak] - // buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av)) - // } - // } - // } - - buf.WriteString("[reset]\n") + p.buf.WriteString("[reset]\n") } func formatNestedList(indent string, outputList []interface{}) string { diff --git a/command/format/state_test.go b/command/format/state_test.go index 3415841c5..7da016cb6 100644 --- a/command/format/state_test.go +++ b/command/format/state_test.go @@ -1,11 +1,13 @@ package format import ( - "fmt" "testing" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/colorstring" "github.com/zclconf/go-cty/cty" ) @@ -28,7 +30,7 @@ func TestState(t *testing.T) { rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, - Type: "test_thing", + Type: "test_resource", Name: "baz", }.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ @@ -47,17 +49,19 @@ func TestState(t *testing.T) { }{ { &StateOpts{ - State: &states.State{}, - Color: disabledColorize, + State: &states.State{}, + Color: disabledColorize, + Schemas: &terraform.Schemas{}, }, "The state file is empty. No resources are represented.", }, { &StateOpts{ - State: state, - Color: disabledColorize, + State: state, + Color: disabledColorize, + Schemas: testSchemas(), }, - "module.test_module.test_resource.foo", + "test_resource.baz", }, } @@ -72,11 +76,49 @@ func TestState(t *testing.T) { } } -func mustParseModuleInstanceStr(s string) addrs.ModuleInstance { - addr, err := addrs.ParseModuleInstanceStr(s) - if err != nil { - fmt.Printf(err.Err().Error()) - panic(err) +func testProvider() *terraform.MockProvider { + p := new(terraform.MockProvider) + p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { + return providers.ReadResourceResponse{NewState: req.PriorState} + } + + p.GetSchemaReturn = testProviderSchema() + + return p +} + +func testProviderSchema() *terraform.ProviderSchema { + return &terraform.ProviderSchema{ + Provider: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "region": {Type: cty.String, Optional: true}, + }, + }, + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "foo": {Type: cty.String, Optional: true}, + "woozles": {Type: cty.String, Optional: true}, + }, + }, + }, + DataSources: map[string]*configschema.Block{ + "test_data_source": { + Attributes: map[string]*configschema.Attribute{ + "compute": {Type: cty.String, Optional: true}, + "value": {Type: cty.String, Computed: true}, + }, + }, + }, + } +} + +func testSchemas() *terraform.Schemas { + provider := testProvider() + return &terraform.Schemas{ + Providers: map[string]*terraform.ProviderSchema{ + "test": provider.GetSchemaReturn, + }, } - return addr } diff --git a/command/show.go b/command/show.go index 01a9406cb..e1ccf95ac 100644 --- a/command/show.go +++ b/command/show.go @@ -6,8 +6,10 @@ import ( "os" "strings" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/states/statefile" + "github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/command/format" "github.com/hashicorp/terraform/plans" @@ -43,6 +45,52 @@ func (c *ShowCommand) Run(args []string) int { return 1 } + configPath, err := ModulePath(cmdFlags.Args()) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + var diags tfdiags.Diagnostics + + // Load the backend + b, backendDiags := c.Backend(nil) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // We require a local backend + local, ok := b.(backend.Local) + if !ok { + c.showDiagnostics(diags) // in case of any warnings in here + c.Ui.Error(ErrUnsupportedLocalOp) + return 1 + } + + // Build the operation + opReq := c.Operation(b) + opReq.ConfigDir = configPath + opReq.ConfigLoader, err = c.initConfigLoader() + if err != nil { + diags = diags.Append(err) + c.showDiagnostics(diags) + return 1 + } + + // Get the context + ctx, _, ctxDiags := local.Context(opReq) + diags = diags.Append(ctxDiags) + if ctxDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + schemas := ctx.Schemas() + + env := c.Workspace() + var planErr, stateErr error var path string var plan *plans.Plan @@ -72,15 +120,6 @@ func (c *ShowCommand) Run(args []string) int { } } } else { - // Load the backend - b, backendDiags := c.Backend(nil) - if backendDiags.HasErrors() { - c.showDiagnostics(backendDiags) - return 1 - } - - env := c.Workspace() - // Get the state stateStore, err := b.StateMgr(env) if err != nil { @@ -118,8 +157,9 @@ func (c *ShowCommand) Run(args []string) int { } c.Ui.Output(format.State(&format.StateOpts{ - State: state, - Color: c.Colorize(), + State: state, + Color: c.Colorize(), + Schemas: schemas, })) return 0 }