2021-02-26 10:41:00 +00:00
# Add new resources
2021-08-24 15:07:34 +00:00
First, you need to understand how `driftctl scan` works. Here you'll find a global overview of the steps that compose the scan:
2021-08-02 15:01:01 +00:00
![Diagram ](media/generalflow.png )
2021-08-24 15:07:34 +00:00
Then, you'll find below a more detailed flow of how we handle the enumeration and the fetching of resource's details from the remote:
2020-12-09 15:31:34 +00:00
![Diagram ](media/resource.png )
2021-02-26 10:41:00 +00:00
## Defining the resource
2021-08-24 15:07:34 +00:00
First step would be to add a file called `pkg/resource/<providername>/<resourcetype>.go` .
This file will define a string constant that will be the resource type identifier in driftctl.
Optionally, if your resource is to be supported by driftctl experimental deep mode, you can add a function that will be applied to this resource at creation.
This allows to prevent useless diffs to be displayed.
You can also add metadata to fields so that they are compared or displayed differently.
For example this defines the `aws_iam_role` resource:
2020-12-09 15:31:34 +00:00
```go
2021-08-02 15:01:01 +00:00
const AwsIamRoleResourceType = "aws_iam_role"
func initAwsIAMRoleMetaData(resourceSchemaRepository resource.SchemaRepositoryInterface) {
// assume_role_policy drifts will be displayed as json
resourceSchemaRepository.UpdateSchema(AwsIamRoleResourceType, map[string]func(attributeSchema *resource.AttributeSchema){
"assume_role_policy": func(attributeSchema *resource.AttributeSchema) {
attributeSchema.JsonString = true
},
})
// force_detach_policies should not be compared so it will be removed before the comparison
2021-08-10 13:53:39 +00:00
resourceSchemaRepository.SetNormalizeFunc(AwsIamRoleResourceType, func(res *resource.Resource) {
2021-08-02 15:01:01 +00:00
val := res.Attrs
val.SafeDelete([]string{"force_detach_policies"})
})
}
2020-12-09 15:31:34 +00:00
```
2021-08-24 15:07:34 +00:00
When it's done you'll have to add this function to the metadata initialisation located in `pkg/resource/<providername>/metadatas.go` :
2020-12-09 15:31:34 +00:00
```go
2021-08-02 15:01:01 +00:00
func InitResourcesMetadata(resourceSchemaRepository resource.SchemaRepositoryInterface) {
initAwsAmiMetaData(resourceSchemaRepository)
2020-12-09 15:31:34 +00:00
}
```
2021-08-02 15:01:01 +00:00
In order for you new resource to be supported by our terraform state reader you should add it in `pkg/resource/resource_types.go` inside the `supportedTypes` slice.
2021-08-24 15:07:34 +00:00
2020-12-09 15:31:34 +00:00
```go
2021-08-02 15:01:01 +00:00
var supportedTypes = map[string]struct{}{
"aws_ami": {},
2020-12-09 15:31:34 +00:00
}
```
2021-08-02 15:01:01 +00:00
2021-08-10 13:53:39 +00:00
All resources inside driftctl are `resource.Resource` structs.
2021-08-02 15:01:01 +00:00
All the other attributes are represented inside a `map[string]interface`
## Repository, Enumerator and DetailsFetcher
Then you will have to implement two interfaces:
2021-08-24 15:07:34 +00:00
- Repositories are the way we decided to hide direct calls to SDK and pagination logic. It's a common abstraction pattern for data retrieval.
- `remote.common.Enumerator` is used to enumerate resources. It will call the cloud provider SDK to get the list of resources.
2021-08-10 13:53:39 +00:00
For some resource it could make other call to enrich the resource with additional attributes when driftctl is used in deep mode
2021-08-24 15:07:34 +00:00
- `remote.common.DetailsFetcher` is used to retrieve resource's details. It makes a call to Terraform provider `ReadResource` .
2021-08-02 15:01:01 +00:00
This implementation is optional and is only needed if your resource type is to be supported by experimental deep mode.
2021-08-24 15:07:34 +00:00
Please also note that it exists a generic implementation called `remote.common.GenericDetailsFetcher` that can be used with most resource types.
2021-08-02 15:01:01 +00:00
### Repository
2021-08-24 15:07:34 +00:00
This will be the component that hides all the logic linked to your provider SDK. All providers have different ways to implement pagination or to name function in their API.
2021-08-02 15:01:01 +00:00
2021-08-24 15:07:34 +00:00
Here we will name all listing functions `ListAll<ResourceTypeName>` .
For AWS we decided to split repositories using the Amazon logic. So you'll find repositories for EC2, S3 and so on.
2021-08-02 15:01:01 +00:00
Some provider does not have this grouping logic. Keep in mind that like all our file/struct repositories should not be too big.
2021-08-24 15:07:34 +00:00
For our GitHub implementation the number of listing functions was not that heavy, so we created a unique repository for everything:
2020-12-09 15:31:34 +00:00
```go
2021-08-02 15:01:01 +00:00
type GithubRepository interface {
ListRepositories() ([]string, error)
ListTeams() ([]Team, error)
ListMembership() ([]string, error)
ListTeamMemberships() ([]string, error)
ListBranchProtection() ([]string, error)
2020-12-09 15:31:34 +00:00
}
2021-08-02 15:01:01 +00:00
type githubRepository struct {
client GithubGraphQLClient
ctx context.Context
config githubConfig
cache cache.Cache
2020-12-09 15:31:34 +00:00
}
2021-02-26 10:41:00 +00:00
2021-08-02 15:01:01 +00:00
func NewGithubRepository(config githubConfig, c cache.Cache) *githubRepository {
ctx := context.Background()
ts := oauth2.StaticTokenSource(
& oauth2.Token{AccessToken: config.Token},
)
oauthClient := oauth2.NewClient(ctx, ts)
repo := & githubRepository{
client: githubv4.NewClient(oauthClient),
ctx: context.Background(),
config: config,
cache: c,
2020-12-09 15:31:34 +00:00
}
2021-08-02 15:01:01 +00:00
return repo
2020-12-09 15:31:34 +00:00
}
```
2021-08-24 15:07:34 +00:00
As you can see, this contains the logic to create the GitHub client (it might be created outside the repository if it makes sense to share it between multiple repositories).
driftctl, sometimes, needs to retrieve the list of resources more than once, so we cache each request to avoid unnecessary call.
2020-12-09 15:31:34 +00:00
2021-08-02 15:01:01 +00:00
### Enumerator
2021-08-24 15:07:34 +00:00
Enumerators can be found in `pkg/remote/<providername>/<type>_enumerator.go` . It will call the cloud provider SDK to get the list of resources.
2021-08-02 15:01:01 +00:00
2021-08-24 15:07:34 +00:00
Note that at this point, resources should not be entirely fetched and most of them will have empty attributes (e.g. only their id and type).
2021-08-02 15:01:01 +00:00
Most of the resource returned by enumerator have empty attributes: they only represent type and terraform id.
2021-08-10 13:53:39 +00:00
2021-08-24 15:07:34 +00:00
**There are exceptions to this**:
- Sometimes, you will need more information about resources for them to be fetched in the `DetailsFetcher` . For those cases, you will add specific attributes to the map of data.
- For complex cases (e.g. middlewares) where you would need driftctl to run as expected in deep and non-deep mode, you would need to enumerate resources as well as to fetch manually specific attributes, using the remote SDK, before adding them to the map of data.
2020-12-09 15:31:34 +00:00
2021-08-24 15:07:34 +00:00
You can use an already implemented Enumerator as example.
2020-12-09 15:31:34 +00:00
2021-08-24 15:07:34 +00:00
For example, to implement `aws_instance` resource you will need to add a `ListAllInstances()` function to `repository.EC2Repository` .
2021-08-02 15:01:01 +00:00
2021-08-24 15:07:34 +00:00
Bear in mind it will be called by the Enumerator to retrieve the list of instances.
2020-12-09 15:31:34 +00:00
2021-08-10 13:53:39 +00:00
Enumerator constructor could use these arguments:
2021-08-24 15:07:34 +00:00
- an instance of `Repository` that you will use to retrieve information about the resource
2021-08-02 15:01:01 +00:00
- the global resource factory that should always be used to create a new `resource.Resource`
2021-08-24 15:07:34 +00:00
Enumerator then needs to implement:
- `SupportedType() resource.ResourceType` that will return the constant you defined in the type file
- `Enumerate() ([]*resource.Resource, error)` that will return the list of resources
2020-12-09 15:31:34 +00:00
```go
2021-08-02 15:01:01 +00:00
type EC2InstanceEnumerator struct {
repository repository.EC2Repository
factory resource.ResourceFactory
2020-12-09 15:31:34 +00:00
}
2021-08-02 15:01:01 +00:00
func NewEC2InstanceEnumerator(repo repository.EC2Repository, factory resource.ResourceFactory) *EC2InstanceEnumerator {
return & EC2InstanceEnumerator{
repository: repo,
factory: factory,
}
}
2020-12-09 15:31:34 +00:00
2021-08-02 15:01:01 +00:00
func (e *EC2InstanceEnumerator) SupportedType() resource.ResourceType {
return aws.AwsInstanceResourceType
2020-12-09 15:31:34 +00:00
}
2021-08-10 13:53:39 +00:00
func (e *EC2InstanceEnumerator) Enumerate() ([]*resource.Resource, error) {
2021-08-02 15:01:01 +00:00
instances, err := e.repository.ListAllInstances()
if err != nil {
return nil, remoteerror.NewResourceListingError(err, string(e.SupportedType()))
}
2020-12-09 15:31:34 +00:00
2021-08-10 13:53:39 +00:00
results := make([]*resource.Resource, len(instances))
2021-08-02 15:01:01 +00:00
for _, instance := range instances {
results = append(
results,
e.factory.CreateAbstractResource(
string(e.SupportedType()),
*instance.InstanceId,
map[string]interface{}{},
),
)
}
2020-12-09 15:31:34 +00:00
2021-08-02 15:01:01 +00:00
return results, err
}
```
2021-08-10 13:53:39 +00:00
2021-08-24 15:07:34 +00:00
As you can see, listing errors are treated in a particular way. Instead of failing and stopping the scan they will be handled, and an alert will be created.
2021-08-10 13:53:39 +00:00
So please don't forget to wrap these errors inside a `NewResourceListingError` .
2021-08-02 15:01:01 +00:00
For some provider error handling is not that coherent, so you might need to check in `pkg/remote/resource_enumeration_error_handler.go` and add a new case for your error.
2021-08-24 15:07:34 +00:00
You should test enumerator behavior when you do not have permission to enumerate resources. In the snippet above, `ListAllInstances` may return an `AccessDenied` error that should be handled.
Once the enumerator is written you have to add it to the remote initialization located in `pkg/remote/<providername>/init.go` :
2020-12-09 15:31:34 +00:00
2021-08-02 15:01:01 +00:00
```go
2021-08-10 13:53:39 +00:00
remoteLibrary.AddEnumerator(NewEC2InstanceEnumerator(s3Repository, factory))
2021-08-02 15:01:01 +00:00
```
### DetailsFetcher
2020-12-09 15:31:34 +00:00
2021-08-24 15:07:34 +00:00
DetailsFetchers are only used by driftctl experimental deep mode.
2020-12-09 15:31:34 +00:00
2021-08-24 15:07:34 +00:00
This is the component that call Terraform provider to retrieve all attributes for each resource.
We do not want to reimplement what has already been done in each Terraform provider. Thus, you should not call the remote SDK there.
2020-12-09 15:31:34 +00:00
2021-08-24 15:07:34 +00:00
If `common.GenericDetailsFetcher` satisfies your needs you should always prefer using it instead of implementing a custom `DetailsFetcher` in a new struct.
The `DetailsFetcher` should also be added to `pkg/remote/<providername>/init.go` even if you use the generic version:
2021-02-26 10:41:00 +00:00
2020-12-09 15:31:34 +00:00
```go
2021-08-02 15:01:01 +00:00
remoteLibrary.AddDetailsFetcher(aws.AwsEbsVolumeResourceType, common.NewGenericDetailsFetcher(aws.AwsEbsVolumeResourceType, provider, deserializer))
2020-12-09 15:31:34 +00:00
```
2021-08-02 15:01:01 +00:00
***Don't forget to add unit tests after adding a new resource.***
2020-12-09 15:31:34 +00:00
2021-08-24 15:07:34 +00:00
You can find example of **functional tests** in `pkg/remote/<type>_scanner_test.go` .
2020-12-09 15:31:34 +00:00
2021-08-24 15:07:34 +00:00
You should also add **acceptance tests** if you think it makes sense. They are located next to the resource definition described in the first step.
2021-08-10 13:53:39 +00:00
2021-08-24 15:07:34 +00:00
More information about adding tests can be found in [testing documentation ](testing.md )