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