github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/states/state_string.go (about) 1 package states 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "sort" 9 "strings" 10 11 ctyjson "github.com/zclconf/go-cty/cty/json" 12 13 "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" 14 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/hcl2shim" 15 ) 16 17 // String returns a rather-odd string representation of the entire state. 18 // 19 // This is intended to match the behavior of the older terraform.State.String 20 // method that is used in lots of existing tests. It should not be used in 21 // new tests: instead, use "cmp" to directly compare the state data structures 22 // and print out a diff if they do not match. 23 // 24 // This method should never be used in non-test code, whether directly by call 25 // or indirectly via a %s or %q verb in package fmt. 26 func (s *State) String() string { 27 if s == nil { 28 return "<nil>" 29 } 30 31 // sort the modules by name for consistent output 32 modules := make([]string, 0, len(s.Modules)) 33 for m := range s.Modules { 34 modules = append(modules, m) 35 } 36 sort.Strings(modules) 37 38 var buf bytes.Buffer 39 for _, name := range modules { 40 m := s.Modules[name] 41 mStr := m.testString() 42 43 // If we're the root module, we just write the output directly. 44 if m.Addr.IsRoot() { 45 buf.WriteString(mStr + "\n") 46 continue 47 } 48 49 // We need to build out a string that resembles the not-quite-standard 50 // format that terraform.State.String used to use, where there's a 51 // "module." prefix but then just a chain of all of the module names 52 // without any further "module." portions. 53 buf.WriteString("module") 54 for _, step := range m.Addr { 55 buf.WriteByte('.') 56 buf.WriteString(step.Name) 57 if step.InstanceKey != addrs.NoKey { 58 buf.WriteByte('[') 59 buf.WriteString(step.InstanceKey.String()) 60 buf.WriteByte(']') 61 } 62 } 63 buf.WriteString(":\n") 64 65 s := bufio.NewScanner(strings.NewReader(mStr)) 66 for s.Scan() { 67 text := s.Text() 68 if text != "" { 69 text = " " + text 70 } 71 72 buf.WriteString(fmt.Sprintf("%s\n", text)) 73 } 74 } 75 76 return strings.TrimSpace(buf.String()) 77 } 78 79 // testString is used to produce part of the output of State.String. It should 80 // never be used directly. 81 func (m *Module) testString() string { 82 var buf bytes.Buffer 83 84 if len(m.Resources) == 0 { 85 buf.WriteString("<no state>") 86 } 87 88 // We use AbsResourceInstance here, even though everything belongs to 89 // the same module, just because we have a sorting behavior defined 90 // for those but not for just ResourceInstance. 91 addrsOrder := make([]addrs.AbsResourceInstance, 0, len(m.Resources)) 92 for _, rs := range m.Resources { 93 for ik := range rs.Instances { 94 addrsOrder = append(addrsOrder, rs.Addr.Instance(ik).Absolute(addrs.RootModuleInstance)) 95 } 96 } 97 98 sort.Slice(addrsOrder, func(i, j int) bool { 99 return addrsOrder[i].Less(addrsOrder[j]) 100 }) 101 102 for _, fakeAbsAddr := range addrsOrder { 103 addr := fakeAbsAddr.Resource 104 rs := m.Resource(addr.ContainingResource()) 105 is := m.ResourceInstance(addr) 106 107 // Here we need to fake up a legacy-style address as the old state 108 // types would've used, since that's what our tests against those 109 // old types expect. The significant difference is that instancekey 110 // is dot-separated rather than using index brackets. 111 k := addr.ContainingResource().String() 112 if addr.Key != addrs.NoKey { 113 switch tk := addr.Key.(type) { 114 case addrs.IntKey: 115 k = fmt.Sprintf("%s.%d", k, tk) 116 default: 117 // No other key types existed for the legacy types, so we 118 // can do whatever we want here. We'll just use our standard 119 // syntax for these. 120 k = k + tk.String() 121 } 122 } 123 124 id := LegacyInstanceObjectID(is.Current) 125 126 taintStr := "" 127 if is.Current != nil && is.Current.Status == ObjectTainted { 128 taintStr = " (tainted)" 129 } 130 131 deposedStr := "" 132 if len(is.Deposed) > 0 { 133 deposedStr = fmt.Sprintf(" (%d deposed)", len(is.Deposed)) 134 } 135 136 buf.WriteString(fmt.Sprintf("%s:%s%s\n", k, taintStr, deposedStr)) 137 buf.WriteString(fmt.Sprintf(" ID = %s\n", id)) 138 buf.WriteString(fmt.Sprintf(" provider = %s\n", rs.ProviderConfig.String())) 139 140 // Attributes were a flatmap before, but are not anymore. To preserve 141 // our old output as closely as possible we need to do a conversion 142 // to flatmap. Normally we'd want to do this with schema for 143 // accuracy, but for our purposes here it only needs to be approximate. 144 // This should produce an identical result for most cases, though 145 // in particular will differ in a few cases: 146 // - The keys used for elements in a set will be different 147 // - Values for attributes of type cty.DynamicPseudoType will be 148 // misinterpreted (but these weren't possible in old world anyway) 149 var attributes map[string]string 150 if obj := is.Current; obj != nil { 151 switch { 152 case obj.AttrsFlat != nil: 153 // Easy (but increasingly unlikely) case: the state hasn't 154 // actually been upgraded to the new form yet. 155 attributes = obj.AttrsFlat 156 case obj.AttrsJSON != nil: 157 ty, err := ctyjson.ImpliedType(obj.AttrsJSON) 158 if err == nil { 159 val, err := ctyjson.Unmarshal(obj.AttrsJSON, ty) 160 if err == nil { 161 attributes = hcl2shim.FlatmapValueFromHCL2(val) 162 } 163 } 164 } 165 } 166 attrKeys := make([]string, 0, len(attributes)) 167 for ak, val := range attributes { 168 if ak == "id" { 169 continue 170 } 171 172 // don't show empty containers in the output 173 if val == "0" && (strings.HasSuffix(ak, ".#") || strings.HasSuffix(ak, ".%")) { 174 continue 175 } 176 177 attrKeys = append(attrKeys, ak) 178 } 179 180 sort.Strings(attrKeys) 181 182 for _, ak := range attrKeys { 183 av := attributes[ak] 184 buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av)) 185 } 186 187 // CAUTION: Since deposed keys are now random strings instead of 188 // incrementing integers, this result will not be deterministic 189 // if there is more than one deposed object. 190 i := 1 191 for _, t := range is.Deposed { 192 id := LegacyInstanceObjectID(t) 193 taintStr := "" 194 if t.Status == ObjectTainted { 195 taintStr = " (tainted)" 196 } 197 buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s%s\n", i, id, taintStr)) 198 i++ 199 } 200 201 if obj := is.Current; obj != nil && len(obj.Dependencies) > 0 { 202 buf.WriteString(fmt.Sprintf("\n Dependencies:\n")) 203 for _, dep := range obj.Dependencies { 204 buf.WriteString(fmt.Sprintf(" %s\n", dep.String())) 205 } 206 } 207 } 208 209 if len(m.OutputValues) > 0 { 210 buf.WriteString("\nOutputs:\n\n") 211 212 ks := make([]string, 0, len(m.OutputValues)) 213 for k := range m.OutputValues { 214 ks = append(ks, k) 215 } 216 sort.Strings(ks) 217 218 for _, k := range ks { 219 v := m.OutputValues[k] 220 lv := hcl2shim.ConfigValueFromHCL2(v.Value) 221 switch vTyped := lv.(type) { 222 case string: 223 buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped)) 224 case []interface{}: 225 buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped)) 226 case map[string]interface{}: 227 var mapKeys []string 228 for key := range vTyped { 229 mapKeys = append(mapKeys, key) 230 } 231 sort.Strings(mapKeys) 232 233 var mapBuf bytes.Buffer 234 mapBuf.WriteString("{") 235 for _, key := range mapKeys { 236 mapBuf.WriteString(fmt.Sprintf("%s:%s ", key, vTyped[key])) 237 } 238 mapBuf.WriteString("}") 239 240 buf.WriteString(fmt.Sprintf("%s = %s\n", k, mapBuf.String())) 241 default: 242 buf.WriteString(fmt.Sprintf("%s = %#v\n", k, lv)) 243 } 244 } 245 } 246 247 return buf.String() 248 } 249 250 // LegacyInstanceObjectID is a helper for extracting an object id value from 251 // an instance object in a way that approximates how we used to do this 252 // for the old state types. ID is no longer first-class, so this is preserved 253 // only for compatibility with old tests that include the id as part of their 254 // expected value. 255 func LegacyInstanceObjectID(obj *ResourceInstanceObjectSrc) string { 256 if obj == nil { 257 return "<not created>" 258 } 259 260 if obj.AttrsJSON != nil { 261 type WithID struct { 262 ID string `json:"id"` 263 } 264 var withID WithID 265 err := json.Unmarshal(obj.AttrsJSON, &withID) 266 if err == nil { 267 return withID.ID 268 } 269 } else if obj.AttrsFlat != nil { 270 if flatID, exists := obj.AttrsFlat["id"]; exists { 271 return flatID 272 } 273 } 274 275 // For resource types created after we removed id as special there may 276 // not actually be one at all. This is okay because older tests won't 277 // encounter this, and new tests shouldn't be using ids. 278 return "<none>" 279 }