297 lines
8.4 KiB
Go
297 lines
8.4 KiB
Go
package state
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/states/statefile"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/snyk/driftctl/pkg/alerter"
|
|
"github.com/snyk/driftctl/pkg/filter"
|
|
"github.com/snyk/driftctl/pkg/iac"
|
|
"github.com/snyk/driftctl/pkg/output"
|
|
"github.com/zclconf/go-cty/cty"
|
|
ctyconvert "github.com/zclconf/go-cty/cty/convert"
|
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
|
|
|
"github.com/snyk/driftctl/pkg/iac/config"
|
|
"github.com/snyk/driftctl/pkg/iac/terraform/state/backend"
|
|
"github.com/snyk/driftctl/pkg/iac/terraform/state/enumerator"
|
|
"github.com/snyk/driftctl/pkg/resource"
|
|
"github.com/snyk/driftctl/pkg/terraform"
|
|
)
|
|
|
|
const TerraformStateReaderSupplier = "tfstate"
|
|
|
|
type decodedRes struct {
|
|
source resource.Source
|
|
val cty.Value
|
|
}
|
|
|
|
type TerraformStateReader struct {
|
|
library *terraform.ProviderLibrary
|
|
config config.SupplierConfig
|
|
backend backend.Backend
|
|
enumerator enumerator.StateEnumerator
|
|
deserializer *resource.Deserializer
|
|
backendOptions *backend.Options
|
|
progress output.Progress
|
|
filter filter.Filter
|
|
alerter *alerter.Alerter
|
|
}
|
|
|
|
func (r *TerraformStateReader) initReader() error {
|
|
r.enumerator = enumerator.GetEnumerator(r.config)
|
|
return nil
|
|
}
|
|
|
|
func NewReader(config config.SupplierConfig, library *terraform.ProviderLibrary, backendOpts *backend.Options, progress output.Progress, alerter *alerter.Alerter, deserializer *resource.Deserializer, filter filter.Filter) (*TerraformStateReader, error) {
|
|
reader := TerraformStateReader{library: library, config: config, deserializer: deserializer, backendOptions: backendOpts, progress: progress, alerter: alerter, filter: filter}
|
|
err := reader.initReader()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &reader, nil
|
|
}
|
|
|
|
func (r *TerraformStateReader) retrieve() (map[string][]decodedRes, error) {
|
|
b, err := backend.GetBackend(r.config, r.backendOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.backend = b
|
|
|
|
state, err := read(r.config.Path, r.backend)
|
|
defer r.backend.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resMap := make(map[string][]decodedRes)
|
|
for moduleName, module := range state.Modules {
|
|
logrus.WithFields(logrus.Fields{
|
|
"module": moduleName,
|
|
"resourceCount": fmt.Sprintf("%d", len(module.Resources)),
|
|
}).Debug("Found module in state")
|
|
for _, stateRes := range module.Resources {
|
|
resName := stateRes.Addr.Resource.Name
|
|
resType := stateRes.Addr.Resource.Type
|
|
|
|
if !resource.IsResourceTypeSupported(resType) {
|
|
logrus.WithFields(logrus.Fields{
|
|
"name": resName,
|
|
"type": resType,
|
|
}).Debug("Ignored unsupported resource from state")
|
|
continue
|
|
}
|
|
|
|
if r.filter != nil && r.filter.IsTypeIgnored(resource.ResourceType(resType)) {
|
|
logrus.WithFields(logrus.Fields{
|
|
"name": resName,
|
|
"type": resType,
|
|
}).Debug("Ignored resource from state since it is ignored in filter")
|
|
continue
|
|
}
|
|
|
|
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 := r.library.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]
|
|
val := decodedRes{
|
|
source: resource.NewTerraformStateSource(r.config.String(), moduleName, resName),
|
|
val: decodedVal.Value,
|
|
}
|
|
if !exists {
|
|
resMap[stateRes.Addr.Resource.Type] = []decodedRes{val}
|
|
} else {
|
|
resMap[stateRes.Addr.Resource.Type] = append(resMap[stateRes.Addr.Resource.Type], val)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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(valFromState map[string][]decodedRes) ([]*resource.Resource, error) {
|
|
results := make([]*resource.Resource, 0)
|
|
|
|
for ty, val := range valFromState {
|
|
for _, stateVal := range val {
|
|
res, err := r.deserializer.DeserializeOne(ty, stateVal.val)
|
|
if err != nil {
|
|
logrus.WithFields(logrus.Fields{
|
|
"type": ty,
|
|
"name": stateVal.source.InternalName(),
|
|
"state": stateVal.source.Source(),
|
|
}).Warnf("Could not read from state: %+v", err)
|
|
continue
|
|
}
|
|
res.Source = stateVal.source
|
|
results = append(results, res)
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (r *TerraformStateReader) Resources() ([]*resource.Resource, error) {
|
|
if r.enumerator == nil {
|
|
return r.retrieveForState(r.config.Path)
|
|
}
|
|
|
|
return r.retrieveMultiplesStates()
|
|
}
|
|
|
|
func (r *TerraformStateReader) retrieveForState(path string) ([]*resource.Resource, error) {
|
|
r.config.Path = path
|
|
logrus.WithFields(logrus.Fields{
|
|
"path": r.config.Path,
|
|
"backend": r.config.Backend,
|
|
}).Debug("Reading resources from state")
|
|
r.progress.Inc()
|
|
values, err := r.retrieve()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, r.config.String())
|
|
}
|
|
decode, err := r.decode(values)
|
|
return decode, errors.Wrap(err, r.config.String())
|
|
}
|
|
|
|
func (r *TerraformStateReader) retrieveMultiplesStates() ([]*resource.Resource, error) {
|
|
keys, err := r.enumerator.Enumerate()
|
|
if err != nil {
|
|
r.alerter.SendAlert("", NewStateReadingAlert(r.enumerator.Origin(), err))
|
|
return nil, errors.Wrap(err, r.config.String())
|
|
}
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
"keys": keys,
|
|
}).Debug("Enumerated keys")
|
|
|
|
results := make([]*resource.Resource, 0)
|
|
isSuccess := false
|
|
readingError := iac.NewStateReadingError()
|
|
|
|
for _, key := range keys {
|
|
resources, err := r.retrieveForState(key)
|
|
if err != nil {
|
|
readingError.Add(err)
|
|
r.alerter.SendAlert("", NewStateReadingAlert(key, err))
|
|
continue
|
|
}
|
|
isSuccess = true
|
|
results = append(results, resources...)
|
|
}
|
|
|
|
if !isSuccess {
|
|
// all key failed, throw an error
|
|
return results, readingError
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func read(path string, reader backend.Backend) (*states.State, error) {
|
|
state, err := readState(path, reader)
|
|
if err != nil {
|
|
if _, ok := reader.(*backend.HTTPBackend); ok && strings.Contains(err.Error(), "The state file could not be parsed as JSON") {
|
|
return nil, errors.Errorf("given url is not a valid state file")
|
|
}
|
|
return nil, err
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func readState(path string, reader backend.Backend) (*states.State, error) {
|
|
state, err := statefile.Read(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
supported, err := IsVersionSupported(state.TerraformVersion.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !supported {
|
|
return nil, &UnsupportedVersionError{
|
|
StateFile: path,
|
|
Version: state.TerraformVersion,
|
|
}
|
|
}
|
|
|
|
return state.State, nil
|
|
}
|