| package states |
| |
| import ( |
| "bufio" |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "sort" |
| "strings" |
| |
| ctyjson "github.com/zclconf/go-cty/cty/json" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/configs/hcl2shim" |
| ) |
| |
| // String returns a rather-odd string representation of the entire state. |
| // |
| // This is intended to match the behavior of the older terraform.State.String |
| // method that is used in lots of existing tests. It should not be used in |
| // new tests: instead, use "cmp" to directly compare the state data structures |
| // and print out a diff if they do not match. |
| // |
| // This method should never be used in non-test code, whether directly by call |
| // or indirectly via a %s or %q verb in package fmt. |
| func (s *State) String() string { |
| if s == nil { |
| return "<nil>" |
| } |
| |
| // sort the modules by name for consistent output |
| modules := make([]string, 0, len(s.Modules)) |
| for m := range s.Modules { |
| modules = append(modules, m) |
| } |
| sort.Strings(modules) |
| |
| var buf bytes.Buffer |
| for _, name := range modules { |
| m := s.Modules[name] |
| mStr := m.testString() |
| |
| // If we're the root module, we just write the output directly. |
| if m.Addr.IsRoot() { |
| buf.WriteString(mStr + "\n") |
| continue |
| } |
| |
| // We need to build out a string that resembles the not-quite-standard |
| // format that terraform.State.String used to use, where there's a |
| // "module." prefix but then just a chain of all of the module names |
| // without any further "module." portions. |
| buf.WriteString("module") |
| for _, step := range m.Addr { |
| buf.WriteByte('.') |
| buf.WriteString(step.Name) |
| if step.InstanceKey != addrs.NoKey { |
| buf.WriteString(step.InstanceKey.String()) |
| } |
| } |
| buf.WriteString(":\n") |
| |
| s := bufio.NewScanner(strings.NewReader(mStr)) |
| for s.Scan() { |
| text := s.Text() |
| if text != "" { |
| text = " " + text |
| } |
| |
| buf.WriteString(fmt.Sprintf("%s\n", text)) |
| } |
| } |
| |
| return strings.TrimSpace(buf.String()) |
| } |
| |
| // testString is used to produce part of the output of State.String. It should |
| // never be used directly. |
| func (ms *Module) testString() string { |
| var buf bytes.Buffer |
| |
| if len(ms.Resources) == 0 { |
| buf.WriteString("<no state>") |
| } |
| |
| // We use AbsResourceInstance here, even though everything belongs to |
| // the same module, just because we have a sorting behavior defined |
| // for those but not for just ResourceInstance. |
| addrsOrder := make([]addrs.AbsResourceInstance, 0, len(ms.Resources)) |
| for _, rs := range ms.Resources { |
| for ik := range rs.Instances { |
| addrsOrder = append(addrsOrder, rs.Addr.Instance(ik)) |
| } |
| } |
| |
| sort.Slice(addrsOrder, func(i, j int) bool { |
| return addrsOrder[i].Less(addrsOrder[j]) |
| }) |
| |
| for _, fakeAbsAddr := range addrsOrder { |
| addr := fakeAbsAddr.Resource |
| rs := ms.Resource(addr.ContainingResource()) |
| is := ms.ResourceInstance(addr) |
| |
| // Here we need to fake up a legacy-style address as the old state |
| // types would've used, since that's what our tests against those |
| // old types expect. The significant difference is that instancekey |
| // is dot-separated rather than using index brackets. |
| k := addr.ContainingResource().String() |
| if addr.Key != addrs.NoKey { |
| switch tk := addr.Key.(type) { |
| case addrs.IntKey: |
| k = fmt.Sprintf("%s.%d", k, tk) |
| default: |
| // No other key types existed for the legacy types, so we |
| // can do whatever we want here. We'll just use our standard |
| // syntax for these. |
| k = k + tk.String() |
| } |
| } |
| |
| id := LegacyInstanceObjectID(is.Current) |
| |
| taintStr := "" |
| if is.Current != nil && is.Current.Status == ObjectTainted { |
| taintStr = " (tainted)" |
| } |
| |
| deposedStr := "" |
| if len(is.Deposed) > 0 { |
| deposedStr = fmt.Sprintf(" (%d deposed)", len(is.Deposed)) |
| } |
| |
| buf.WriteString(fmt.Sprintf("%s:%s%s\n", k, taintStr, deposedStr)) |
| buf.WriteString(fmt.Sprintf(" ID = %s\n", id)) |
| buf.WriteString(fmt.Sprintf(" provider = %s\n", rs.ProviderConfig.String())) |
| |
| // Attributes were a flatmap before, but are not anymore. To preserve |
| // our old output as closely as possible we need to do a conversion |
| // to flatmap. Normally we'd want to do this with schema for |
| // accuracy, but for our purposes here it only needs to be approximate. |
| // This should produce an identical result for most cases, though |
| // in particular will differ in a few cases: |
| // - The keys used for elements in a set will be different |
| // - Values for attributes of type cty.DynamicPseudoType will be |
| // misinterpreted (but these weren't possible in old world anyway) |
| var attributes map[string]string |
| if obj := is.Current; obj != nil { |
| switch { |
| case obj.AttrsFlat != nil: |
| // Easy (but increasingly unlikely) case: the state hasn't |
| // actually been upgraded to the new form yet. |
| attributes = obj.AttrsFlat |
| case obj.AttrsJSON != nil: |
| ty, err := ctyjson.ImpliedType(obj.AttrsJSON) |
| if err == nil { |
| val, err := ctyjson.Unmarshal(obj.AttrsJSON, ty) |
| if err == nil { |
| attributes = hcl2shim.FlatmapValueFromHCL2(val) |
| } |
| } |
| } |
| } |
| attrKeys := make([]string, 0, len(attributes)) |
| for ak, val := range attributes { |
| if ak == "id" { |
| continue |
| } |
| |
| // don't show empty containers in the output |
| if val == "0" && (strings.HasSuffix(ak, ".#") || strings.HasSuffix(ak, ".%")) { |
| continue |
| } |
| |
| attrKeys = append(attrKeys, ak) |
| } |
| |
| sort.Strings(attrKeys) |
| |
| for _, ak := range attrKeys { |
| av := attributes[ak] |
| buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av)) |
| } |
| |
| // CAUTION: Since deposed keys are now random strings instead of |
| // incrementing integers, this result will not be deterministic |
| // if there is more than one deposed object. |
| i := 1 |
| for _, t := range is.Deposed { |
| id := LegacyInstanceObjectID(t) |
| taintStr := "" |
| if t.Status == ObjectTainted { |
| taintStr = " (tainted)" |
| } |
| buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s%s\n", i, id, taintStr)) |
| i++ |
| } |
| |
| if obj := is.Current; obj != nil && len(obj.Dependencies) > 0 { |
| buf.WriteString("\n Dependencies:\n") |
| for _, dep := range obj.Dependencies { |
| buf.WriteString(fmt.Sprintf(" %s\n", dep.String())) |
| } |
| } |
| } |
| |
| if len(ms.OutputValues) > 0 { |
| buf.WriteString("\nOutputs:\n\n") |
| |
| ks := make([]string, 0, len(ms.OutputValues)) |
| for k := range ms.OutputValues { |
| ks = append(ks, k) |
| } |
| sort.Strings(ks) |
| |
| for _, k := range ks { |
| v := ms.OutputValues[k] |
| lv := hcl2shim.ConfigValueFromHCL2(v.Value) |
| switch vTyped := lv.(type) { |
| case string: |
| buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped)) |
| case []interface{}: |
| buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped)) |
| case map[string]interface{}: |
| var mapKeys []string |
| for key := range vTyped { |
| mapKeys = append(mapKeys, key) |
| } |
| sort.Strings(mapKeys) |
| |
| var mapBuf bytes.Buffer |
| mapBuf.WriteString("{") |
| for _, key := range mapKeys { |
| mapBuf.WriteString(fmt.Sprintf("%s:%s ", key, vTyped[key])) |
| } |
| mapBuf.WriteString("}") |
| |
| buf.WriteString(fmt.Sprintf("%s = %s\n", k, mapBuf.String())) |
| default: |
| buf.WriteString(fmt.Sprintf("%s = %#v\n", k, lv)) |
| } |
| } |
| } |
| |
| return buf.String() |
| } |
| |
| // LegacyInstanceObjectID is a helper for extracting an object id value from |
| // an instance object in a way that approximates how we used to do this |
| // for the old state types. ID is no longer first-class, so this is preserved |
| // only for compatibility with old tests that include the id as part of their |
| // expected value. |
| func LegacyInstanceObjectID(obj *ResourceInstanceObjectSrc) string { |
| if obj == nil { |
| return "<not created>" |
| } |
| |
| if obj.AttrsJSON != nil { |
| type WithID struct { |
| ID string `json:"id"` |
| } |
| var withID WithID |
| err := json.Unmarshal(obj.AttrsJSON, &withID) |
| if err == nil { |
| return withID.ID |
| } |
| } else if obj.AttrsFlat != nil { |
| if flatID, exists := obj.AttrsFlat["id"]; exists { |
| return flatID |
| } |
| } |
| |
| // For resource types created after we removed id as special there may |
| // not actually be one at all. This is okay because older tests won't |
| // encounter this, and new tests shouldn't be using ids. |
| return "<none>" |
| } |