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  }