driftctl/doc/contributing/adding_a_new_resource.md

4.7 KiB

Adding a new resource type to driftctl

Diagram

1 Defining the resource

First step is to implement a new resource will be to define a go struct representing all fields that needs to be monitored for this kind of resource. You can find example in already implemented resource like aws.S3Bucket

type AwsS3Bucket struct {
	AccelerationStatus       *string           `cty:"acceleration_status"`
	Acl                      *string           `cty:"acl" diff:"-"`
	Arn                      *string           `cty:"arn"`
	Bucket                   *string           `cty:"bucket"`
...

Your new type will need to implement resource.Resource interface in order for driftctl to retrieve its type and a unique identifier for it.

type Resource interface {
	TerraformId() string
	TerraformType() string
}

Some resources are read differently by the terraform state reader and the supplier. You can optionally implement resource.NormalizedResource to add a normalization step before the comparison is made.

type NormalizedResource interface {
	NormalizeForState() (Resource, error)
	NormalizeForProvider() (Resource, error)
}

For example S3Bucket policy is encoded in json but the formatting (newline and tabs) differs when read using the state reader. S3Bucket implements resource.NormalizedResource:

func (s S3Bucket) NormalizeForState() (resource.Resource, error) {
	err := normalizePolicy(&s)
	return &s, err
}

func (s S3Bucket) NormalizeForProvider() (resource.Resource, error) {
	err := normalizePolicy(&s)
	return &s, err
}
func normalizePolicy(s *S3Bucket) error {
	if s.Policy.Policy != nil {
		jsonString, err := structure.NormalizeJsonString(*s.Policy.Policy)
		if err != nil {
			return err
		}
		s.Policy.Policy = &jsonString
	}
	return nil
}

You can implement different normalization for the state representation and the supplier one.

2 Supplier and Deserializer

Then you will have to implement two interfaces:

  • resource.supplier is used to read resources list. It will call the cloud provider sdk to get the list of resources, and the terraform provider to get the details for each of these resources.
  • remote.CTYDeserializer is used to transform terraform cty output into your resource

Supplier

This is used to read resources list. It will call the cloud provider sdk to get the list of resources, and the terraform provider to get the details for each of these resources. You can use an already implemented resource as example. Supplier constructor could use these arguments:

  • an instance of ParallelRunner that you will use to parallelize your call to the supplier:
results := make(map[string][]cty.Value)
for _, bucket := range response.Buckets {
    b := *bucket
    s.runner.Run(func() error {
        return s.readBucket(b, results)
    })
}
if err := s.runner.Wait(); err != nil {
    return nil, err
}
  • an instance of terraform.ResourceReader that you can use to read resource using the supplier:
s3Bucket, err := s.reader.ReadResource(aws.AwsS3BucketResourceType, name)
if err != nil {
    logrus.Warnf("Error reading bucket %s[%s]: %+v", name, aws.AwsS3BucketResourceType, err)
    return err
}
appendValueIntoMap(results, aws.AwsS3BucketResourceType, s3Bucket)
  • an instance of the cloud provider sdk that you will use to retrieve resources list.

Deserializer

The deserializer is used when reading resource from the terraform provider or from the state. The interface contains a Deserialize(values []cty.Value) ([]resource.Resource, error) method that you'll implement.

You should then deserialize the obtained cty values into your resource and return the list.

Example: aws_s3_bucket_deserializer.go

3 Adding your resource

There are two files you are going to edit to make driftctl aware of your new resource.

For the state reader you will need to add your CTYDeserializer implementation into iac/deserializers.go Just add an instance in the list:

func Deserializers() []remote.CTYDeserializer {
	return []remote.CTYDeserializer{
        aws.NewS3BucketDeserializer(),
		...
	}
}

Then in the cloud provider's init file (e.g. in remote/aws/init.go) add your new implementation for resource.Supplier:

func Init() error {
	provider, err := NewTerraFormProvider()
	if err != nil {
		return err
	}

	terraform.AddProvider(terraform.AWS, provider)
	resource.AddSupplier(NewS3BucketSupplier(provider.Runner().SubRunner(), s3.New(provider.session)))
	...
}

Don't forget to add unit tests after adding a new resource. You can also add acceptance test if you think it makes sense.