github.com/opentofu/opentofu@v1.7.1/internal/command/jsonplan/values.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package jsonplan
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"sort"
    12  
    13  	"github.com/zclconf/go-cty/cty"
    14  	ctyjson "github.com/zclconf/go-cty/cty/json"
    15  
    16  	"github.com/opentofu/opentofu/internal/addrs"
    17  	"github.com/opentofu/opentofu/internal/command/jsonstate"
    18  	"github.com/opentofu/opentofu/internal/configs/configschema"
    19  	"github.com/opentofu/opentofu/internal/plans"
    20  	"github.com/opentofu/opentofu/internal/states"
    21  	"github.com/opentofu/opentofu/internal/tofu"
    22  )
    23  
    24  // StateValues is the common representation of resolved values for both the
    25  // prior state (which is always complete) and the planned new state.
    26  type StateValues struct {
    27  	Outputs    map[string]Output `json:"outputs,omitempty"`
    28  	RootModule Module            `json:"root_module,omitempty"`
    29  }
    30  
    31  // AttributeValues is the JSON representation of the attribute values of the
    32  // resource, whose structure depends on the resource type schema.
    33  type AttributeValues map[string]interface{}
    34  
    35  func marshalAttributeValues(value cty.Value, schema *configschema.Block) AttributeValues {
    36  	if value == cty.NilVal || value.IsNull() {
    37  		return nil
    38  	}
    39  	ret := make(AttributeValues)
    40  
    41  	it := value.ElementIterator()
    42  	for it.Next() {
    43  		k, v := it.Element()
    44  		vJSON, _ := ctyjson.Marshal(v, v.Type())
    45  		ret[k.AsString()] = json.RawMessage(vJSON)
    46  	}
    47  	return ret
    48  }
    49  
    50  // marshalPlannedOutputs takes a list of changes and returns a map of output
    51  // values
    52  func marshalPlannedOutputs(changes *plans.Changes) (map[string]Output, error) {
    53  	if changes.Outputs == nil {
    54  		// No changes - we're done here!
    55  		return nil, nil
    56  	}
    57  
    58  	ret := make(map[string]Output)
    59  
    60  	for _, oc := range changes.Outputs {
    61  		if oc.ChangeSrc.Action == plans.Delete {
    62  			continue
    63  		}
    64  
    65  		var after, afterType []byte
    66  		changeV, err := oc.Decode()
    67  		if err != nil {
    68  			return ret, err
    69  		}
    70  		// The values may be marked, but we must rely on the Sensitive flag
    71  		// as the decoded value is only an intermediate step in transcoding
    72  		// this to a json format.
    73  		changeV.After, _ = changeV.After.UnmarkDeep()
    74  
    75  		if changeV.After != cty.NilVal && changeV.After.IsWhollyKnown() {
    76  			ty := changeV.After.Type()
    77  			after, err = ctyjson.Marshal(changeV.After, ty)
    78  			if err != nil {
    79  				return ret, err
    80  			}
    81  			afterType, err = ctyjson.MarshalType(ty)
    82  			if err != nil {
    83  				return ret, err
    84  			}
    85  		}
    86  
    87  		ret[oc.Addr.OutputValue.Name] = Output{
    88  			Value:     json.RawMessage(after),
    89  			Type:      json.RawMessage(afterType),
    90  			Sensitive: oc.Sensitive,
    91  		}
    92  	}
    93  
    94  	return ret, nil
    95  
    96  }
    97  
    98  func marshalPlannedValues(changes *plans.Changes, schemas *tofu.Schemas) (Module, error) {
    99  	var ret Module
   100  
   101  	// build two maps:
   102  	// 		module name -> [resource addresses]
   103  	// 		module -> [children modules]
   104  	moduleResourceMap := make(map[string][]addrs.AbsResourceInstance)
   105  	moduleMap := make(map[string][]addrs.ModuleInstance)
   106  	seenModules := make(map[string]bool)
   107  
   108  	for _, resource := range changes.Resources {
   109  		// If the resource is being deleted, skip over it.
   110  		// Deposed instances are always conceptually a destroy, but if they
   111  		// were gone during refresh then the change becomes a noop.
   112  		if resource.Action != plans.Delete && resource.DeposedKey == states.NotDeposed {
   113  			containingModule := resource.Addr.Module.String()
   114  			moduleResourceMap[containingModule] = append(moduleResourceMap[containingModule], resource.Addr)
   115  
   116  			// the root module has no parents
   117  			if !resource.Addr.Module.IsRoot() {
   118  				parent := resource.Addr.Module.Parent().String()
   119  				// we expect to see multiple resources in one module, so we
   120  				// only need to report the "parent" module for each child module
   121  				// once.
   122  				if !seenModules[containingModule] {
   123  					moduleMap[parent] = append(moduleMap[parent], resource.Addr.Module)
   124  					seenModules[containingModule] = true
   125  				}
   126  
   127  				// If any given parent module has no resources, it needs to be
   128  				// added to the moduleMap. This walks through the current
   129  				// resources' modules' ancestors, taking advantage of the fact
   130  				// that Ancestors() returns an ordered slice, and verifies that
   131  				// each one is in the map.
   132  				ancestors := resource.Addr.Module.Ancestors()
   133  				for i, ancestor := range ancestors[:len(ancestors)-1] {
   134  					aStr := ancestor.String()
   135  
   136  					// childStr here is the immediate child of the current step
   137  					childStr := ancestors[i+1].String()
   138  					// we likely will see multiple resources in one module, so we
   139  					// only need to report the "parent" module for each child module
   140  					// once.
   141  					if !seenModules[childStr] {
   142  						moduleMap[aStr] = append(moduleMap[aStr], ancestors[i+1])
   143  						seenModules[childStr] = true
   144  					}
   145  				}
   146  			}
   147  		}
   148  	}
   149  
   150  	// start with the root module
   151  	resources, err := marshalPlanResources(changes, moduleResourceMap[""], schemas)
   152  	if err != nil {
   153  		return ret, err
   154  	}
   155  	ret.Resources = resources
   156  
   157  	childModules, err := marshalPlanModules(changes, schemas, moduleMap[""], moduleMap, moduleResourceMap)
   158  	if err != nil {
   159  		return ret, err
   160  	}
   161  	sort.Slice(childModules, func(i, j int) bool {
   162  		return childModules[i].Address < childModules[j].Address
   163  	})
   164  
   165  	ret.ChildModules = childModules
   166  
   167  	return ret, nil
   168  }
   169  
   170  // marshalPlanResources
   171  func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstance, schemas *tofu.Schemas) ([]Resource, error) {
   172  	var ret []Resource
   173  
   174  	for _, ri := range ris {
   175  		r := changes.ResourceInstance(ri)
   176  		if r.Action == plans.Delete {
   177  			continue
   178  		}
   179  
   180  		resource := Resource{
   181  			Address:      r.Addr.String(),
   182  			Type:         r.Addr.Resource.Resource.Type,
   183  			Name:         r.Addr.Resource.Resource.Name,
   184  			ProviderName: r.ProviderAddr.Provider.String(),
   185  			Index:        r.Addr.Resource.Key,
   186  		}
   187  
   188  		switch r.Addr.Resource.Resource.Mode {
   189  		case addrs.ManagedResourceMode:
   190  			resource.Mode = "managed"
   191  		case addrs.DataResourceMode:
   192  			resource.Mode = "data"
   193  		default:
   194  			return nil, fmt.Errorf("resource %s has an unsupported mode %s",
   195  				r.Addr.String(),
   196  				r.Addr.Resource.Resource.Mode.String(),
   197  			)
   198  		}
   199  
   200  		schema, schemaVer := schemas.ResourceTypeConfig(
   201  			r.ProviderAddr.Provider,
   202  			r.Addr.Resource.Resource.Mode,
   203  			resource.Type,
   204  		)
   205  		if schema == nil {
   206  			return nil, fmt.Errorf("no schema found for %s", r.Addr.String())
   207  		}
   208  		resource.SchemaVersion = schemaVer
   209  		changeV, err := r.Decode(schema.ImpliedType())
   210  		if err != nil {
   211  			return nil, err
   212  		}
   213  
   214  		// copy the marked After values so we can use these in marshalSensitiveValues
   215  		markedAfter := changeV.After
   216  
   217  		// The values may be marked, but we must rely on the Sensitive flag
   218  		// as the decoded value is only an intermediate step in transcoding
   219  		// this to a json format.
   220  		changeV.Before, _ = changeV.Before.UnmarkDeep()
   221  		changeV.After, _ = changeV.After.UnmarkDeep()
   222  
   223  		if changeV.After != cty.NilVal {
   224  			if changeV.After.IsWhollyKnown() {
   225  				resource.AttributeValues = marshalAttributeValues(changeV.After, schema)
   226  			} else {
   227  				knowns := omitUnknowns(changeV.After)
   228  				resource.AttributeValues = marshalAttributeValues(knowns, schema)
   229  			}
   230  		}
   231  
   232  		s := jsonstate.SensitiveAsBool(markedAfter)
   233  		v, err := ctyjson.Marshal(s, s.Type())
   234  		if err != nil {
   235  			return nil, err
   236  		}
   237  		resource.SensitiveValues = v
   238  
   239  		ret = append(ret, resource)
   240  	}
   241  
   242  	sort.Slice(ret, func(i, j int) bool {
   243  		return ret[i].Address < ret[j].Address
   244  	})
   245  
   246  	return ret, nil
   247  }
   248  
   249  // marshalPlanModules iterates over a list of modules to recursively describe
   250  // the full module tree.
   251  func marshalPlanModules(
   252  	changes *plans.Changes,
   253  	schemas *tofu.Schemas,
   254  	childModules []addrs.ModuleInstance,
   255  	moduleMap map[string][]addrs.ModuleInstance,
   256  	moduleResourceMap map[string][]addrs.AbsResourceInstance,
   257  ) ([]Module, error) {
   258  
   259  	var ret []Module
   260  
   261  	for _, child := range childModules {
   262  		moduleResources := moduleResourceMap[child.String()]
   263  		// cm for child module, naming things is hard.
   264  		var cm Module
   265  		// don't populate the address for the root module
   266  		if child.String() != "" {
   267  			cm.Address = child.String()
   268  		}
   269  		rs, err := marshalPlanResources(changes, moduleResources, schemas)
   270  		if err != nil {
   271  			return nil, err
   272  		}
   273  		cm.Resources = rs
   274  
   275  		if len(moduleMap[child.String()]) > 0 {
   276  			moreChildModules, err := marshalPlanModules(changes, schemas, moduleMap[child.String()], moduleMap, moduleResourceMap)
   277  			if err != nil {
   278  				return nil, err
   279  			}
   280  			cm.ChildModules = moreChildModules
   281  		}
   282  
   283  		ret = append(ret, cm)
   284  	}
   285  
   286  	return ret, nil
   287  }