ekristen/aws-nuke

View on GitHub
docs/resources.md

Summary

Maintainability
Test Coverage
# Resources

Resources are the core of the tool, they are what is used to list and remove resources from AWS. The resources are
broken down into separate files. 

When creating a resource there's the base resource type, then there's the `Lister` type that returns a list of resources
that it discovers. Those resources are then filtered by any filtering criteria on the resource itself.

## Anatomy of a Resource

The anatomy of a resource is fairly simple, it's broken down into a few parts:

- `Resource` - This is the base resource type that is used to define the resource.
- `Lister` - This is the type that is used to list the resources.

### Resource

The resource must have the `func Remove() error` method defined on it, this is what is used to remove the resource.

It can optionally have the following methods defined:

- `func Filter() error` - This is used to pre-filter resources, usually based on internal criteria, like system defaults.
- `func String() string` - This is used to print the resource in a human-readable format.
- `func Properties() types.Properties` - This is used to print the resource in a human-readable format.

```go
package resources

import (
    "context"

    "github.com/ekristen/libnuke/pkg/resource"
    "github.com/ekristen/libnuke/pkg/types"

    "github.com/ekristen/aws-nuke/v3/pkg/nuke"
)

type ExampleResource struct {
    ID *string `description:"The ID of the resource"`
}

func (r *ExampleResource) Remove(_ context.Context) error {
    // remove the resource, an error will put the resource in failed state
    // resources in failed state are retried a number of times
    return nil
}

func (r *ExampleResource) Filter() error {
    // filter the resource, this is useful for built-in resources that cannot
    // be removed, like an AWS managed resource, return an error here to filter
    // it before it even gets to the user supplied filters.
    return nil
}

func (r *ExampleResource) String() string {
    // return a string representation of the resource, this is legacy, but still
    // used for a number of reasons.
    return *r.ID
}

func (r *ExampleResource) Properties() types.Properties {
    // return a properties representation of the resource via struct inspection
    return types.NewPropertiesFromStruct(r)
}
```

## Lister

The lister must have the `func List(ctx context.Context, o interface{}) ([]resource.Resource, error)` method defined on it.

```go
package resources

import (
    "context"

    "github.com/ekristen/libnuke/pkg/resource"

    "github.com/ekristen/aws-nuke/v3/pkg/nuke"
)

type ExampleResourceLister struct{}

func (l *ExampleResourceLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) {
    opts := o.(*nuke.ListerOpts)

    var resources []resource.Resource
    
    // list the resources and add to resources slice

    return resources, nil
}
```

### Example

```go
package resources

import (
    "context"
    
    "github.com/ekristen/libnuke/pkg/resource"
    "github.com/ekristen/libnuke/pkg/types"

    "github.com/ekristen/aws-nuke/v3/pkg/nuke"
)

type ExampleResourceLister struct{}

func (l *ExampleResourceLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) {
    opts := o.(*nuke.ListerOpts)

    var resources []resource.Resource
    
    // list the resources and add to resources slice

    return resources, nil
}

// -----------------------------------------------------------------------------

type ExampleResource struct {
    ID *string `description:"The unique ID of the resource"`
}

func (r *ExampleResource) Remove(_ context.Context) error {
    // remove the resource, an error will put the resource in failed state
    // resources in failed state are retried a number of times
    return nil
}

func (r *ExampleResource) Filter() error {
    // filter the resource, this is useful for built-in resources that cannot
    // be removed, like an AWS managed resource, return an error here to filter
    // it before it even gets to the user supplied filters.
    return nil
}

func (r *ExampleResource) String() string {
    // return a string representation of the resource, this is legacy, but still
    // used for a number of reasons.
    return *r.ID
}

func (r *ExampleResource) Properties() types.Properties {
    // return a properties representation of the resource from struct inspection
    return types.NewPropertiesFromStruct(r)
}
```

## Creating a new resource

Creating a new resources is fairly straightforward and a template is provided for you, along with a tool to help you
generate the boilerplate code.

Currently, the code is generated using a tool that is located in `tools/create-resource/main.go` and can be run like so:

!!! note
    At present, the tool does not check if the service or the resource type is valid, this is purely a helper tool to
    generate the boilerplate code.

```bash
go run tools/create-resource/main.go <service> <resource-type>
```

This will output the boilerplate code to stdout, so you can copy and paste it into the appropriate file or you can
redirect to a file like so:

```bash
go run tools/create-resource/main.go <service> <resource-type> > resources/<resource-type>.go
```

## Converting a resource for self documenting

To convert a resource for self documenting, you need to do the following:

- Write a test for the resource to verify its current properties
- Add the `Resource` field to the registration struct
- Capitalize the first letter of the field names
- Match the field names to the property names that are defined in `Properties()` method
- Switch to `NewPropertiesFromStruct` method in `Properties()` method
- Run the tests to verify the properties are still correct
- Run the `generate-resource-docs` command to generate the documentation
- Commit your changes and open a pull request

### Example

**Before**

```go
package resources

import (
    "context"

    "fmt"

    "github.com/aws/aws-sdk-go/service/sns"

    "github.com/ekristen/libnuke/pkg/registry"
    "github.com/ekristen/libnuke/pkg/resource"
    "github.com/ekristen/libnuke/pkg/types"

    "github.com/ekristen/aws-nuke/v3/pkg/nuke"
)

const SNSTopicResource = "SNSTopic"

func init() {
    registry.Register(&registry.Registration{
        Name:   SNSTopicResource,
        Scope:  nuke.Account,
        Lister: &SNSTopicLister{},
    })
}

type SNSTopic struct {
    svc  *sns.SNS
    id   *string
    tags []*sns.Tag
}

func (r *SNSTopic) Remove(_ context.Context) error {
    _, err := r.svc.DeleteTopic(&sns.DeleteTopicInput{
        TopicArn: r.id,
    })
    return err
}

func (r *SNSTopic) Properties() types.Properties {
    properties := types.NewProperties()

    for _, tag := range r.tags {
        properties.SetTag(tag.Key, tag.Value)
    }
    properties.Set("TopicARN", r.id)

    return properties
}

func (r *SNSTopic) String() string {
    return fmt.Sprintf("TopicARN: %s", *r.id)
}

type SNSTopicLister struct{}

func (l *SNSTopicLister) List(_ context.Context, _ interface{}) ([]resource.Resource, error) {
    return nil, nil
}
```

**After**

```go
package resources

import (
    "context"

    "fmt"

    "github.com/aws/aws-sdk-go/service/sns"

    "github.com/ekristen/libnuke/pkg/registry"
    "github.com/ekristen/libnuke/pkg/resource"
    "github.com/ekristen/libnuke/pkg/types"

    "github.com/ekristen/aws-nuke/v3/pkg/nuke"
)

const SNSTopicResource = "SNSTopic"

func init() {
    registry.Register(&registry.Registration{
        Name:     SNSTopicResource,
        Scope:    nuke.Account,
        Resource: &SNSTopic{},
        Lister:   &SNSTopicLister{},
    })
}

type SNSTopic struct {
    svc      *sns.SNS
    TopicARN *string `description:"The ARN of the SNS Topic"`
    Tags     []*sns.Tag
}

func (r *SNSTopic) Remove(_ context.Context) error {
    _, err := r.svc.DeleteTopic(&sns.DeleteTopicInput{
        TopicArn: r.TopicARN,
    })
    return err
}

func (r *SNSTopic) Properties() types.Properties {
    return types.NewPropertiesFromStruct(r)
}

func (r *SNSTopic) String() string {
    return fmt.Sprintf("TopicARN: %s", *r.TopicARN)
}

type SNSTopicLister struct{}

func (l *SNSTopicLister) List(_ context.Context, _ interface{}) ([]resource.Resource, error) {
    return nil, nil
}
```