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