2015-02-06 22:31:59 +01:00
package main
import (
2015-05-27 20:10:43 +02:00
"encoding/json"
2019-08-01 17:21:54 +02:00
"fmt"
2015-02-06 22:31:59 +01:00
"io"
"io/ioutil"
2019-08-01 17:21:54 +02:00
"os"
2015-12-15 05:30:23 +01:00
"sort"
2019-08-01 17:21:54 +02:00
"strconv"
2018-09-20 16:37:44 +02:00
"strings"
2015-02-06 22:31:59 +01:00
)
2019-08-01 17:21:54 +02:00
// TerraformVersion defines which version of Terraform state applies
type TerraformVersion int
const (
// TerraformVersionUnknown means unknown version
TerraformVersionUnknown TerraformVersion = 0
// TerraformVersionPre0dot12 means < 0.12
TerraformVersionPre0dot12 TerraformVersion = 1
// TerraformVersion0dot12 means >= 0.12
TerraformVersion0dot12 TerraformVersion = 2
)
type stateAnyTerraformVersion struct {
StatePre0dot12 state
State0dot12 stateTerraform0dot12
TerraformVersion TerraformVersion
}
// Terraform < v0.12
2015-02-06 22:31:59 +01:00
type state struct {
Modules [ ] moduleState ` json:"modules" `
}
2019-08-01 17:21:54 +02:00
type moduleState struct {
Path [ ] string ` json:"path" `
ResourceStates map [ string ] resourceState ` json:"resources" `
Outputs map [ string ] interface { } ` json:"outputs" `
}
type resourceState struct {
// Populated from statefile
Type string ` json:"type" `
Primary instanceState ` json:"primary" `
}
type instanceState struct {
ID string ` json:"id" `
Attributes map [ string ] string ` json:"attributes,omitempty" `
}
// Terraform <= v0.12
type stateTerraform0dot12 struct {
Values valuesStateTerraform0dot12 ` json:"values" `
}
type valuesStateTerraform0dot12 struct {
RootModule * moduleStateTerraform0dot12 ` json:"root_module" `
Outputs map [ string ] interface { } ` json:"outputs" `
}
type moduleStateTerraform0dot12 struct {
ResourceStates [ ] resourceStateTerraform0dot12 ` json:"resources" `
ChildModules [ ] moduleStateTerraform0dot12 ` json:"child_modules" `
Address string ` json:"address" ` // empty for root module, else e.g. `module.mymodulename`
}
type resourceStateTerraform0dot12 struct {
Address string ` json:"address" `
2020-02-05 22:00:44 +01:00
Index * interface { } ` json:"index" ` // only set by Terraform for counted resources
2019-08-01 17:21:54 +02:00
Name string ` json:"name" `
RawValues map [ string ] interface { } ` json:"values" `
Type string ` json:"type" `
}
2015-02-06 22:31:59 +01:00
// read populates the state object from a statefile.
2019-08-01 17:21:54 +02:00
func ( s * stateAnyTerraformVersion ) read ( stateFile io . Reader ) error {
s . TerraformVersion = TerraformVersionUnknown
2015-02-06 22:31:59 +01:00
2019-08-01 17:21:54 +02:00
b , readErr := ioutil . ReadAll ( stateFile )
if readErr != nil {
return readErr
2015-02-06 22:31:59 +01:00
}
2019-08-01 17:21:54 +02:00
err0dot12 := json . Unmarshal ( b , & ( * s ) . State0dot12 )
if err0dot12 == nil && s . State0dot12 . Values . RootModule != nil {
s . TerraformVersion = TerraformVersion0dot12
} else {
errPre0dot12 := json . Unmarshal ( b , & ( * s ) . StatePre0dot12 )
if errPre0dot12 == nil && s . StatePre0dot12 . Modules != nil {
s . TerraformVersion = TerraformVersionPre0dot12
} else {
return fmt . Errorf ( "0.12 format error: %v; pre-0.12 format error: %v (nil error means no content/modules found in the respective format)" , err0dot12 , errPre0dot12 )
}
2015-02-06 22:31:59 +01:00
}
return nil
}
2016-02-16 12:12:49 +01:00
// outputs returns a slice of the Outputs found in the statefile.
func ( s * state ) outputs ( ) [ ] * Output {
inst := make ( [ ] * Output , 0 )
for _ , m := range s . Modules {
for k , v := range m . Outputs {
2016-09-07 05:05:30 +02:00
var o * Output
2016-08-19 16:43:14 +02:00
switch v := v . ( type ) {
case map [ string ] interface { } :
2016-09-07 05:05:30 +02:00
o , _ = NewOutput ( k , v [ "value" ] )
2016-08-19 16:43:14 +02:00
case string :
o , _ = NewOutput ( k , v )
default :
o , _ = NewOutput ( k , "<error>" )
}
2016-02-16 12:12:49 +01:00
inst = append ( inst , o )
}
}
return inst
}
2019-08-01 17:21:54 +02:00
// outputs returns a slice of the Outputs found in the statefile.
func ( s * stateTerraform0dot12 ) outputs ( ) [ ] * Output {
inst := make ( [ ] * Output , 0 )
for k , v := range s . Values . Outputs {
var o * Output
switch v := v . ( type ) {
case map [ string ] interface { } :
o , _ = NewOutput ( k , v [ "value" ] )
default : // not expected
o , _ = NewOutput ( k , "<error>" )
}
inst = append ( inst , o )
}
return inst
}
2018-09-20 16:37:44 +02:00
// map of resource ID -> resource Name
func ( s * state ) mapResourceIDNames ( ) map [ string ] string {
t := map [ string ] string { }
for _ , m := range s . Modules {
for _ , k := range m . resourceKeys ( ) {
if m . ResourceStates [ k ] . Primary . ID != "" && m . ResourceStates [ k ] . Primary . Attributes [ "name" ] != "" {
kk := strings . ToLower ( m . ResourceStates [ k ] . Primary . ID )
t [ kk ] = m . ResourceStates [ k ] . Primary . Attributes [ "name" ]
}
}
}
return t
}
2019-08-01 17:21:54 +02:00
// map of resource ID -> resource Name
func ( s * stateTerraform0dot12 ) mapResourceIDNames ( ) map [ string ] string {
t := map [ string ] string { }
for _ , module := range s . getAllModules ( ) {
for _ , resourceState := range module . ResourceStates {
id , typeOk := resourceState . RawValues [ "id" ] . ( string )
if typeOk && id != "" && resourceState . Name != "" {
k := strings . ToLower ( id )
2019-09-18 04:01:54 +02:00
if val , ok := resourceState . RawValues [ "category_id" ] ; ok && resourceState . Type == "vsphere_tag" {
if categoryID , typeOk := val . ( string ) ; typeOk {
if categoryName := s . getResourceIDName ( categoryID ) ; categoryName != "" {
t [ k ] = fmt . Sprintf ( "%s_%s" , s . getResourceIDName ( categoryID ) , resourceState . Name )
continue
}
}
}
2019-08-01 17:21:54 +02:00
t [ k ] = resourceState . Name
}
}
}
return t
}
2019-09-18 04:01:54 +02:00
func ( s * stateTerraform0dot12 ) getResourceIDName ( matchingID string ) string {
for _ , module := range s . getAllModules ( ) {
for _ , resourceState := range module . ResourceStates {
id , typeOk := resourceState . RawValues [ "id" ] . ( string )
if typeOk && id == matchingID {
return resourceState . Name
}
}
}
return ""
}
2019-08-01 17:21:54 +02:00
func ( s * stateTerraform0dot12 ) getAllModules ( ) [ ] * moduleStateTerraform0dot12 {
var allModules [ ] * moduleStateTerraform0dot12
allModules = append ( allModules , s . Values . RootModule )
addChildModules ( & allModules , s . Values . RootModule )
return allModules
}
// recursively adds all child modules to the slice
func addChildModules ( out * [ ] * moduleStateTerraform0dot12 , from * moduleStateTerraform0dot12 ) {
for i := range from . ChildModules {
addChildModules ( out , & from . ChildModules [ i ] )
* out = append ( * out , & from . ChildModules [ i ] )
}
}
// resources returns a slice of the Resources found in the statefile.
func ( s * stateAnyTerraformVersion ) resources ( ) [ ] * Resource {
switch s . TerraformVersion {
case TerraformVersionPre0dot12 :
return s . StatePre0dot12 . resources ( )
case TerraformVersion0dot12 :
return s . State0dot12 . resources ( )
case TerraformVersionUnknown :
}
panic ( "Unimplemented Terraform version enum" )
}
2015-12-15 04:24:41 +01:00
// resources returns a slice of the Resources found in the statefile.
func ( s * state ) resources ( ) [ ] * Resource {
inst := make ( [ ] * Resource , 0 )
2015-02-06 22:31:59 +01:00
for _ , m := range s . Modules {
2015-12-15 05:30:23 +01:00
for _ , k := range m . resourceKeys ( ) {
2019-08-01 17:21:54 +02:00
if strings . HasPrefix ( k , "data." ) {
// This does not represent a host (e.g. AWS AMI)
continue
}
// If a module is used, the resource key may not be unique, for instance:
//
// The module cannot use dynamic resource naming and thus has to use some hardcoded name:
//
// resource "aws_instance" "host" { ... }
//
// The main file then uses the module twice:
//
// module "application1" { source = "./modules/mymodulename" }
// module "application2" { source = "./modules/mymodulename" }
//
// Avoid key clashes by prepending module name to the key. If path is ["root"], don't
// prepend anything.
//
// In the above example: `aws_instance.host` -> `aws_instance.application1_host`
fullKey := k
resourceNameIndex := strings . Index ( fullKey , "." ) + 1
if len ( m . Path ) > 1 && resourceNameIndex > 0 {
for i := len ( m . Path ) - 1 ; i >= 1 ; i -- {
fullKey = fullKey [ : resourceNameIndex ] + strings . Replace ( m . Path [ i ] , "." , "_" , - 1 ) + "_" + fullKey [ resourceNameIndex : ]
}
}
2015-12-15 04:24:41 +01:00
// Terraform stores resources in a name->map map, but we need the name to
// decide which groups to include the resource in. So wrap it in a higher-
// level object with both properties.
2019-08-01 17:21:54 +02:00
r , err := NewResource ( fullKey , m . ResourceStates [ k ] )
2015-12-15 04:24:41 +01:00
if err != nil {
2019-08-01 17:21:54 +02:00
asJSON , _ := json . Marshal ( m . ResourceStates [ k ] )
fmt . Fprintf ( os . Stderr , "Warning: failed to parse resource %s (%v)\n" , asJSON , err )
2015-12-15 04:24:41 +01:00
continue
}
if r . IsSupported ( ) {
inst = append ( inst , r )
2015-02-06 22:31:59 +01:00
}
}
}
return inst
}
2019-08-01 17:21:54 +02:00
func encodeTerraform0Dot12ValuesAsAttributes ( rawValues * map [ string ] interface { } ) map [ string ] string {
ret := make ( map [ string ] string )
for k , v := range * rawValues {
switch v := v . ( type ) {
case map [ string ] interface { } :
ret [ k + ".#" ] = strconv . Itoa ( len ( v ) )
for kk , vv := range v {
if str , typeOk := vv . ( string ) ; typeOk {
ret [ k + "." + kk ] = str
} else {
ret [ k + "." + kk ] = "<error>"
}
}
2019-09-18 04:01:54 +02:00
case [ ] interface { } :
ret [ k + ".#" ] = strconv . Itoa ( len ( v ) )
for kk , vv := range v {
2020-06-22 17:14:15 +02:00
switch o := vv . ( type ) {
case string :
ret [ k + "." + strconv . Itoa ( kk ) ] = o
case map [ string ] interface { } :
for kkk , vvv := range o {
if str , typeOk := vvv . ( string ) ; typeOk {
ret [ k + "." + strconv . Itoa ( kk ) + "." + kkk ] = str
} else {
ret [ k + "." + strconv . Itoa ( kk ) + "." + kkk ] = "<error>"
}
}
default :
2019-09-18 04:01:54 +02:00
ret [ k + "." + strconv . Itoa ( kk ) ] = "<error>"
}
}
2019-08-01 17:21:54 +02:00
case string :
ret [ k ] = v
default :
ret [ k ] = "<error>"
}
}
return ret
}
// resources returns a slice of the Resources found in the statefile.
func ( s * stateTerraform0dot12 ) resources ( ) [ ] * Resource {
inst := make ( [ ] * Resource , 0 )
for _ , module := range s . getAllModules ( ) {
for _ , rs := range module . ResourceStates {
id , typeOk := rs . RawValues [ "id" ] . ( string )
if ! typeOk {
continue
}
if strings . HasPrefix ( rs . Address , "data." ) {
// This does not represent a host (e.g. AWS AMI)
continue
}
modulePrefix := ""
if module . Address != "" {
modulePrefix = strings . Replace ( module . Address , "." , "_" , - 1 ) + "_"
}
resourceKeyName := rs . Type + "." + modulePrefix + rs . Name
if rs . Index != nil {
2020-02-05 22:00:44 +01:00
i := * rs . Index
switch v := i . ( type ) {
case int :
resourceKeyName += "." + strconv . Itoa ( v )
case float64 :
resourceKeyName += "." + strconv . Itoa ( int ( v ) )
case string :
2021-07-30 23:06:38 +02:00
resourceKeyName += "." + strings . Replace ( v , "." , "_" , - 1 )
2020-02-05 22:00:44 +01:00
default :
fmt . Fprintf ( os . Stderr , "Warning: unknown index type %v\n" , v )
}
2019-08-01 17:21:54 +02:00
}
// Terraform stores resources in a name->map map, but we need the name to
// decide which groups to include the resource in. So wrap it in a higher-
// level object with both properties.
//
// Convert to the pre-0.12 structure for backwards compatibility of code.
r , err := NewResource ( resourceKeyName , resourceState {
Type : rs . Type ,
Primary : instanceState {
ID : id ,
Attributes : encodeTerraform0Dot12ValuesAsAttributes ( & rs . RawValues ) ,
} ,
} )
if err != nil {
asJSON , _ := json . Marshal ( rs )
fmt . Fprintf ( os . Stderr , "Warning: failed to parse resource %s (%v)\n" , asJSON , err )
continue
}
if r . IsSupported ( ) {
inst = append ( inst , r )
}
}
}
sort . Slice ( inst , func ( i , j int ) bool {
return inst [ i ] . baseName < inst [ j ] . baseName
} )
return inst
2015-02-06 22:31:59 +01:00
}
2015-12-15 05:30:23 +01:00
// resourceKeys returns a sorted slice of the key names of the resources in this
// module. Do this instead of range over ResourceStates, to ensure that the
// output is consistent.
func ( ms * moduleState ) resourceKeys ( ) [ ] string {
lk := len ( ms . ResourceStates )
keys := make ( [ ] string , lk , lk )
i := 0
for k := range ms . ResourceStates {
keys [ i ] = k
2019-08-01 17:21:54 +02:00
i ++
2015-12-15 05:30:23 +01:00
}
sort . Strings ( keys )
return keys
}