diff --git a/docs/resources/cloud-formation-stack.md b/docs/resources/cloud-formation-stack.md index e7402ac1..0ec3fd84 100644 --- a/docs/resources/cloud-formation-stack.md +++ b/docs/resources/cloud-formation-stack.md @@ -11,8 +11,16 @@ generated: true CloudFormationStack ``` +## Properties +- `CreationTime`: No Description +- `LastUpdatedTime`: No Description +- `Name`: No Description +- `Status`: No Description +- `tag::`: This resource has tags with property `Tags`. These are key/value pairs that are + added as their own property with the prefix of `tag:` (e.g. [tag:example: "value"]) + !!! note - Using Properties Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property names to write filters for what you want to **keep** and omit from the nuke process. @@ -28,6 +36,7 @@ The string value is always what is used in the output of the log format when a r ## Settings - `DisableDeletionProtection` +- `CreateRoleToDeleteStack` ### DisableDeletionProtection @@ -40,3 +49,14 @@ The string value is always what is used in the output of the log format when a r DisableDeletionProtection ``` + +### CreateRoleToDeleteStack + +!!! note + There is currently no description for this setting. Often times settings are fairly self-explanatory. However, we + are working on adding descriptions for all settings. + +```text +CreateRoleToDeleteStack +``` + diff --git a/docs/resources/cloud-watch-alarm.md b/docs/resources/cloud-watch-alarm.md index faaadf34..64b58c02 100644 --- a/docs/resources/cloud-watch-alarm.md +++ b/docs/resources/cloud-watch-alarm.md @@ -11,8 +11,14 @@ generated: true CloudWatchAlarm ``` +## Properties +- `Name`: No Description +- `Type`: No Description +- `tag::`: This resource has tags with property `Tags`. These are key/value pairs that are + added as their own property with the prefix of `tag:` (e.g. [tag:example: "value"]) + !!! note - Using Properties Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property names to write filters for what you want to **keep** and omit from the nuke process. diff --git a/docs/resources/cloud-watch-events-target.md b/docs/resources/cloud-watch-events-target.md index ed49fc9f..397d1dc2 100644 --- a/docs/resources/cloud-watch-events-target.md +++ b/docs/resources/cloud-watch-events-target.md @@ -11,8 +11,13 @@ generated: true CloudWatchEventsTarget ``` +## Properties +- `BusName`: The name of the event bus the rule applies to +- `Name`: The name of the rule +- `TargetID`: The ID of the target for the rule + !!! note - Using Properties Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property names to write filters for what you want to **keep** and omit from the nuke process. diff --git a/docs/resources/ec2-tgw.md b/docs/resources/ec2-tgw.md index 45df81e7..459cb40d 100644 --- a/docs/resources/ec2-tgw.md +++ b/docs/resources/ec2-tgw.md @@ -11,8 +11,15 @@ generated: true EC2TGW ``` +## Properties +- `ID`: The ID of the transit gateway. +- `OwnerId`: The ID of the AWS account that owns the transit gateway. +- `State`: The state of the transit gateway. +- `tag::`: This resource has tags with property `Tags`. These are key/value pairs that are + added as their own property with the prefix of `tag:` (e.g. [tag:example: "value"]) + !!! note - Using Properties Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property names to write filters for what you want to **keep** and omit from the nuke process. diff --git a/docs/resources/neptune-snapshot.md b/docs/resources/neptune-snapshot.md index a1c02696..4a938c37 100644 --- a/docs/resources/neptune-snapshot.md +++ b/docs/resources/neptune-snapshot.md @@ -14,7 +14,9 @@ NeptuneSnapshot ## Properties +- `CreateTime`: No Description - `ID`: No Description +- `Status`: No Description !!! note - Using Properties Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property diff --git a/docs/resources/resource-explorer-2index.md b/docs/resources/resource-explorer-2index.md index fcac6e61..244bb6de 100644 --- a/docs/resources/resource-explorer-2index.md +++ b/docs/resources/resource-explorer-2index.md @@ -11,8 +11,12 @@ generated: true ResourceExplorer2Index ``` +## Properties +- `ARN`: No Description +- `Type`: No Description + !!! note - Using Properties Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property names to write filters for what you want to **keep** and omit from the nuke process. diff --git a/docs/resources/resource-explorer-2view.md b/docs/resources/resource-explorer-2view.md index a2841730..44bc5f9a 100644 --- a/docs/resources/resource-explorer-2view.md +++ b/docs/resources/resource-explorer-2view.md @@ -11,8 +11,11 @@ generated: true ResourceExplorer2View ``` +## Properties +- `ARN`: The ARN of the Resource Explorer View + !!! note - Using Properties Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property names to write filters for what you want to **keep** and omit from the nuke process. diff --git a/docs/resources/route-53-resource-record-set.md b/docs/resources/route-53-resource-record-set.md index 608876d1..558d85ec 100644 --- a/docs/resources/route-53-resource-record-set.md +++ b/docs/resources/route-53-resource-record-set.md @@ -11,8 +11,15 @@ generated: true Route53ResourceRecordSet ``` +## Properties +- `HostedZoneName`: The name of the zone to which the record belongs +- `Name`: The name of the record +- `Type`: The type of the record +- `tag::`: This resource has tags with property `Tags`. These are key/value pairs that are + added as their own property with the prefix of `tag:` (e.g. [tag:example: "value"]) + !!! note - Using Properties Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property names to write filters for what you want to **keep** and omit from the nuke process. diff --git a/docs/resources/s3-access-grants-grant.md b/docs/resources/s3-access-grants-grant.md new file mode 100644 index 00000000..3976c384 --- /dev/null +++ b/docs/resources/s3-access-grants-grant.md @@ -0,0 +1,34 @@ +--- +generated: true +--- + +# S3AccessGrantsGrant + + +## Resource + +```text +S3AccessGrantsGrant +``` + +## Properties + + +- `CreatedAt`: The date and time the access grant was created. +- `GrantScope`: The scope of the access grant. +- `GranteeID`: The ARN of the grantee. +- `GranteeType`: The type of the grantee, (e.g. IAM). +- `ID`: The ID of the access grant. + +!!! note - Using Properties + Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property + names to write filters for what you want to **keep** and omit from the nuke process. + +### String Property + +The string representation of a resource is generally the value of the Name, ID or ARN field of the resource. Not all +resources support properties. To write a filter against the string representation, simply omit the `property` field in +the filter. + +The string value is always what is used in the output of the log format when a resource is identified. + diff --git a/docs/resources/s3-access-grants-instance.md b/docs/resources/s3-access-grants-instance.md new file mode 100644 index 00000000..8d313ae5 --- /dev/null +++ b/docs/resources/s3-access-grants-instance.md @@ -0,0 +1,31 @@ +--- +generated: true +--- + +# S3AccessGrantsInstance + + +## Resource + +```text +S3AccessGrantsInstance +``` + +## Properties + + +- `CreatedAt`: The time the access grants instance was created. +- `ID`: The ID of the access grants instance. + +!!! note - Using Properties + Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property + names to write filters for what you want to **keep** and omit from the nuke process. + +### String Property + +The string representation of a resource is generally the value of the Name, ID or ARN field of the resource. Not all +resources support properties. To write a filter against the string representation, simply omit the `property` field in +the filter. + +The string value is always what is used in the output of the log format when a resource is identified. + diff --git a/docs/resources/s3-access-grants-location.md b/docs/resources/s3-access-grants-location.md new file mode 100644 index 00000000..e5396b29 --- /dev/null +++ b/docs/resources/s3-access-grants-location.md @@ -0,0 +1,32 @@ +--- +generated: true +--- + +# S3AccessGrantsLocation + + +## Resource + +```text +S3AccessGrantsLocation +``` + +## Properties + + +- `CreatedAt`: The time the access grants location was created. +- `ID`: The ID of the access grants location. +- `LocationScope`: The scope of the access grants location. + +!!! note - Using Properties + Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property + names to write filters for what you want to **keep** and omit from the nuke process. + +### String Property + +The string representation of a resource is generally the value of the Name, ID or ARN field of the resource. Not all +resources support properties. To write a filter against the string representation, simply omit the `property` field in +the filter. + +The string value is always what is used in the output of the log format when a resource is identified. + diff --git a/docs/resources/transfer-web-app.md b/docs/resources/transfer-web-app.md new file mode 100644 index 00000000..38078214 --- /dev/null +++ b/docs/resources/transfer-web-app.md @@ -0,0 +1,30 @@ +--- +generated: true +--- + +# TransferWebApp + + +## Resource + +```text +TransferWebApp +``` + +## Properties + + +- `ID`: No Description + +!!! note - Using Properties + Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property + names to write filters for what you want to **keep** and omit from the nuke process. + +### String Property + +The string representation of a resource is generally the value of the Name, ID or ARN field of the resource. Not all +resources support properties. To write a filter against the string representation, simply omit the `property` field in +the filter. + +The string value is always what is used in the output of the log format when a resource is identified. + diff --git a/go.mod b/go.mod index bc36f7f3..715536b7 100644 --- a/go.mod +++ b/go.mod @@ -32,15 +32,20 @@ require ( github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.38.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 // indirect + github.com/aws/aws-sdk-go-v2/service/s3control v1.52.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssmquicksetup v1.3.2 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect + github.com/aws/aws-sdk-go-v2/service/transfer v1.55.1 // indirect github.com/benbjohnson/clock v1.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index d5ecf236..b0847562 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvK github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 h1:GeNJsIFHB+WW5ap2Tec4K6dzcVTsRbsT1Lra46Hv9ME= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26/go.mod h1:zfgMpwHDXX2WGoG84xG2H+ZlPTkJUU4YUvx2svLQYWo= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.3 h1:2sFIoFzU1IEL9epJWubJm9Dhrn45aTNEJuwsesaCGnk= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.3/go.mod h1:KzlNINwfr/47tKkEhgk0r10/OZq3rjtyWy0txL3lM+I= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 h1:tB4tNw83KcajNAzaIMhkhVI2Nt8fAZd5A5ro113FEMY= @@ -26,8 +28,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQz github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 h1:Hi0KGbrnr57bEHWM0bJ1QcBzxLrL/k2DHvGYhb8+W1w= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7/go.mod h1:wKNgWgExdjjrm4qvfbTorkvocEstaoDl4WCvGfeCy9c= -github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1 h1:aOVVZJgWbaH+EJYPvEgkNhCEbXXvH7+oML36oaPK3zE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1/go.mod h1:r+xl5yzMk9083rMR+sJ5TYj9Tihvf/l1oxzZXDgGj2Q= +github.com/aws/aws-sdk-go-v2/service/s3control v1.52.1 h1:xxGbXbGtO/VMz2JqB1UwEDlSchryUss0KmQJSZ0oTUE= +github.com/aws/aws-sdk-go-v2/service/s3control v1.52.1/go.mod h1:6BuUa52of67a+ri/poTH82XiL+rTGQWUPZCmf2cfVHI= github.com/aws/aws-sdk-go-v2/service/s3 v1.72.0 h1:SAfh4pNx5LuTafKKWR02Y+hL3A+3TX8cTKG1OIAJaBk= github.com/aws/aws-sdk-go-v2/service/s3 v1.72.0/go.mod h1:r+xl5yzMk9083rMR+sJ5TYj9Tihvf/l1oxzZXDgGj2Q= github.com/aws/aws-sdk-go-v2/service/ssmquicksetup v1.3.2 h1:4siT1z3nEVxJq1jZYu1SRoct5xgbKen+ammCuZBZ2zI= @@ -38,6 +40,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k= github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY= github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc= +github.com/aws/aws-sdk-go-v2/service/transfer v1.55.1 h1:bENkaFtA6rxHAwNPjYbgwYxUHGJbL7QocCt8nKZ7d10= +github.com/aws/aws-sdk-go-v2/service/transfer v1.55.1/go.mod h1:C7x9hpm90ZocJ9GbauHMkVMU0m7knEiKhOaa4um9tBU= github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= @@ -82,6 +86,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gotidy/ptr v1.4.0 h1:7++suUs+HNHMnyz6/AW3SE+4EnBhupPSQTSI7QNijVc= github.com/gotidy/ptr v1.4.0/go.mod h1:MjRBG6/IETiiZGWI8LrRtISXEji+8b/jigmj2q0mEyM= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= diff --git a/mkdocs.yml b/mkdocs.yml index e188d0cb..144221b8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -543,6 +543,9 @@ nav: - Route 53 Resolver Rule: resources/route-53-resolver-rule.md - Route 53 Resource Record Set: resources/route-53-resource-record-set.md - Route 53 Traffic Policy: resources/route-53-traffic-policy.md + - S3 Access Grants Grant: resources/s3-access-grants-grant.md + - S3 Access Grants Instance: resources/s3-access-grants-instance.md + - S3 Access Grants Location: resources/s3-access-grants-location.md - S3 Access Point: resources/s3-access-point.md - S3 Bucket: resources/s3-bucket.md - S3 Multipart Upload: resources/s3-multipart-upload.md @@ -607,6 +610,7 @@ nav: - Transcribe Vocabulary: resources/transcribe-vocabulary.md - Transfer Server User: resources/transfer-server-user.md - Transfer Server: resources/transfer-server.md + - Transfer Web App: resources/transfer-web-app.md - WAF Regional Byte Match Set Ip: resources/waf-regional-byte-match-set-ip.md - WAF Regional Byte Match Set: resources/waf-regional-byte-match-set.md - WAF Regional Ip Set Ip: resources/waf-regional-ip-set-ip.md diff --git a/resources/cloudformation-stack.go b/resources/cloudformation-stack.go index 5ed09049..66840a7b 100644 --- a/resources/cloudformation-stack.go +++ b/resources/cloudformation-stack.go @@ -15,6 +15,9 @@ import ( "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" + "github.com/aws/aws-sdk-go-v2/service/iam" + iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + liberrors "github.com/ekristen/libnuke/pkg/errors" "github.com/ekristen/libnuke/pkg/registry" "github.com/ekristen/libnuke/pkg/resource" @@ -36,6 +39,7 @@ func init() { Lister: &CloudFormationStackLister{}, Settings: []string{ "DisableDeletionProtection", + "CreateRoleToDeleteStack", }, }) } @@ -46,6 +50,7 @@ func (l *CloudFormationStackLister) List(_ context.Context, o interface{}) ([]re opts := o.(*nuke.ListerOpts) svc := cloudformation.New(opts.Session) + iamSvc := iam.NewFromConfig(*opts.Config) params := &cloudformation.DescribeStacksInput{} resources := make([]resource.Resource, 0) @@ -56,11 +61,26 @@ func (l *CloudFormationStackLister) List(_ context.Context, o interface{}) ([]re return nil, err } for _, stack := range resp.Stacks { - resources = append(resources, &CloudFormationStack{ + newResource := &CloudFormationStack{ svc: svc, - stack: stack, + iamSvc: iamSvc, + logger: opts.Logger, maxDeleteAttempts: CloudformationMaxDeleteAttempt, - }) + Name: stack.StackName, + Status: stack.StackStatus, + description: stack.Description, + parentID: stack.ParentId, + roleARN: stack.RoleARN, + CreationTime: stack.CreationTime, + LastUpdatedTime: stack.LastUpdatedTime, + Tags: stack.Tags, + } + + if newResource.LastUpdatedTime == nil { + newResource.LastUpdatedTime = newResource.CreationTime + } + + resources = append(resources, newResource) } if resp.NextToken == nil { @@ -75,62 +95,126 @@ func (l *CloudFormationStackLister) List(_ context.Context, o interface{}) ([]re type CloudFormationStack struct { svc cloudformationiface.CloudFormationAPI - stack *cloudformation.Stack - maxDeleteAttempts int + iamSvc *iam.Client settings *settings.Setting + logger *logrus.Entry + Name *string + Status *string + CreationTime *time.Time + LastUpdatedTime *time.Time + Tags []*cloudformation.Tag + description *string + parentID *string + roleARN *string + maxDeleteAttempts int + roleCreated bool + roleName string } -func (cfs *CloudFormationStack) Filter() error { - if ptr.ToString(cfs.stack.Description) == "DO NOT MODIFY THIS STACK! This stack is managed by Config Conformance Packs." { +func (r *CloudFormationStack) Filter() error { + if ptr.ToString(r.description) == "DO NOT MODIFY THIS STACK! This stack is managed by Config Conformance Packs." { return fmt.Errorf("stack is managed by Config Conformance Packs") } return nil } -func (cfs *CloudFormationStack) Settings(setting *settings.Setting) { - cfs.settings = setting +func (r *CloudFormationStack) Settings(setting *settings.Setting) { + r.settings = setting } -func (cfs *CloudFormationStack) Remove(_ context.Context) error { - return cfs.removeWithAttempts(0) +func (r *CloudFormationStack) Remove(ctx context.Context) error { + return r.removeWithAttempts(ctx, 0) } -func (cfs *CloudFormationStack) removeWithAttempts(attempt int) error { - if err := cfs.doRemove(); err != nil { - // TODO: pass logrus in via ListerOpts so that it can be used here instead of global +func (r *CloudFormationStack) createRole(ctx context.Context) error { + roleParts := strings.Split(ptr.ToString(r.roleARN), "/") + _, err := r.iamSvc.CreateRole(ctx, &iam.CreateRoleInput{ + RoleName: ptr.String(roleParts[len(roleParts)-1]), + AssumeRolePolicyDocument: ptr.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +}`), + Tags: []iamtypes.Tag{ + { + Key: ptr.String("Managed"), + Value: ptr.String("aws-nuke"), + }, + }, + }) + + r.roleCreated = true + r.roleName = roleParts[len(roleParts)-1] + + return err +} + +func (r *CloudFormationStack) removeRole(ctx context.Context) error { + if !r.roleCreated { + return nil + } + + _, err := r.iamSvc.DeleteRole(ctx, &iam.DeleteRoleInput{ + RoleName: ptr.String(r.roleName), + }) + return err +} - logrus.Errorf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d delete failed: %s", - *cfs.stack.StackName, attempt, cfs.maxDeleteAttempts, err.Error()) +func (r *CloudFormationStack) removeWithAttempts(ctx context.Context, attempt int) error { + if err := r.doRemove(); err != nil { + r.logger.Errorf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d delete failed: %s", + *r.Name, attempt, r.maxDeleteAttempts, err.Error()) var awsErr awserr.Error ok := errors.As(err, &awsErr) - if ok && awsErr.Code() == "ValidationError" && - awsErr.Message() == "Stack ["+*cfs.stack.StackName+"] cannot be deleted while TerminationProtection is enabled" { - // check if the setting for the resource is set to allow deletion protection to be disabled - if cfs.settings.GetBool("DisableDeletionProtection") { - logrus.Infof("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d updating termination protection", - *cfs.stack.StackName, attempt, cfs.maxDeleteAttempts) - _, err = cfs.svc.UpdateTerminationProtection(&cloudformation.UpdateTerminationProtectionInput{ - EnableTerminationProtection: aws.Bool(false), - StackName: cfs.stack.StackName, - }) - if err != nil { + if ok && awsErr.Code() == "ValidationError" { + if awsErr.Message() == fmt.Sprintf("Role %s is invalid or cannot be assumed", *r.roleARN) { + if r.settings.GetBool("CreateRoleToDeleteStack") { + r.logger.Infof("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d creating role to delete stack", + *r.Name, attempt, r.maxDeleteAttempts) + if err := r.createRole(ctx); err != nil { + return err + } + } else { + r.logger.Warnf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d set feature flag to create role to delete stack", + *r.Name, attempt, r.maxDeleteAttempts) + return err + } + } else if awsErr.Message() == fmt.Sprintf("Stack [%s] cannot be deleted while TerminationProtection is enabled", *r.Name) { + // check if the setting for the resource is set to allow deletion protection to be disabled + if r.settings.GetBool("DisableDeletionProtection") { + r.logger.Infof("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d updating termination protection", + *r.Name, attempt, r.maxDeleteAttempts) + _, err = r.svc.UpdateTerminationProtection(&cloudformation.UpdateTerminationProtectionInput{ + EnableTerminationProtection: aws.Bool(false), + StackName: r.Name, + }) + if err != nil { + return err + } + } else { + r.logger.Warnf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d set feature flag to disable deletion protection", + *r.Name, attempt, r.maxDeleteAttempts) return err } - } else { - logrus.Warnf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d set feature flag to disable deletion protection", - *cfs.stack.StackName, attempt, cfs.maxDeleteAttempts) - return err } } - if attempt >= cfs.maxDeleteAttempts { + + if attempt >= r.maxDeleteAttempts { return errors.New("CFS might not be deleted after this run") } else { - return cfs.removeWithAttempts(attempt + 1) + return r.removeWithAttempts(ctx, attempt+1) } - } else { - return nil } + + return r.removeRole(ctx) } func GetParentStack(svc cloudformationiface.CloudFormationAPI, stackID string) (*cloudformation.Stack, error) { @@ -148,9 +232,9 @@ func GetParentStack(svc cloudformationiface.CloudFormationAPI, stackID string) ( return nil, nil //nolint:nilnil } -func (cfs *CloudFormationStack) doRemove() error { //nolint:gocyclo - if cfs.stack.ParentId != nil { - p, err := GetParentStack(cfs.svc, *cfs.stack.ParentId) +func (r *CloudFormationStack) doRemove() error { //nolint:gocyclo + if r.parentID != nil { + p, err := GetParentStack(r.svc, *r.parentID) if err != nil { return err } @@ -160,14 +244,14 @@ func (cfs *CloudFormationStack) doRemove() error { //nolint:gocyclo } } - o, err := cfs.svc.DescribeStacks(&cloudformation.DescribeStacksInput{ - StackName: cfs.stack.StackName, + o, err := r.svc.DescribeStacks(&cloudformation.DescribeStacksInput{ + StackName: r.Name, }) if err != nil { var awsErr awserr.Error if errors.As(err, &awsErr) { if awsErr.Code() == "ValidationFailed" && strings.HasSuffix(awsErr.Message(), " does not exist") { - logrus.Infof("CloudFormationStack stackName=%s no longer exists", *cfs.stack.StackName) + r.logger.Infof("CloudFormationStack stackName=%s no longer exists", *r.Name) return nil } } @@ -179,16 +263,16 @@ func (cfs *CloudFormationStack) doRemove() error { //nolint:gocyclo // stack already deleted, no need to re-delete return nil } else if *stack.StackStatus == cloudformation.StackStatusDeleteInProgress { - logrus.Infof("CloudFormationStack stackName=%s delete in progress. Waiting", *cfs.stack.StackName) - return cfs.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{ - StackName: cfs.stack.StackName, + r.logger.Infof("CloudFormationStack stackName=%s delete in progress. Waiting", *r.Name) + return r.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{ + StackName: r.Name, }) } else if *stack.StackStatus == cloudformation.StackStatusDeleteFailed { - logrus.Infof("CloudFormationStack stackName=%s delete failed. Attempting to retain and delete stack", *cfs.stack.StackName) + r.logger.Infof("CloudFormationStack stackName=%s delete failed. Attempting to retain and delete stack", *r.Name) // This means the CFS has undetectable resources. // In order to move on with nuking, we retain them in the deletion. - retainableResources, err := cfs.svc.ListStackResources(&cloudformation.ListStackResourcesInput{ - StackName: cfs.stack.StackName, + retainableResources, err := r.svc.ListStackResources(&cloudformation.ListStackResourcesInput{ + StackName: r.Name, }) if err != nil { return err @@ -202,25 +286,25 @@ func (cfs *CloudFormationStack) doRemove() error { //nolint:gocyclo } } - _, err = cfs.svc.DeleteStack(&cloudformation.DeleteStackInput{ - StackName: cfs.stack.StackName, + if _, err = r.svc.DeleteStack(&cloudformation.DeleteStackInput{ + StackName: r.Name, RetainResources: retain, - }) - if err != nil { + }); err != nil { return err } - return cfs.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{ - StackName: cfs.stack.StackName, + + return r.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{ + StackName: r.Name, }) } else { - if err := cfs.waitForStackToStabilize(*stack.StackStatus); err != nil { + if err := r.waitForStackToStabilize(*stack.StackStatus); err != nil { return err - } else if _, err := cfs.svc.DeleteStack(&cloudformation.DeleteStackInput{ - StackName: cfs.stack.StackName, + } else if _, err := r.svc.DeleteStack(&cloudformation.DeleteStackInput{ + StackName: r.Name, }); err != nil { return err - } else if err := cfs.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{ - StackName: cfs.stack.StackName, + } else if err := r.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{ + StackName: r.Name, }); err != nil { return err } else { @@ -228,45 +312,33 @@ func (cfs *CloudFormationStack) doRemove() error { //nolint:gocyclo } } } -func (cfs *CloudFormationStack) waitForStackToStabilize(currentStatus string) error { + +func (r *CloudFormationStack) waitForStackToStabilize(currentStatus string) error { switch currentStatus { case cloudformation.StackStatusUpdateInProgress, cloudformation.StackStatusUpdateRollbackCompleteCleanupInProgress, cloudformation.StackStatusUpdateRollbackInProgress: - logrus.Infof("CloudFormationStack stackName=%s update in progress. Waiting to stabalize", *cfs.stack.StackName) + r.logger.Infof("CloudFormationStack stackName=%s update in progress. Waiting to stabalize", *r.Name) - return cfs.svc.WaitUntilStackUpdateComplete(&cloudformation.DescribeStacksInput{ - StackName: cfs.stack.StackName, + return r.svc.WaitUntilStackUpdateComplete(&cloudformation.DescribeStacksInput{ + StackName: r.Name, }) case cloudformation.StackStatusCreateInProgress, cloudformation.StackStatusRollbackInProgress: - logrus.Infof("CloudFormationStack stackName=%s create in progress. Waiting to stabalize", *cfs.stack.StackName) + r.logger.Infof("CloudFormationStack stackName=%s create in progress. Waiting to stabalize", *r.Name) - return cfs.svc.WaitUntilStackCreateComplete(&cloudformation.DescribeStacksInput{ - StackName: cfs.stack.StackName, + return r.svc.WaitUntilStackCreateComplete(&cloudformation.DescribeStacksInput{ + StackName: r.Name, }) default: return nil } } -func (cfs *CloudFormationStack) Properties() types.Properties { - properties := types.NewProperties() - properties.Set("Name", cfs.stack.StackName) - properties.Set("CreationTime", cfs.stack.CreationTime.Format(time.RFC3339)) - if cfs.stack.LastUpdatedTime == nil { - properties.Set("LastUpdatedTime", cfs.stack.CreationTime.Format(time.RFC3339)) - } else { - properties.Set("LastUpdatedTime", cfs.stack.LastUpdatedTime.Format(time.RFC3339)) - } - - for _, tagValue := range cfs.stack.Tags { - properties.SetTag(tagValue.Key, tagValue.Value) - } - - return properties +func (r *CloudFormationStack) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) } -func (cfs *CloudFormationStack) String() string { - return *cfs.stack.StackName +func (r *CloudFormationStack) String() string { + return *r.Name } diff --git a/resources/cloudformation-stack_test.go b/resources/cloudformation-stack_test.go index 9b7bc82a..9eba55a7 100644 --- a/resources/cloudformation-stack_test.go +++ b/resources/cloudformation-stack_test.go @@ -2,13 +2,14 @@ package resources import ( "context" - "testing" + "time" "github.com/golang/mock/gomock" + "github.com/gotidy/ptr" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/cloudformation" @@ -17,6 +18,35 @@ import ( "github.com/ekristen/aws-nuke/v3/mocks/mock_cloudformationiface" ) +func TestCloudformationStack_Properties(t *testing.T) { + a := assert.New(t) + + now := time.Now() + + stack := CloudFormationStack{ + Name: ptr.String("foobar"), + Status: ptr.String(cloudformation.StackStatusCreateComplete), + CreationTime: ptr.Time(now), + LastUpdatedTime: ptr.Time(now), + Tags: []*cloudformation.Tag{ + { + Key: ptr.String("Name"), + Value: ptr.String("foobar"), + }, + }, + } + + props := stack.Properties() + + a.Equal("foobar", props.Get("Name")) + a.Equal(cloudformation.StackStatusCreateComplete, props.Get("Status")) + a.Equal(now.Format(time.RFC3339), props.Get("CreationTime")) + a.Equal(now.Format(time.RFC3339), props.Get("LastUpdatedTime")) + a.Equal("foobar", props.Get("tag:Name")) + + a.Equal("foobar", stack.String()) +} + func TestCloudformationStack_Remove_StackAlreadyDeleted(t *testing.T) { a := assert.New(t) ctrl := gomock.NewController(t) @@ -25,21 +55,20 @@ func TestCloudformationStack_Remove_StackAlreadyDeleted(t *testing.T) { mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl) stack := CloudFormationStack{ - svc: mockCloudformation, - stack: &cloudformation.Stack{ - StackName: aws.String("foobar"), - }, + svc: mockCloudformation, + logger: logrus.NewEntry(logrus.StandardLogger()), + Name: ptr.String("foobar"), settings: &libsettings.Setting{ "DisableDeletionProtection": true, }, } mockCloudformation.EXPECT().DescribeStacks(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(&cloudformation.DescribeStacksOutput{ Stacks: []*cloudformation.Stack{ { - StackStatus: aws.String(cloudformation.StackStatusDeleteComplete), + StackStatus: ptr.String(cloudformation.StackStatusDeleteComplete), }, }, }, nil) @@ -56,17 +85,16 @@ func TestCloudformationStack_Remove_StackDoesNotExist(t *testing.T) { mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl) stack := CloudFormationStack{ - svc: mockCloudformation, - stack: &cloudformation.Stack{ - StackName: aws.String("foobar"), - }, + svc: mockCloudformation, + logger: logrus.NewEntry(logrus.StandardLogger()), + Name: ptr.String("foobar"), settings: &libsettings.Setting{ "DisableDeletionProtection": true, }, } mockCloudformation.EXPECT().DescribeStacks(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil, awserr.New("ValidationFailed", "Stack with id foobar does not exist", nil)) err := stack.Remove(context.TODO()) @@ -81,10 +109,9 @@ func TestCloudformationStack_Remove_DeleteFailed(t *testing.T) { mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl) stack := CloudFormationStack{ - svc: mockCloudformation, - stack: &cloudformation.Stack{ - StackName: aws.String("foobar"), - }, + svc: mockCloudformation, + logger: logrus.NewEntry(logrus.StandardLogger()), + Name: ptr.String("foobar"), settings: &libsettings.Setting{ "DisableDeletionProtection": true, }, @@ -92,36 +119,36 @@ func TestCloudformationStack_Remove_DeleteFailed(t *testing.T) { gomock.InOrder( mockCloudformation.EXPECT().DescribeStacks(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(&cloudformation.DescribeStacksOutput{ Stacks: []*cloudformation.Stack{ { - StackStatus: aws.String(cloudformation.StackStatusDeleteFailed), + StackStatus: ptr.String(cloudformation.StackStatusDeleteFailed), }, }, }, nil), mockCloudformation.EXPECT().ListStackResources(gomock.Eq(&cloudformation.ListStackResourcesInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(&cloudformation.ListStackResourcesOutput{ StackResourceSummaries: []*cloudformation.StackResourceSummary{ { - ResourceStatus: aws.String(cloudformation.ResourceStatusDeleteComplete), - LogicalResourceId: aws.String("fooDeleteComplete"), + ResourceStatus: ptr.String(cloudformation.ResourceStatusDeleteComplete), + LogicalResourceId: ptr.String("fooDeleteComplete"), }, { - ResourceStatus: aws.String(cloudformation.ResourceStatusDeleteFailed), - LogicalResourceId: aws.String("fooDeleteFailed"), + ResourceStatus: ptr.String(cloudformation.ResourceStatusDeleteFailed), + LogicalResourceId: ptr.String("fooDeleteFailed"), }, }, }, nil), mockCloudformation.EXPECT().DeleteStack(gomock.Eq(&cloudformation.DeleteStackInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), RetainResources: []*string{ - aws.String("fooDeleteFailed"), + ptr.String("fooDeleteFailed"), }, })).Return(nil, nil), mockCloudformation.EXPECT().WaitUntilStackDeleteComplete(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil), ) @@ -138,10 +165,9 @@ func TestCloudformationStack_Remove_DeleteInProgress(t *testing.T) { mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl) stack := CloudFormationStack{ - svc: mockCloudformation, - stack: &cloudformation.Stack{ - StackName: aws.String("foobar"), - }, + svc: mockCloudformation, + logger: logrus.NewEntry(logrus.StandardLogger()), + Name: ptr.String("foobar"), settings: &libsettings.Setting{ "DisableDeletionProtection": true, }, @@ -149,17 +175,17 @@ func TestCloudformationStack_Remove_DeleteInProgress(t *testing.T) { gomock.InOrder( mockCloudformation.EXPECT().DescribeStacks(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(&cloudformation.DescribeStacksOutput{ Stacks: []*cloudformation.Stack{ { - StackStatus: aws.String(cloudformation.StackStatusDeleteInProgress), + StackStatus: ptr.String(cloudformation.StackStatusDeleteInProgress), }, }, }, nil), mockCloudformation.EXPECT().WaitUntilStackDeleteComplete(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil), ) @@ -188,10 +214,9 @@ func TestCloudformationStack_Remove_Stack_InCompletedStatus(t *testing.T) { mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl) stack := CloudFormationStack{ - svc: mockCloudformation, - stack: &cloudformation.Stack{ - StackName: aws.String("foobar"), - }, + svc: mockCloudformation, + logger: logrus.NewEntry(logrus.StandardLogger()), + Name: ptr.String("foobar"), settings: &libsettings.Setting{ "DisableDeletionProtection": true, }, @@ -199,21 +224,21 @@ func TestCloudformationStack_Remove_Stack_InCompletedStatus(t *testing.T) { gomock.InOrder( mockCloudformation.EXPECT().DescribeStacks(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(&cloudformation.DescribeStacksOutput{ Stacks: []*cloudformation.Stack{ { - StackStatus: aws.String(stackStatus), + StackStatus: ptr.String(stackStatus), }, }, }, nil), mockCloudformation.EXPECT().DeleteStack(gomock.Eq(&cloudformation.DeleteStackInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil, nil), mockCloudformation.EXPECT().WaitUntilStackDeleteComplete(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil), ) @@ -238,10 +263,9 @@ func TestCloudformationStack_Remove_Stack_CreateInProgress(t *testing.T) { mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl) stack := CloudFormationStack{ - svc: mockCloudformation, - stack: &cloudformation.Stack{ - StackName: aws.String("foobar"), - }, + svc: mockCloudformation, + logger: logrus.NewEntry(logrus.StandardLogger()), + Name: ptr.String("foobar"), settings: &libsettings.Setting{ "DisableDeletionProtection": true, }, @@ -249,25 +273,25 @@ func TestCloudformationStack_Remove_Stack_CreateInProgress(t *testing.T) { gomock.InOrder( mockCloudformation.EXPECT().DescribeStacks(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(&cloudformation.DescribeStacksOutput{ Stacks: []*cloudformation.Stack{ { - StackStatus: aws.String(stackStatus), + StackStatus: ptr.String(stackStatus), }, }, }, nil), mockCloudformation.EXPECT().WaitUntilStackCreateComplete(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil), mockCloudformation.EXPECT().DeleteStack(gomock.Eq(&cloudformation.DeleteStackInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil, nil), mockCloudformation.EXPECT().WaitUntilStackDeleteComplete(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil), ) @@ -293,10 +317,9 @@ func TestCloudformationStack_Remove_Stack_UpdateInProgress(t *testing.T) { mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl) stack := CloudFormationStack{ - svc: mockCloudformation, - stack: &cloudformation.Stack{ - StackName: aws.String("foobar"), - }, + svc: mockCloudformation, + logger: logrus.NewEntry(logrus.StandardLogger()), + Name: ptr.String("foobar"), settings: &libsettings.Setting{ "DisableDeletionProtection": true, }, @@ -304,25 +327,25 @@ func TestCloudformationStack_Remove_Stack_UpdateInProgress(t *testing.T) { gomock.InOrder( mockCloudformation.EXPECT().DescribeStacks(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(&cloudformation.DescribeStacksOutput{ Stacks: []*cloudformation.Stack{ { - StackStatus: aws.String(stackStatus), + StackStatus: ptr.String(stackStatus), }, }, }, nil), mockCloudformation.EXPECT().WaitUntilStackUpdateComplete(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil), mockCloudformation.EXPECT().DeleteStack(gomock.Eq(&cloudformation.DeleteStackInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil, nil), mockCloudformation.EXPECT().WaitUntilStackDeleteComplete(gomock.Eq(&cloudformation.DescribeStacksInput{ - StackName: aws.String("foobar"), + StackName: ptr.String("foobar"), })).Return(nil), ) diff --git a/resources/cloudwatchevents-targets.go b/resources/cloudwatchevents-target.go similarity index 66% rename from resources/cloudwatchevents-targets.go rename to resources/cloudwatchevents-target.go index 932b4368..c8918b66 100644 --- a/resources/cloudwatchevents-targets.go +++ b/resources/cloudwatchevents-target.go @@ -2,14 +2,15 @@ package resources import ( "context" - "fmt" - "github.com/aws/aws-sdk-go/aws" + "github.com/gotidy/ptr" + "github.com/aws/aws-sdk-go/service/cloudwatchevents" "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" ) @@ -56,9 +57,9 @@ func (l *CloudWatchEventsTargetLister) List(_ context.Context, o interface{}) ([ for _, target := range targetResp.Targets { resources = append(resources, &CloudWatchEventsTarget{ svc: svc, - ruleName: rule.Name, - targetID: target.Id, - busName: bus.Name, + Name: rule.Name, + TargetID: target.Id, + BusName: bus.Name, }) } } @@ -69,24 +70,27 @@ func (l *CloudWatchEventsTargetLister) List(_ context.Context, o interface{}) ([ type CloudWatchEventsTarget struct { svc *cloudwatchevents.CloudWatchEvents - targetID *string - ruleName *string - busName *string + TargetID *string `description:"The ID of the target for the rule"` + Name *string `description:"The name of the rule"` + BusName *string `description:"The name of the event bus the rule applies to"` } -func (target *CloudWatchEventsTarget) Remove(_ context.Context) error { - ids := []*string{target.targetID} - _, err := target.svc.RemoveTargets(&cloudwatchevents.RemoveTargetsInput{ +func (r *CloudWatchEventsTarget) Remove(_ context.Context) error { + ids := []*string{r.TargetID} + _, err := r.svc.RemoveTargets(&cloudwatchevents.RemoveTargetsInput{ Ids: ids, - Rule: target.ruleName, - EventBusName: target.busName, - Force: aws.Bool(true), + Rule: r.Name, + EventBusName: r.BusName, + Force: ptr.Bool(true), }) return err } -func (target *CloudWatchEventsTarget) String() string { +func (r *CloudWatchEventsTarget) String() string { // TODO: change this to IAM format rule -> target and mark as breaking change for filters - // TODO: add properties for rule and target separately - return fmt.Sprintf("Rule: %s Target ID: %s", *target.ruleName, *target.targetID) + return fmt.Sprintf("Rule: %s Target ID: %s", *r.Name, *r.TargetID) +} + +func (r *CloudWatchEventsTarget) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) } diff --git a/resources/iam-account-setting-password-policy.go b/resources/iam-account-setting-password-policy.go index 0e555db8..46af99ab 100644 --- a/resources/iam-account-setting-password-policy.go +++ b/resources/iam-account-setting-password-policy.go @@ -2,7 +2,6 @@ package resources import ( "context" - "errors" "github.com/aws/aws-sdk-go/aws/awserr" @@ -11,6 +10,7 @@ import ( "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" ) @@ -64,8 +64,8 @@ type IAMAccountSettingPasswordPolicy struct { policy *iam.PasswordPolicy } -func (e *IAMAccountSettingPasswordPolicy) Remove(_ context.Context) error { - _, err := e.svc.DeleteAccountPasswordPolicy(&iam.DeleteAccountPasswordPolicyInput{}) +func (r *IAMAccountSettingPasswordPolicy) Remove(_ context.Context) error { + _, err := r.svc.DeleteAccountPasswordPolicy(&iam.DeleteAccountPasswordPolicyInput{}) if err != nil { return err } @@ -73,6 +73,10 @@ func (e *IAMAccountSettingPasswordPolicy) Remove(_ context.Context) error { return nil } -func (e *IAMAccountSettingPasswordPolicy) String() string { +func (r *IAMAccountSettingPasswordPolicy) String() string { return "custom" } + +func (r *IAMAccountSettingPasswordPolicy) Properties() types.Properties { + return types.NewProperties().Set("type", "custom") +} diff --git a/resources/mobile-projects.go b/resources/mobile-projects.go deleted file mode 100644 index 48b5a763..00000000 --- a/resources/mobile-projects.go +++ /dev/null @@ -1,77 +0,0 @@ -package resources - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/mobile" - - "github.com/ekristen/libnuke/pkg/registry" - "github.com/ekristen/libnuke/pkg/resource" - - "github.com/ekristen/aws-nuke/v3/pkg/nuke" -) - -const MobileProjectResource = "MobileProject" - -func init() { - registry.Register(®istry.Registration{ - Name: MobileProjectResource, - Scope: nuke.Account, - Resource: &MobileProject{}, - Lister: &MobileProjectLister{}, - }) -} - -type MobileProjectLister struct{} - -func (l *MobileProjectLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) { - opts := o.(*nuke.ListerOpts) - - svc := mobile.New(opts.Session) - svc.ClientInfo.SigningName = "AWSMobileHubService" - resources := make([]resource.Resource, 0) - - params := &mobile.ListProjectsInput{ - MaxResults: aws.Int64(100), - } - - for { - output, err := svc.ListProjects(params) - if err != nil { - return nil, err - } - - for _, project := range output.Projects { - resources = append(resources, &MobileProject{ - svc: svc, - projectID: project.ProjectId, - }) - } - - if output.NextToken == nil { - break - } - - params.NextToken = output.NextToken - } - - return resources, nil -} - -type MobileProject struct { - svc *mobile.Mobile - projectID *string -} - -func (f *MobileProject) Remove(_ context.Context) error { - _, err := f.svc.DeleteProject(&mobile.DeleteProjectInput{ - ProjectId: f.projectID, - }) - - return err -} - -func (f *MobileProject) String() string { - return *f.projectID -} diff --git a/resources/s3-access-grants-grant.go b/resources/s3-access-grants-grant.go new file mode 100644 index 00000000..bc5798df --- /dev/null +++ b/resources/s3-access-grants-grant.go @@ -0,0 +1,87 @@ +package resources + +import ( + "context" + "strings" + "time" + + "github.com/gotidy/ptr" + + "github.com/aws/aws-sdk-go-v2/service/s3control" + + "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 S3AccessGrantsGrantResource = "S3AccessGrantsGrant" + +func init() { + registry.Register(®istry.Registration{ + Name: S3AccessGrantsGrantResource, + Scope: nuke.Account, + Resource: &S3AccessGrantsGrant{}, + Lister: &S3AccessGrantsGrantLister{}, + }) +} + +type S3AccessGrantsGrantLister struct{} + +func (l *S3AccessGrantsGrantLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + svc := s3control.NewFromConfig(*opts.Config) + var resources []resource.Resource + + res, err := svc.ListAccessGrants(ctx, &s3control.ListAccessGrantsInput{ + AccountId: opts.AccountID, + }) + if err != nil { + if strings.Contains(err.Error(), "AccessGrantsInstanceNotExistsError") { + return resources, nil + } else { + return nil, err + } + } + + for _, p := range res.AccessGrantsList { + resources = append(resources, &S3AccessGrantsGrant{ + svc: svc, + accountID: opts.AccountID, + ID: p.AccessGrantId, + GrantScope: p.GrantScope, + GranteeType: ptr.String(string(p.Grantee.GranteeType)), + GranteeID: p.Grantee.GranteeIdentifier, + CreatedAt: p.CreatedAt, + }) + } + + return resources, nil +} + +type S3AccessGrantsGrant struct { + svc *s3control.Client + accountID *string + ID *string `description:"The ID of the access grant."` + GrantScope *string `description:"The scope of the access grant."` + GranteeType *string `description:"The type of the grantee, (e.g. IAM)."` + GranteeID *string `description:"The ARN of the grantee."` + CreatedAt *time.Time `description:"The date and time the access grant was created."` +} + +func (r *S3AccessGrantsGrant) Remove(ctx context.Context) error { + _, err := r.svc.DeleteAccessGrant(ctx, &s3control.DeleteAccessGrantInput{ + AccountId: r.accountID, + AccessGrantId: r.ID, + }) + return err +} + +func (r *S3AccessGrantsGrant) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *S3AccessGrantsGrant) String() string { + return *r.ID +} diff --git a/resources/s3-access-grants-instance.go b/resources/s3-access-grants-instance.go new file mode 100644 index 00000000..05e9b0d8 --- /dev/null +++ b/resources/s3-access-grants-instance.go @@ -0,0 +1,73 @@ +package resources + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/service/s3control" + + "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 S3AccessGrantsInstanceResource = "S3AccessGrantsInstance" + +func init() { + registry.Register(®istry.Registration{ + Name: S3AccessGrantsInstanceResource, + Scope: nuke.Account, + Resource: &S3AccessGrantsInstance{}, + Lister: &S3AccessGrantsInstanceLister{}, + }) +} + +type S3AccessGrantsInstanceLister struct{} + +func (l *S3AccessGrantsInstanceLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + svc := s3control.NewFromConfig(*opts.Config) + var resources []resource.Resource + + res, err := svc.ListAccessGrantsInstances(ctx, &s3control.ListAccessGrantsInstancesInput{ + AccountId: opts.AccountID, + }) + if err != nil { + return nil, err + } + + for _, entity := range res.AccessGrantsInstancesList { + resources = append(resources, &S3AccessGrantsInstance{ + svc: svc, + accountID: opts.AccountID, + ID: entity.AccessGrantsInstanceId, + CreatedAt: entity.CreatedAt, + }) + } + + return resources, nil +} + +type S3AccessGrantsInstance struct { + svc *s3control.Client + accountID *string + ID *string `description:"The ID of the access grants instance."` + CreatedAt *time.Time `description:"The time the access grants instance was created."` +} + +func (r *S3AccessGrantsInstance) Remove(ctx context.Context) error { + _, err := r.svc.DeleteAccessGrantsInstance(ctx, &s3control.DeleteAccessGrantsInstanceInput{ + AccountId: r.accountID, + }) + return err +} + +func (r *S3AccessGrantsInstance) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *S3AccessGrantsInstance) String() string { + return *r.ID +} diff --git a/resources/s3-access-grants-location.go b/resources/s3-access-grants-location.go new file mode 100644 index 00000000..c8ac0186 --- /dev/null +++ b/resources/s3-access-grants-location.go @@ -0,0 +1,81 @@ +package resources + +import ( + "context" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/service/s3control" + + "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 S3AccessGrantsLocationResource = "S3AccessGrantsLocation" + +func init() { + registry.Register(®istry.Registration{ + Name: S3AccessGrantsLocationResource, + Scope: nuke.Account, + Resource: &S3AccessGrantsLocation{}, + Lister: &S3AccessGrantsLocationLister{}, + }) +} + +type S3AccessGrantsLocationLister struct{} + +func (l *S3AccessGrantsLocationLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + svc := s3control.NewFromConfig(*opts.Config) + var resources []resource.Resource + + res, err := svc.ListAccessGrantsLocations(ctx, &s3control.ListAccessGrantsLocationsInput{ + AccountId: opts.AccountID, + }) + if err != nil { + if strings.Contains(err.Error(), "AccessGrantsInstanceNotExistsError") { + return resources, nil + } else { + return nil, err + } + } + + for _, entity := range res.AccessGrantsLocationsList { + resources = append(resources, &S3AccessGrantsLocation{ + svc: svc, + accountID: opts.AccountID, + ID: entity.AccessGrantsLocationId, + LocationScope: entity.LocationScope, + CreatedAt: entity.CreatedAt, + }) + } + + return resources, nil +} + +type S3AccessGrantsLocation struct { + svc *s3control.Client + accountID *string + ID *string `description:"The ID of the access grants location."` + LocationScope *string `description:"The scope of the access grants location."` + CreatedAt *time.Time `description:"The time the access grants location was created."` +} + +func (r *S3AccessGrantsLocation) Remove(ctx context.Context) error { + _, err := r.svc.DeleteAccessGrantsLocation(ctx, &s3control.DeleteAccessGrantsLocationInput{ + AccessGrantsLocationId: r.ID, + AccountId: r.accountID, + }) + return err +} + +func (r *S3AccessGrantsLocation) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *S3AccessGrantsLocation) String() string { + return *r.ID +} diff --git a/resources/transfer-web-app.go b/resources/transfer-web-app.go new file mode 100644 index 00000000..37fa19dd --- /dev/null +++ b/resources/transfer-web-app.go @@ -0,0 +1,66 @@ +package resources + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/transfer" + + "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 TransferWebAppResource = "TransferWebApp" + +func init() { + registry.Register(®istry.Registration{ + Name: TransferWebAppResource, + Scope: nuke.Account, + Resource: &TransferWebApp{}, + Lister: &TransferWebAppLister{}, + }) +} + +type TransferWebAppLister struct{} + +func (l *TransferWebAppLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + svc := transfer.NewFromConfig(*opts.Config) + var resources []resource.Resource + + res, err := svc.ListWebApps(ctx, &transfer.ListWebAppsInput{}) + if err != nil { + return nil, err + } + + for _, entry := range res.WebApps { + resources = append(resources, &TransferWebApp{ + svc: svc, + ID: entry.WebAppId, + }) + } + + return resources, nil +} + +type TransferWebApp struct { + svc *transfer.Client + ID *string +} + +func (r *TransferWebApp) Remove(ctx context.Context) error { + _, err := r.svc.DeleteWebApp(ctx, &transfer.DeleteWebAppInput{ + WebAppId: r.ID, + }) + return err +} + +func (r *TransferWebApp) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *TransferWebApp) String() string { + return *r.ID +} diff --git a/tools/create-resource/main.go b/tools/create-resource/main.go index a2c5e6de..bae0c114 100644 --- a/tools/create-resource/main.go +++ b/tools/create-resource/main.go @@ -9,6 +9,8 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" + + "github.com/iancoleman/strcase" ) const resourceTemplate = `package resources @@ -38,7 +40,7 @@ func init() { type {{.Combined}}Lister struct{} -func (l *{{.Combined}}Lister) List(_ context.Context, o interface{}) ([]resource.Resource, error) { +func (l *{{.Combined}}Lister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { opts := o.(*nuke.ListerOpts) svc := {{.Service}}.NewFromConfig(*opts.Config) var resources []resource.Resource @@ -50,10 +52,10 @@ func (l *{{.Combined}}Lister) List(_ context.Context, o interface{}) ([]resource return nil, err } - for _, p := range res.{{.ResourceTypeTitle}}s { + for _, p := range res.{{.ResourceTypeTitle}}sList { resources = append(resources, &{{.Combined}}{ svc: svc, - ID: p.Id, + ID: p.{{.ResourceTypeTitle}}Id, Tags: p.Tags, }) } @@ -69,7 +71,7 @@ type {{.Combined}} struct { func (r *{{.Combined}}) Remove(ctx context.Context) error { _, err := r.svc.Delete{{.ResourceTypeTitle}}(ctx, &{{.Service}}.Delete{{.ResourceTypeTitle}}Input{ - {{.ResourceTypeTitle}}Id: r.id, + {{.ResourceTypeTitle}}Id: r.ID, }) return err } @@ -106,8 +108,8 @@ func main() { Service: strings.ToLower(service), ServiceTitle: caser.String(service), ResourceType: resourceType, - ResourceTypeTitle: caser.String(resourceType), - Combined: fmt.Sprintf("%s%s", caser.String(service), caser.String(resourceType)), + ResourceTypeTitle: strcase.ToCamel(resourceType), + Combined: fmt.Sprintf("%s%s", caser.String(service), strcase.ToCamel(resourceType)), } tmpl, err := template.New("resource").Parse(resourceTemplate)