216 lines
6.0 KiB
Go
216 lines
6.0 KiB
Go
package state
|
|
|
|
import (
|
|
"github.com/cloudskiff/driftctl/pkg/iac"
|
|
"github.com/cloudskiff/driftctl/pkg/iac/config"
|
|
"github.com/cloudskiff/driftctl/pkg/iac/terraform/state/backend"
|
|
"github.com/cloudskiff/driftctl/pkg/remote/deserializer"
|
|
"github.com/cloudskiff/driftctl/pkg/resource"
|
|
"github.com/cloudskiff/driftctl/pkg/terraform"
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/states/statefile"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/zclconf/go-cty/cty"
|
|
ctyconvert "github.com/zclconf/go-cty/cty/convert"
|
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
|
)
|
|
|
|
const TerraformStateReaderSupplier = "tfstate"
|
|
|
|
type TerraformStateReader struct {
|
|
config config.SupplierConfig
|
|
backend backend.Backend
|
|
deserializers []deserializer.CTYDeserializer
|
|
}
|
|
|
|
func (r *TerraformStateReader) initReader() error {
|
|
b, err := backend.GetBackend(r.config)
|
|
r.backend = b
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewReader(config config.SupplierConfig) (*TerraformStateReader, error) {
|
|
reader := TerraformStateReader{config: config, deserializers: iac.Deserializers()}
|
|
err := reader.initReader()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &reader, nil
|
|
}
|
|
|
|
func (r *TerraformStateReader) retrieve() (map[string][]cty.Value, error) {
|
|
|
|
state, err := read(r.backend)
|
|
defer r.backend.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stateResources := state.RootModule().Resources
|
|
resMap := make(map[string][]cty.Value)
|
|
for _, stateRes := range stateResources {
|
|
resName := stateRes.Addr.Resource.Name
|
|
resType := stateRes.Addr.Resource.Type
|
|
if stateRes.Addr.Resource.Mode != addrs.ManagedResourceMode {
|
|
logrus.WithFields(logrus.Fields{
|
|
"mode": stateRes.Addr.Resource.Mode,
|
|
"name": resName,
|
|
"type": resType,
|
|
}).Debug("Skipping state entry as it is not a managed resource")
|
|
continue
|
|
}
|
|
providerType := stateRes.ProviderConfig.Provider.Type
|
|
provider := terraform.Provider(providerType)
|
|
if provider == nil {
|
|
logrus.WithFields(logrus.Fields{
|
|
"providerKey": providerType,
|
|
}).Debug("Unsupported provider found in state")
|
|
continue
|
|
}
|
|
schema := provider.Schema()[stateRes.Addr.Resource.Type]
|
|
for _, instance := range stateRes.Instances {
|
|
decodedVal, err := instance.Current.Decode(schema.Block.ImpliedType())
|
|
if err != nil {
|
|
// Try to do a manual type conversion if we got a path error
|
|
// It will allow driftctl to read state generated with a superior version of provider
|
|
// than the actually supported one
|
|
// by ignoring new fields
|
|
_, isPathError := err.(cty.PathError)
|
|
if isPathError {
|
|
logrus.WithFields(logrus.Fields{
|
|
"name": resName,
|
|
"type": resType,
|
|
"err": err.Error(),
|
|
}).Debug("Got a cty path error when deserializing state")
|
|
|
|
decodedVal, err = r.convertInstance(instance.Current, schema.Block.ImpliedType())
|
|
}
|
|
|
|
if err != nil {
|
|
logrus.WithFields(logrus.Fields{
|
|
"name": resName,
|
|
"type": resType,
|
|
}).Error("Unable to decode resource from state")
|
|
return nil, err
|
|
}
|
|
}
|
|
_, exists := resMap[stateRes.Addr.Resource.Type]
|
|
if !exists {
|
|
resMap[stateRes.Addr.Resource.Type] = []cty.Value{
|
|
decodedVal.Value,
|
|
}
|
|
} else {
|
|
resMap[stateRes.Addr.Resource.Type] = append(resMap[stateRes.Addr.Resource.Type], decodedVal.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
return resMap, nil
|
|
}
|
|
|
|
func (r *TerraformStateReader) convertInstance(instance *states.ResourceInstanceObjectSrc, ty cty.Type) (*states.ResourceInstanceObject, error) {
|
|
inputType, err := ctyjson.ImpliedType(instance.AttrsJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
input, err := ctyjson.Unmarshal(instance.AttrsJSON, inputType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
convertedVal, err := ctyconvert.Convert(input, ty)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
instanceObj := &states.ResourceInstanceObject{
|
|
Value: convertedVal,
|
|
Status: instance.Status,
|
|
Dependencies: instance.Dependencies,
|
|
Private: instance.Private,
|
|
CreateBeforeDestroy: instance.CreateBeforeDestroy,
|
|
}
|
|
|
|
logrus.Debug("Successfully converted resource")
|
|
|
|
return instanceObj, nil
|
|
}
|
|
|
|
func (r *TerraformStateReader) decode(values map[string][]cty.Value) ([]resource.Resource, error) {
|
|
results := make([]resource.Resource, 0)
|
|
for _, deserializer := range r.deserializers {
|
|
|
|
typ := deserializer.HandledType().String()
|
|
vals, exists := values[typ]
|
|
if !exists {
|
|
logrus.WithFields(logrus.Fields{
|
|
"path": r.config.Path,
|
|
"backend": r.config.Backend,
|
|
}).Debugf("No resource of type %s found in state", typ)
|
|
continue
|
|
}
|
|
decodedResources, err := deserializer.Deserialize(vals)
|
|
if err != nil {
|
|
logrus.Warnf("Could not read from decoder for %s: %+v", typ, err)
|
|
continue
|
|
}
|
|
for _, res := range decodedResources {
|
|
logrus.WithFields(logrus.Fields{
|
|
"path": r.config.Path,
|
|
"backend": r.config.Backend,
|
|
"id": res.TerraformId(),
|
|
"type": res.TerraformType(),
|
|
}).Debug("Found IAC resource")
|
|
normalisable, ok := res.(resource.NormalizedResource)
|
|
if ok {
|
|
normalizedRes, err := normalisable.NormalizeForState()
|
|
if err != nil {
|
|
logrus.Errorf("Could not normalize state for res %s: %+v", res.TerraformId(), err)
|
|
results = append(results, res)
|
|
}
|
|
|
|
if err == nil {
|
|
results = append(results, normalizedRes)
|
|
}
|
|
}
|
|
if !ok {
|
|
results = append(results, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (r *TerraformStateReader) Resources() ([]resource.Resource, error) {
|
|
logrus.WithFields(logrus.Fields{
|
|
"path": r.config.Path,
|
|
"backend": r.config.Backend,
|
|
}).Debug("Starting state reader supplier")
|
|
values, err := r.retrieve()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r.decode(values)
|
|
}
|
|
|
|
func read(reader backend.Backend) (*states.State, error) {
|
|
state, err := readState(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func readState(reader backend.Backend) (*states.State, error) {
|
|
state, err := statefile.Read(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return state.State, nil
|
|
}
|