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