github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/jsonstate/state.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package jsonstate 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "sort" 10 11 "github.com/zclconf/go-cty/cty" 12 ctyjson "github.com/zclconf/go-cty/cty/json" 13 14 "github.com/terramate-io/tf/addrs" 15 "github.com/terramate-io/tf/command/jsonchecks" 16 "github.com/terramate-io/tf/lang/marks" 17 "github.com/terramate-io/tf/states" 18 "github.com/terramate-io/tf/states/statefile" 19 "github.com/terramate-io/tf/terraform" 20 ) 21 22 const ( 23 // FormatVersion represents the version of the json format and will be 24 // incremented for any change to this format that requires changes to a 25 // consuming parser. 26 FormatVersion = "1.0" 27 28 ManagedResourceMode = "managed" 29 DataResourceMode = "data" 30 ) 31 32 // State is the top-level representation of the json format of a terraform 33 // state. 34 type State struct { 35 FormatVersion string `json:"format_version,omitempty"` 36 TerraformVersion string `json:"terraform_version,omitempty"` 37 Values *StateValues `json:"values,omitempty"` 38 Checks json.RawMessage `json:"checks,omitempty"` 39 } 40 41 // StateValues is the common representation of resolved values for both the prior 42 // state (which is always complete) and the planned new state. 43 type StateValues struct { 44 Outputs map[string]Output `json:"outputs,omitempty"` 45 RootModule Module `json:"root_module,omitempty"` 46 } 47 48 type Output struct { 49 Sensitive bool `json:"sensitive"` 50 Value json.RawMessage `json:"value,omitempty"` 51 Type json.RawMessage `json:"type,omitempty"` 52 } 53 54 // Module is the representation of a module in state. This can be the root module 55 // or a child module 56 type Module struct { 57 // Resources are sorted in a user-friendly order that is undefined at this 58 // time, but consistent. 59 Resources []Resource `json:"resources,omitempty"` 60 61 // Address is the absolute module address, omitted for the root module 62 Address string `json:"address,omitempty"` 63 64 // Each module object can optionally have its own nested "child_modules", 65 // recursively describing the full module tree. 66 ChildModules []Module `json:"child_modules,omitempty"` 67 } 68 69 // Resource is the representation of a resource in the state. 70 type Resource struct { 71 // Address is the absolute resource address 72 Address string `json:"address,omitempty"` 73 74 // Mode can be "managed" or "data" 75 Mode string `json:"mode,omitempty"` 76 77 Type string `json:"type,omitempty"` 78 Name string `json:"name,omitempty"` 79 80 // Index is omitted for a resource not using `count` or `for_each`. 81 Index json.RawMessage `json:"index,omitempty"` 82 83 // ProviderName allows the property "type" to be interpreted unambiguously 84 // in the unusual situation where a provider offers a resource type whose 85 // name does not start with its own name, such as the "googlebeta" provider 86 // offering "google_compute_instance". 87 ProviderName string `json:"provider_name"` 88 89 // SchemaVersion indicates which version of the resource type schema the 90 // "values" property conforms to. 91 SchemaVersion uint64 `json:"schema_version"` 92 93 // AttributeValues is the JSON representation of the attribute values of the 94 // resource, whose structure depends on the resource type schema. Any 95 // unknown values are omitted or set to null, making them indistinguishable 96 // from absent values. 97 AttributeValues AttributeValues `json:"values,omitempty"` 98 99 // SensitiveValues is similar to AttributeValues, but with all sensitive 100 // values replaced with true, and all non-sensitive leaf values omitted. 101 SensitiveValues json.RawMessage `json:"sensitive_values,omitempty"` 102 103 // DependsOn contains a list of the resource's dependencies. The entries are 104 // addresses relative to the containing module. 105 DependsOn []string `json:"depends_on,omitempty"` 106 107 // Tainted is true if the resource is tainted in terraform state. 108 Tainted bool `json:"tainted,omitempty"` 109 110 // Deposed is set if the resource is deposed in terraform state. 111 DeposedKey string `json:"deposed_key,omitempty"` 112 } 113 114 // AttributeValues is the JSON representation of the attribute values of the 115 // resource, whose structure depends on the resource type schema. 116 type AttributeValues map[string]json.RawMessage 117 118 func marshalAttributeValues(value cty.Value) AttributeValues { 119 // unmark our value to show all values 120 value, _ = value.UnmarkDeep() 121 122 if value == cty.NilVal || value.IsNull() { 123 return nil 124 } 125 126 ret := make(AttributeValues) 127 128 it := value.ElementIterator() 129 for it.Next() { 130 k, v := it.Element() 131 vJSON, _ := ctyjson.Marshal(v, v.Type()) 132 ret[k.AsString()] = json.RawMessage(vJSON) 133 } 134 return ret 135 } 136 137 // newState() returns a minimally-initialized state 138 func newState() *State { 139 return &State{ 140 FormatVersion: FormatVersion, 141 } 142 } 143 144 // MarshalForRenderer returns the pre-json encoding changes of the state, in a 145 // format available to the structured renderer. 146 func MarshalForRenderer(sf *statefile.File, schemas *terraform.Schemas) (Module, map[string]Output, error) { 147 if sf.State.Modules == nil { 148 // Empty state case. 149 return Module{}, nil, nil 150 } 151 152 outputs, err := MarshalOutputs(sf.State.RootModule().OutputValues) 153 if err != nil { 154 return Module{}, nil, err 155 } 156 157 root, err := marshalRootModule(sf.State, schemas) 158 if err != nil { 159 return Module{}, nil, err 160 } 161 162 return root, outputs, err 163 } 164 165 // MarshalForLog returns the origin JSON compatible state, read for a logging 166 // package to marshal further. 167 func MarshalForLog(sf *statefile.File, schemas *terraform.Schemas) (*State, error) { 168 output := newState() 169 170 if sf == nil || sf.State.Empty() { 171 return output, nil 172 } 173 174 if sf.TerraformVersion != nil { 175 output.TerraformVersion = sf.TerraformVersion.String() 176 } 177 178 // output.StateValues 179 err := output.marshalStateValues(sf.State, schemas) 180 if err != nil { 181 return nil, err 182 } 183 184 // output.Checks 185 if sf.State.CheckResults != nil && sf.State.CheckResults.ConfigResults.Len() > 0 { 186 output.Checks = jsonchecks.MarshalCheckStates(sf.State.CheckResults) 187 } 188 189 return output, nil 190 } 191 192 // Marshal returns the json encoding of a terraform state. 193 func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) { 194 output, err := MarshalForLog(sf, schemas) 195 if err != nil { 196 return nil, err 197 } 198 199 ret, err := json.Marshal(output) 200 return ret, err 201 } 202 203 func (jsonstate *State) marshalStateValues(s *states.State, schemas *terraform.Schemas) error { 204 var sv StateValues 205 var err error 206 207 // only marshal the root module outputs 208 sv.Outputs, err = MarshalOutputs(s.RootModule().OutputValues) 209 if err != nil { 210 return err 211 } 212 213 // use the state and module map to build up the module structure 214 sv.RootModule, err = marshalRootModule(s, schemas) 215 if err != nil { 216 return err 217 } 218 219 jsonstate.Values = &sv 220 return nil 221 } 222 223 // MarshalOutputs translates a map of states.OutputValue to a map of jsonstate.Output, 224 // which are defined for json encoding. 225 func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]Output, error) { 226 if outputs == nil { 227 return nil, nil 228 } 229 230 ret := make(map[string]Output) 231 for k, v := range outputs { 232 ty := v.Value.Type() 233 ov, err := ctyjson.Marshal(v.Value, ty) 234 if err != nil { 235 return ret, err 236 } 237 ot, err := ctyjson.MarshalType(ty) 238 if err != nil { 239 return ret, err 240 } 241 ret[k] = Output{ 242 Value: ov, 243 Type: ot, 244 Sensitive: v.Sensitive, 245 } 246 } 247 248 return ret, nil 249 } 250 251 func marshalRootModule(s *states.State, schemas *terraform.Schemas) (Module, error) { 252 var ret Module 253 var err error 254 255 ret.Address = "" 256 rs, err := marshalResources(s.RootModule().Resources, addrs.RootModuleInstance, schemas) 257 if err != nil { 258 return ret, err 259 } 260 ret.Resources = rs 261 262 // build a map of module -> set[child module addresses] 263 moduleChildSet := make(map[string]map[string]struct{}) 264 for _, mod := range s.Modules { 265 if mod.Addr.IsRoot() { 266 continue 267 } else { 268 for childAddr := mod.Addr; !childAddr.IsRoot(); childAddr = childAddr.Parent() { 269 if _, ok := moduleChildSet[childAddr.Parent().String()]; !ok { 270 moduleChildSet[childAddr.Parent().String()] = map[string]struct{}{} 271 } 272 moduleChildSet[childAddr.Parent().String()][childAddr.String()] = struct{}{} 273 } 274 } 275 } 276 277 // transform the previous map into map of module -> [child module addresses] 278 moduleMap := make(map[string][]addrs.ModuleInstance) 279 for parent, children := range moduleChildSet { 280 for child := range children { 281 childModuleInstance, diags := addrs.ParseModuleInstanceStr(child) 282 if diags.HasErrors() { 283 return ret, diags.Err() 284 } 285 moduleMap[parent] = append(moduleMap[parent], childModuleInstance) 286 } 287 } 288 289 // use the state and module map to build up the module structure 290 ret.ChildModules, err = marshalModules(s, schemas, moduleMap[""], moduleMap) 291 return ret, err 292 } 293 294 // marshalModules is an ungainly recursive function to build a module structure 295 // out of terraform state. 296 func marshalModules( 297 s *states.State, 298 schemas *terraform.Schemas, 299 modules []addrs.ModuleInstance, 300 moduleMap map[string][]addrs.ModuleInstance, 301 ) ([]Module, error) { 302 var ret []Module 303 for _, child := range modules { 304 // cm for child module, naming things is hard. 305 cm := Module{Address: child.String()} 306 307 // the module may be resourceless and contain only submodules, it will then be nil here 308 stateMod := s.Module(child) 309 if stateMod != nil { 310 rs, err := marshalResources(stateMod.Resources, stateMod.Addr, schemas) 311 if err != nil { 312 return nil, err 313 } 314 cm.Resources = rs 315 } 316 317 if moduleMap[child.String()] != nil { 318 moreChildModules, err := marshalModules(s, schemas, moduleMap[child.String()], moduleMap) 319 if err != nil { 320 return nil, err 321 } 322 cm.ChildModules = moreChildModules 323 } 324 325 ret = append(ret, cm) 326 } 327 328 // sort the child modules by address for consistency. 329 sort.Slice(ret, func(i, j int) bool { 330 return ret[i].Address < ret[j].Address 331 }) 332 333 return ret, nil 334 } 335 336 func marshalResources(resources map[string]*states.Resource, module addrs.ModuleInstance, schemas *terraform.Schemas) ([]Resource, error) { 337 var ret []Resource 338 339 var sortedResources []*states.Resource 340 for _, r := range resources { 341 sortedResources = append(sortedResources, r) 342 } 343 sort.Slice(sortedResources, func(i, j int) bool { 344 return sortedResources[i].Addr.Less(sortedResources[j].Addr) 345 }) 346 347 for _, r := range sortedResources { 348 349 var sortedKeys []addrs.InstanceKey 350 for k := range r.Instances { 351 sortedKeys = append(sortedKeys, k) 352 } 353 sort.Slice(sortedKeys, func(i, j int) bool { 354 return addrs.InstanceKeyLess(sortedKeys[i], sortedKeys[j]) 355 }) 356 357 for _, k := range sortedKeys { 358 ri := r.Instances[k] 359 360 var err error 361 362 resAddr := r.Addr.Resource 363 364 current := Resource{ 365 Address: r.Addr.Instance(k).String(), 366 Type: resAddr.Type, 367 Name: resAddr.Name, 368 ProviderName: r.ProviderConfig.Provider.String(), 369 } 370 371 if k != nil { 372 index := k.Value() 373 if current.Index, err = ctyjson.Marshal(index, index.Type()); err != nil { 374 return nil, err 375 } 376 } 377 378 switch resAddr.Mode { 379 case addrs.ManagedResourceMode: 380 current.Mode = ManagedResourceMode 381 case addrs.DataResourceMode: 382 current.Mode = DataResourceMode 383 default: 384 return ret, fmt.Errorf("resource %s has an unsupported mode %s", 385 resAddr.String(), 386 resAddr.Mode.String(), 387 ) 388 } 389 390 schema, version := schemas.ResourceTypeConfig( 391 r.ProviderConfig.Provider, 392 resAddr.Mode, 393 resAddr.Type, 394 ) 395 396 // It is possible that the only instance is deposed 397 if ri.Current != nil { 398 if version != ri.Current.SchemaVersion { 399 return nil, fmt.Errorf("schema version %d for %s in state does not match version %d from the provider", ri.Current.SchemaVersion, resAddr, version) 400 } 401 402 current.SchemaVersion = ri.Current.SchemaVersion 403 404 if schema == nil { 405 return nil, fmt.Errorf("no schema found for %s (in provider %s)", resAddr.String(), r.ProviderConfig.Provider) 406 } 407 riObj, err := ri.Current.Decode(schema.ImpliedType()) 408 if err != nil { 409 return nil, err 410 } 411 412 current.AttributeValues = marshalAttributeValues(riObj.Value) 413 414 value, marks := riObj.Value.UnmarkDeepWithPaths() 415 if schema.ContainsSensitive() { 416 marks = append(marks, schema.ValueMarks(value, nil)...) 417 } 418 s := SensitiveAsBool(value.MarkWithPaths(marks)) 419 v, err := ctyjson.Marshal(s, s.Type()) 420 if err != nil { 421 return nil, err 422 } 423 current.SensitiveValues = v 424 425 if len(riObj.Dependencies) > 0 { 426 dependencies := make([]string, len(riObj.Dependencies)) 427 for i, v := range riObj.Dependencies { 428 dependencies[i] = v.String() 429 } 430 current.DependsOn = dependencies 431 } 432 433 if riObj.Status == states.ObjectTainted { 434 current.Tainted = true 435 } 436 ret = append(ret, current) 437 } 438 439 var sortedDeposedKeys []string 440 for k := range ri.Deposed { 441 sortedDeposedKeys = append(sortedDeposedKeys, string(k)) 442 } 443 sort.Strings(sortedDeposedKeys) 444 445 for _, deposedKey := range sortedDeposedKeys { 446 rios := ri.Deposed[states.DeposedKey(deposedKey)] 447 448 // copy the base fields from the current instance 449 deposed := Resource{ 450 Address: current.Address, 451 Type: current.Type, 452 Name: current.Name, 453 ProviderName: current.ProviderName, 454 Mode: current.Mode, 455 Index: current.Index, 456 } 457 458 riObj, err := rios.Decode(schema.ImpliedType()) 459 if err != nil { 460 return nil, err 461 } 462 463 deposed.AttributeValues = marshalAttributeValues(riObj.Value) 464 465 value, marks := riObj.Value.UnmarkDeepWithPaths() 466 if schema.ContainsSensitive() { 467 marks = append(marks, schema.ValueMarks(value, nil)...) 468 } 469 s := SensitiveAsBool(value.MarkWithPaths(marks)) 470 v, err := ctyjson.Marshal(s, s.Type()) 471 if err != nil { 472 return nil, err 473 } 474 deposed.SensitiveValues = v 475 476 if len(riObj.Dependencies) > 0 { 477 dependencies := make([]string, len(riObj.Dependencies)) 478 for i, v := range riObj.Dependencies { 479 dependencies[i] = v.String() 480 } 481 deposed.DependsOn = dependencies 482 } 483 484 if riObj.Status == states.ObjectTainted { 485 deposed.Tainted = true 486 } 487 deposed.DeposedKey = deposedKey 488 ret = append(ret, deposed) 489 } 490 } 491 } 492 493 return ret, nil 494 } 495 496 func SensitiveAsBool(val cty.Value) cty.Value { 497 if val.HasMark(marks.Sensitive) { 498 return cty.True 499 } 500 501 ty := val.Type() 502 switch { 503 case val.IsNull(), ty.IsPrimitiveType(), ty.Equals(cty.DynamicPseudoType): 504 return cty.False 505 case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): 506 if !val.IsKnown() { 507 // If the collection is unknown we can't say anything about the 508 // sensitivity of its contents 509 return cty.EmptyTupleVal 510 } 511 length := val.LengthInt() 512 if length == 0 { 513 // If there are no elements then we can't have sensitive values 514 return cty.EmptyTupleVal 515 } 516 vals := make([]cty.Value, 0, length) 517 it := val.ElementIterator() 518 for it.Next() { 519 _, v := it.Element() 520 vals = append(vals, SensitiveAsBool(v)) 521 } 522 // The above transform may have changed the types of some of the 523 // elements, so we'll always use a tuple here in case we've now made 524 // different elements have different types. Our ultimate goal is to 525 // marshal to JSON anyway, and all of these sequence types are 526 // indistinguishable in JSON. 527 return cty.TupleVal(vals) 528 case ty.IsMapType() || ty.IsObjectType(): 529 if !val.IsKnown() { 530 // If the map/object is unknown we can't say anything about the 531 // sensitivity of its attributes 532 return cty.EmptyObjectVal 533 } 534 var length int 535 switch { 536 case ty.IsMapType(): 537 length = val.LengthInt() 538 default: 539 length = len(val.Type().AttributeTypes()) 540 } 541 if length == 0 { 542 // If there are no elements then we can't have sensitive values 543 return cty.EmptyObjectVal 544 } 545 vals := make(map[string]cty.Value) 546 it := val.ElementIterator() 547 for it.Next() { 548 k, v := it.Element() 549 s := SensitiveAsBool(v) 550 // Omit all of the "false"s for non-sensitive values for more 551 // compact serialization 552 if !s.RawEquals(cty.False) { 553 vals[k.AsString()] = s 554 } 555 } 556 // The above transform may have changed the types of some of the 557 // elements, so we'll always use an object here in case we've now made 558 // different elements have different types. Our ultimate goal is to 559 // marshal to JSON anyway, and all of these mapping types are 560 // indistinguishable in JSON. 561 return cty.ObjectVal(vals) 562 default: 563 // Should never happen, since the above should cover all types 564 panic(fmt.Sprintf("sensitiveAsBool cannot handle %#v", val)) 565 } 566 }