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 }