diff --git a/tools/reaper/README.md b/tools/reaper/README.md index c46541c..4eef625 100644 --- a/tools/reaper/README.md +++ b/tools/reaper/README.md @@ -13,6 +13,7 @@ For listing the resources, readonly access to all the resources is needed. - AWS: Use the builtin `AWSResourceGroupsReadOnlyAccess` IAM policy . - Azure: Use the builtin `Reader` IAM role. - GCP: Use the builtin `Cloud Asset Viewer` IAM role. +- aws-nuke: See below for an AWS IAM policy document. For deleting the resources, grant the delete permission for the individual resources. @@ -25,6 +26,77 @@ account with the following permissions to delete integration test resources: - `artifactregistry.repositories.get` - `artifactregistry.repositories.delete` +For [aws-nuke][aws-nuke], a new deleter IAM policy +can be created and assigned to the reaper IAM principal with the following +policy document: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "reaper", + "Effect": "Allow", + "Action": [ + "iam:ListPolicies", + "iam:ListRoles", + "iam:ListOpenIDConnectProviders", + "iam:ListAttachedRolePolicies", + "iam:ListAccountAliases", + "iam:ListRolePolicies", + "iam:GetRole", + "iam:GetPolicy", + "iam:GetOpenIDConnectProvider", + "ec2:DescribeAddresses", + "ec2:DescribeInstances", + "ec2:DescribeLaunchTemplates", + "ec2:DescribeNatGateways", + "ec2:DescribeRegions", + "ec2:DescribeSecurityGroups", + "ec2:DescribeInternetGateways", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeVpcs", + "ec2:DescribeVolumes", + "ec2:DescribeSubnets", + "ec2:DescribeRouteTables", + "autoscaling:DescribeAutoScalingGroups", + "eks:ListClusters", + "eks:ListNodegroups", + "eks:DescribeCluster", + "eks:DescribeNodegroup", + "ecr:ListTagsForResource", + "ecr:DescribeRepositories", + "ec2:DeleteSubnet", + "ec2:DeleteRouteTable", + "ec2:DeleteVolume", + "ec2:DeleteTags", + "ec2:DeleteInternetGateway", + "ec2:DetachInternetGateway", + "ec2:RevokeSecurityGroupEgress", + "ec2:RevokeSecurityGroupIngress", + "ec2:DeleteSecurityGroup", + "ec2:DeleteNatGateway", + "ec2:DeleteVpc", + "ec2:ReleaseAddress", + "ec2:DeleteLaunchTemplate", + "ec2:TerminateInstances", + "eks:DeleteCluster", + "eks:DeleteNodegroup", + "iam:ListPolicyVersions", + "iam:DeletePolicyVersion", + "iam:DeletePolicy", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:DeleteOpenIDConnectProvider", + "iam:DeleteRole", + "ecr:DeleteRepository" + ], + "Resource": "*" + } + ] +} +``` + ## Usage Query the resources by providing the cloud provider name(`provider`) and the @@ -68,16 +140,11 @@ The above command would list the resources that are older than 3 days. In order to delete these resources, pass the `-delete` flag. -**NOTE:** Deleting resources is fully supported in Azure and GCP. Due to the -complexity of deleting the resources created in AWS, it's not implemented yet. -The test infrastructure for AWS involves a lot of individual components that -have to be managed independently, compared of Azure and GCP where resources -related to a cluster are related to one another and can be deleted all together. -If and when the complexity of the AWS test infrastructure is simplified, -deleting the resources can be easily implemented similar to the other providers. -Another issue that contributes to it is the stale resources that are reported -when listing resources via the Resource Groups Tagging API which makes it hard -to find out if a resource still exists or has been deleted without describing -the individual resource and checking their status. +**NOTE:** For AWS, unlike the other providers, a third party tool, [aws-nuke][aws-nuke], +is used. The `aws` provider may be removed in the future. It works in a very +limited manner using the Resource Groups Tagging API. The replacement, +`aws-nuke` provider, is capable of listing and deleting the resources properly. Use the `-h` flag to list all the available options. + +[aws-nuke]: https://github.com/ekristen/aws-nuke diff --git a/tools/reaper/go.mod b/tools/reaper/go.mod index 8a0513d..29a2cdd 100644 --- a/tools/reaper/go.mod +++ b/tools/reaper/go.mod @@ -1,13 +1,17 @@ module github.com/fluxcd/test-infra/tools/reaper -go 1.20 +go 1.22 replace github.com/fluxcd/test-infra/tftestenv => ../../tftestenv require ( + github.com/aws/aws-sdk-go v1.53.19 + github.com/ekristen/aws-nuke/v3 v3.2.2 + github.com/ekristen/libnuke v0.17.1 github.com/fluxcd/test-infra/tftestenv v0.0.0 github.com/k1LoW/duration v1.2.0 github.com/onsi/gomega v1.18.1 + github.com/sirupsen/logrus v1.9.3 ) require ( @@ -22,6 +26,7 @@ require ( github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/emicklei/go-restful v2.9.5+incompatible // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/fatih/color v1.17.0 // indirect github.com/go-logr/logr v1.2.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect @@ -31,6 +36,8 @@ require ( github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-containerregistry v0.11.0 // indirect github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gotidy/ptr v1.4.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -39,10 +46,14 @@ require ( github.com/hashicorp/terraform-exec v0.18.1 // indirect github.com/hashicorp/terraform-json v0.15.0 // indirect github.com/imdario/mergo v0.3.12 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.8 // indirect github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -50,24 +61,24 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stevenle/topsort v0.2.0 // indirect github.com/vbatts/tar-split v0.11.2 // indirect github.com/zclconf/go-cty v1.13.0 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/mod v0.7.0 // indirect - golang.org/x/net v0.5.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 // indirect - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect - golang.org/x/sys v0.4.0 // indirect - golang.org/x/term v0.4.0 // indirect - golang.org/x/text v0.6.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.24.1 // indirect k8s.io/apimachinery v0.24.1 // indirect k8s.io/client-go v0.24.1 // indirect diff --git a/tools/reaper/go.sum b/tools/reaper/go.sum index b8be87b..d27b227 100644 --- a/tools/reaper/go.sum +++ b/tools/reaper/go.sum @@ -73,6 +73,7 @@ github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFP github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= @@ -111,6 +112,8 @@ github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde/go.mod h1:oG9D github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.53.19 h1:WEuWc918RXlIaPCyU11F7hH9H1ItK+8m2c/uoQNRUok= +github.com/aws/aws-sdk-go v1.53.19/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -127,6 +130,7 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -173,6 +177,10 @@ github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/ekristen/aws-nuke/v3 v3.2.2 h1:NW1s/7aKsbVBxy534MezvcZhdGNMZ8K55skvlZ/88JQ= +github.com/ekristen/aws-nuke/v3 v3.2.2/go.mod h1:673wD0fvEiazQd9mWP+rHtPqANogIVrvs9pcuAPGH10= +github.com/ekristen/libnuke v0.17.1 h1:bEroAfJ18eEKq+1B6n1QSJi9NvwIu9RFUxwOrjwn1xI= +github.com/ekristen/libnuke v0.17.1/go.mod h1:riI1tjCf6r+et/9oUBd1vQeFmn2Sn6UeFUR0nWkMeYw= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= @@ -196,6 +204,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -230,6 +240,7 @@ github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.0 h1:n4JnPI1T3Qq1SFEi/F8rwLrZERp2bso19PJZDB9dayk= +github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -281,6 +292,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -365,6 +377,8 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -389,6 +403,8 @@ github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5/g github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= +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/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -464,7 +480,9 @@ github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSn github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +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= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -506,8 +524,9 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -542,6 +561,8 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -549,6 +570,9 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -557,6 +581,9 @@ github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOq github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4 h1:NK3O7S5FRD/wj7ORQ5C3Mx1STpyEMuFe+/F0Lakd1Nk= +github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4/go.mod h1:FqD3ES5hx6zpzDainDaHgkTIqrPaI9uX4CVWqYZoQjY= github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= github.com/mgechev/revive v1.1.2/go.mod h1:bnXsMr+ZTH09V5rssEI+jHAZ4z+ZdyhgO/zsy3EhK+0= @@ -623,6 +650,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= @@ -664,6 +692,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -672,11 +701,13 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA= github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= @@ -691,6 +722,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= @@ -705,6 +738,7 @@ github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdk github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= github.com/shirou/gopsutil/v3 v3.21.10/go.mod h1:t75NhzCZ/dYyPQjyQmrAYP6c8+LCdFANeBMdLPCNnew= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -717,8 +751,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sivchari/tenv v1.4.7/go.mod h1:5nF+bITvkebQVanjU6IuMbvIot/7ReNsUV7I5NbprB0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -745,6 +779,8 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stevenle/topsort v0.2.0 h1:LLWgtp34HPX6/RBDRS0kElVxGOTzGBLI1lSAa5Lb46k= +github.com/stevenle/topsort v0.2.0/go.mod h1:ck2WG2/ZrOr6dLApQ/5Xrqy5wv3T0qhKYWE7r9tkibc= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -755,8 +791,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ= github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= @@ -843,6 +880,7 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -865,8 +903,9 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -905,8 +944,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -962,8 +1002,9 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -995,8 +1036,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1082,13 +1124,17 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1098,8 +1144,9 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1215,6 +1262,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1391,9 +1439,11 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1405,6 +1455,7 @@ honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= k8s.io/api v0.24.1 h1:BjCMRDcyEYz03joa3K1+rbshwh1Ay6oB53+iUx2H8UY= k8s.io/api v0.24.1/go.mod h1:JhoOvNiLXKTPQ60zh2g0ewpA+bnEYf5q44Flhquh4vQ= k8s.io/apiextensions-apiserver v0.24.0 h1:JfgFqbA8gKJ/uDT++feAqk9jBIwNnL9YGdQvaI9DLtY= +k8s.io/apiextensions-apiserver v0.24.0/go.mod h1:iuVe4aEpe6827lvO6yWQVxiPSpPoSKVjkq+MIdg84cM= k8s.io/apimachinery v0.24.1 h1:ShD4aDxTQKN5zNf8K1RQ2u98ELLdIW7jEnlO9uAMX/I= k8s.io/apimachinery v0.24.1/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/client-go v0.24.1 h1:w1hNdI9PFrzu3OlovVeTnf4oHDt+FJLd9Ndluvnb42E= diff --git a/tools/reaper/internal/libnukemod/aws.go b/tools/reaper/internal/libnukemod/aws.go new file mode 100644 index 0000000..d1fced9 --- /dev/null +++ b/tools/reaper/internal/libnukemod/aws.go @@ -0,0 +1,186 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code in this file uses the Nuke data type defined in nuke.go to provider +// helpers for aws-nuke. +package libnukemod + +import ( + "context" + "fmt" + "os" + "slices" + "strings" + + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/ekristen/aws-nuke/v3/pkg/awsutil" + "github.com/ekristen/aws-nuke/v3/pkg/config" + awsnuke "github.com/ekristen/aws-nuke/v3/pkg/nuke" + _ "github.com/ekristen/aws-nuke/v3/resources" // Register all the AWS resources. + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/scanner" + "github.com/ekristen/libnuke/pkg/types" + "github.com/sirupsen/logrus" +) + +// SetUpLibnukeAWS configures and returns Nuke for AWS. This is based on the +// aws-nuke nuke command. +func SetUpLibnukeAWS(ctx context.Context, accountID string, defaultRegion string, cfg config.Config) (*Nuke, error) { + // Read aws credentials from the environment and validate. + creds := &awsutil.Credentials{} + creds.AccessKeyID = os.Getenv("AWS_ACCESS_KEY_ID") + creds.SecretAccessKey = os.Getenv("AWS_SECRET_ACCESS_KEY") + creds.Profile = os.Getenv("AWS_PROFILE") + creds.SessionToken = os.Getenv("AWS_SESSION_TOKEN") + creds.AssumeRoleArn = os.Getenv("AWS_ASSUME_ROLE_ARN") + creds.RoleSessionName = os.Getenv("AWS_ASSUME_ROLE_SESSION_NAME") + creds.ExternalID = os.Getenv("AWS_ASSUME_ROLE_EXTERNAL_ID") + + if err := creds.Validate(); err != nil { + return nil, err + } + + // Initialize the libnuke configuration based on libnuke config.New() + // manually since we aren't reading the configuration from a file path. + if cfg.Log == nil { + cfg.Log = logrus.WithContext(ctx) + } + cfg.Deprecations = registry.GetDeprecatedResourceTypeMapping() + if err := cfg.ResolveDeprecations(); err != nil { + return nil, err + } + cfg.Blocklist = cfg.ResolveBlocklist() + + if defaultRegion != "" { + awsutil.DefaultRegionID = defaultRegion + + partition, ok := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), defaultRegion) + if !ok { + if cfg.CustomEndpoints.GetRegion(defaultRegion) == nil { + err := fmt.Errorf( + "the custom region '%s' must be specified in the configuration 'endpoints'"+ + " to determine its partition", defaultRegion) + logrus.WithError(err).Errorf("unable to resolve partition for region: %s", defaultRegion) + return nil, err + } + } + + awsutil.DefaultAWSPartitionID = partition.ID() + } + + // Create the AWS account object. + account, err := awsutil.NewAccount(creds, cfg.CustomEndpoints) + if err != nil { + return nil, err + } + + // Get filters for the account. + filters, err := cfg.Filters(account.ID()) + if err != nil { + return nil, err + } + + params := &Parameters{ + // Hide filtered resources. + Quiet: true, + } + + // Instantiate libnuke. + n := New(params, filters, cfg.Settings) + + // TODO: Register version? + n.RegisterValidateHandler(func() error { + return cfg.ValidateAccount(account.ID(), account.Aliases(), true) + }) + + // Get any specific account level configuration + accountConfig := cfg.Accounts[account.ID()] + + // Resolve the resource types to be used for the nuke process based on the parameters, global configuration, and + // account level configuration. + resourceTypes := types.ResolveResourceTypes( + registry.GetNames(), + []types.Collection{ + n.Parameters.Includes, + cfg.ResourceTypes.GetIncludes(), + accountConfig.ResourceTypes.GetIncludes(), + }, + []types.Collection{ + n.Parameters.Excludes, + cfg.ResourceTypes.Excludes, + accountConfig.ResourceTypes.Excludes, + }, + []types.Collection{ + n.Parameters.Alternatives, + cfg.ResourceTypes.GetAlternatives(), + accountConfig.ResourceTypes.GetAlternatives(), + }, + registry.GetAlternativeResourceTypeMapping(), + ) + + // If the user has specified the "all" region, then we need to get the enabled regions for the account + // and use those. Otherwise, we will use the regions that are specified in the configuration. + if slices.Contains(cfg.Regions, "all") { + cfg.Regions = account.Regions() + + logrus.Info( + `"all" detected in region list, only enabled regions and "global" will be used, all others ignored`) + + if len(cfg.Regions) > 1 { + logrus.Warnf(`additional regions defined along with "all", these will be ignored!`) + } + + logrus.Infof("The following regions are enabled for the account (%d total):", len(cfg.Regions)) + + printableRegions := make([]string, 0) + for i, region := range cfg.Regions { + printableRegions = append(printableRegions, region) + if i%6 == 0 { // print 5 regions per line + logrus.Infof("> %s", strings.Join(printableRegions, ", ")) + printableRegions = make([]string, 0) + } else if i == len(cfg.Regions)-1 { + logrus.Infof("> %s", strings.Join(printableRegions, ", ")) + } + } + } + + // Register the scanners for each region that is defined in the configuration. + for _, regionName := range cfg.Regions { + // Step 1 - Create the region object + region := awsnuke.NewRegion(regionName, account.ResourceTypeToServiceType, account.NewSession) + + // Step 2 - Create the scannerActual object + scannerActual := scanner.New(regionName, resourceTypes, &awsnuke.ListerOpts{ + Region: region, + }) + + // Step 3 - Register a mutate function that will be called to modify the lister options for each resource type + // see pkg/nuke/resource.go for the MutateOpts function. Its purpose is to create the proper session for the + // proper region. + regMutateErr := scannerActual.RegisterMutateOptsFunc(awsnuke.MutateOpts) + if regMutateErr != nil { + return nil, regMutateErr + } + + // Step 4 - Register the scannerActual with the nuke object + regScanErr := n.RegisterScanner(awsnuke.Account, scannerActual) + if regScanErr != nil { + return nil, regScanErr + } + } + + return n, nil +} diff --git a/tools/reaper/internal/libnukemod/doc.go b/tools/reaper/internal/libnukemod/doc.go new file mode 100644 index 0000000..afe1e48 --- /dev/null +++ b/tools/reaper/internal/libnukemod/doc.go @@ -0,0 +1,32 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package libnukemod contains copies of code from the libnuke project +// https://github.com/ekristen/libnuke and modifications to it. In order to +// integrate with reaper, the resources observed by libnuke needed to be +// converted into the resource data type of the reaper, so that the list of +// resources can be printed in a coherent manner across all the different +// providers. For this, the Nuke.Run() command, which combines scan and delete, +// had to be split into separate steps. Hence, the mods.go adds Delete() to +// Nuke. +// The Nuke.Scan() function prints all the scanned resources. This breaks the +// reaper interface. Scan() is modified to not print the resources. +// To support the retain-period feature of reaper, aws-nuke needs to understand +// the custom timestamp that test-env uses. Since the default aws-nuke filters +// can't be appended without copying and modifying more code, +// ApplyRetentionFilter() is introduced. This allows applying the filter +// on the items after gathering all the resources and before deleting them. +package libnukemod diff --git a/tools/reaper/internal/libnukemod/mock.go b/tools/reaper/internal/libnukemod/mock.go new file mode 100644 index 0000000..31f4d21 --- /dev/null +++ b/tools/reaper/internal/libnukemod/mock.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package libnukemod + +import ( + "context" + + "github.com/ekristen/libnuke/pkg/types" +) + +type MockResource struct { + ARN string + Tags types.Properties + RemoveError error +} + +func (mr MockResource) Remove(ctx context.Context) error { return mr.RemoveError } +func (mr MockResource) String() string { return mr.ARN } +func (mr MockResource) Properties() types.Properties { return mr.Tags } + +func MockResourceWithTags(arn string, props map[string]string) MockResource { + return MockResource{ + ARN: arn, + Tags: types.Properties(props), + } +} diff --git a/tools/reaper/internal/libnukemod/mods.go b/tools/reaper/internal/libnukemod/mods.go new file mode 100644 index 0000000..ebf2d48 --- /dev/null +++ b/tools/reaper/internal/libnukemod/mods.go @@ -0,0 +1,110 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code in this file extends the Nuke data type defined in nuke.go and adds +// other helpers for using libnuke. +package libnukemod + +import ( + "context" + "fmt" + "time" + + "github.com/ekristen/libnuke/pkg/queue" + "github.com/k1LoW/duration" + + "github.com/fluxcd/test-infra/tftestenv" +) + +// TODO: Combine this and the createdat constant in filter.go. +const createdat = "createdat" + +// ApplyRetentionFilter applies the retention filter on the Nuke queue items +// that are to be removed. It only alters the items that were already selected +// for removal by checking if the retention period applies to them. If an item +// is to be removed but don't contain the createdat tag, it is filtered to not +// be removed. This function reduces the number of items to be removed or keeps +// them the same as before. It never increases the items to be deleted. +func ApplyRetentionFilter(n *Nuke, period string) error { + p, err := duration.Parse(period) + if err != nil { + return err + } + // Subtract period from now in UTC. + now := time.Now().UTC() + date := now.Add(-p) + + // Read the createdat tag from resources and check if they were created + // before date. + for _, item := range n.Queue.Items { + if item.State != queue.ItemStateNew { + continue + } + + prop, err := getCreatedAt(item) + // TODO: Maybe return the error if encountered? + if err != nil || prop == "" { + // The item will be removed but don't contain the createdat tag. + // Update the item state to filtered to avoid deleting them. + item.State = queue.ItemStateFiltered + item.Reason = "filtered by retention-period" + continue + } + + createdat, err := tftestenv.ParseCreatedAtTime(prop) + if err != nil { + return err + } + // If the item was not created before the retention period, filter it + // out to not be removed. + if !createdat.Before(date) { + item.State = queue.ItemStateFiltered + item.Reason = "filtered by retention-period" + } + } + + return nil +} + +// getCreatedAt returns the value of createdat tag if present on the given +// resource item. Some resources have variation in the tag key, it attempts to +// handle them and get the createdat tag value. +func getCreatedAt(item *queue.Item) (string, error) { + // TODO: Maybe base this on the type of resource with switch case. + propPrefix := []string{"tag", "tag:role", "tag:igw"} + for _, pp := range propPrefix { + prop, err := item.GetProperty(fmt.Sprintf("%s:%s", pp, createdat)) + if err != nil { + return "", err + } + if prop != "" { + return prop, nil + } + } + return "", nil +} + +// Delete deletes the resources. This deletes the existing scanned items in +// nuke, skipping a re-scan and summarizes the result of delete. +func (n *Nuke) Delete(ctx context.Context) error { + if err := n.run(ctx); err != nil { + return err + } + fmt.Printf("Nuke complete: %d failed, %d skipped, %d finished.\n\n", + n.Queue.Count(queue.ItemStateFailed), n.Queue.Count(queue.ItemStateFiltered), n.Queue.Count(queue.ItemStateFinished)) + + return nil +} diff --git a/tools/reaper/internal/libnukemod/mods_test.go b/tools/reaper/internal/libnukemod/mods_test.go new file mode 100644 index 0000000..c7cd75d --- /dev/null +++ b/tools/reaper/internal/libnukemod/mods_test.go @@ -0,0 +1,137 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package libnukemod + +import ( + "testing" + "time" + + "github.com/ekristen/libnuke/pkg/queue" + . "github.com/onsi/gomega" + + "github.com/fluxcd/test-infra/tftestenv" +) + +func TestNuke_ApplyRetentionFilter(t *testing.T) { + // Construct relative time to be used in the test cases. + now := time.Now().UTC() + period := "2h" + beforePeriod := now.Add(-time.Hour * (2 + 1)) + afterPeriod := now.Add(-time.Hour) + + fakeRegion := "zz" + + tests := []struct { + name string + inputPeriod string + items []*queue.Item + wantErr bool + wantItemStates []queue.ItemState + }{ + { + name: "old, new, without createdat, filtered, waiting states", + inputPeriod: period, + items: []*queue.Item{ + { + Resource: MockResourceWithTags("a1", map[string]string{"tag:" + createdat: beforePeriod.Format(tftestenv.CreatedAtTimeLayout)}), + State: queue.ItemStateNew, + Owner: fakeRegion, + }, + { + Resource: MockResourceWithTags("a2", map[string]string{"tag:" + createdat: afterPeriod.Format(tftestenv.CreatedAtTimeLayout)}), + State: queue.ItemStateNew, + Owner: fakeRegion, + }, + { + Resource: MockResource{ARN: "a3"}, + State: queue.ItemStateNew, + Owner: fakeRegion, + }, + { + Resource: MockResourceWithTags("a4", map[string]string{"tag:" + createdat: beforePeriod.Format(tftestenv.CreatedAtTimeLayout)}), + State: queue.ItemStateFiltered, + Owner: fakeRegion, + }, + { + Resource: MockResourceWithTags("a5", map[string]string{"tag:" + createdat: beforePeriod.Format(tftestenv.CreatedAtTimeLayout)}), + State: queue.ItemStateWaiting, + Owner: fakeRegion, + }, + { + Resource: MockResourceWithTags("a6", map[string]string{"tag:" + createdat: ""}), + State: queue.ItemStateNew, + Owner: fakeRegion, + }, + { + Resource: MockResourceWithTags("a7", map[string]string{"tag:role:" + createdat: beforePeriod.Format(tftestenv.CreatedAtTimeLayout)}), + State: queue.ItemStateNew, + Owner: fakeRegion, + }, + { + Resource: MockResourceWithTags("a8", map[string]string{"tag:igw:" + createdat: beforePeriod.Format(tftestenv.CreatedAtTimeLayout)}), + State: queue.ItemStateNew, + Owner: fakeRegion, + }, + }, + wantItemStates: []queue.ItemState{ + queue.ItemStateNew, + queue.ItemStateFiltered, + queue.ItemStateFiltered, + queue.ItemStateFiltered, + queue.ItemStateWaiting, + queue.ItemStateFiltered, + queue.ItemStateNew, + queue.ItemStateNew, + }, + }, + { + name: "invalid created at", + inputPeriod: period, + items: []*queue.Item{ + { + Resource: MockResourceWithTags("a2", map[string]string{"tag:" + createdat: "222222"}), + State: queue.ItemStateNew, + Owner: fakeRegion, + }, + }, + wantErr: true, + }, + { + name: "invalid period", + inputPeriod: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + n := &Nuke{ + Queue: &queue.Queue{Items: tt.items}, + } + err := ApplyRetentionFilter(n, tt.inputPeriod) + if (err != nil) != tt.wantErr { + t.Fatalf("ApplyRetentionFilter() error = %v, wantErr %v", err, tt.wantErr) + } + if err == nil { + for i, item := range n.Queue.GetItems() { + g.Expect(item.State).To(Equal(tt.wantItemStates[i])) + } + } + }) + } +} diff --git a/tools/reaper/internal/libnukemod/nuke.go b/tools/reaper/internal/libnukemod/nuke.go new file mode 100644 index 0000000..6460daa --- /dev/null +++ b/tools/reaper/internal/libnukemod/nuke.go @@ -0,0 +1,689 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +The MIT License (MIT) + +Copyright (c) 2024 Erik Kristensen +Copyright (c) 2016 reBuy reCommerce GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +This file is copied from the source at +https://github.com/ekristen/libnuke/blob/v0.15.1/pkg/nuke/nuke.go, modified to +comment out the inline print statements to fit in with reaper's UX. Search for +comments starting with "MODIFICATION:". The Nuke object is extended in mods.go +to allow running resource delete with the available scanned items. This is +needed because we apply a custom filter on the scanned resources and delete only +the remaining resources. Updating this file with upstream should require just +commenting out the inline print statements, unless there's a major change. +*/ + +package libnukemod + +import ( + "context" + "errors" + "fmt" + "io" + "slices" + "time" + + "github.com/sirupsen/logrus" + + liberrors "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/scanner" + libsettings "github.com/ekristen/libnuke/pkg/settings" + + "github.com/ekristen/libnuke/pkg/filter" + "github.com/ekristen/libnuke/pkg/queue" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + "github.com/ekristen/libnuke/pkg/utils" +) + +// ListCache is used to cache the list of resources that are returned from the API. +type ListCache map[string]map[string][]resource.Resource + +// Parameters is a collection of common variables used to configure the before of the Nuke instance. +type Parameters struct { + NoDryRun bool // NoDryRun instructs Run to actually perform the remove function + Force bool // Force instructs Run to proceed without confirmation from user + ForceSleep int // ForceSleep indicates how long of a delay before proceeding with confirmation + Quiet bool // Quiet will hide resources if they have been filtered + MaxWaitRetries int // MaxWaitRetries is the total number of times a resource will be retried during wait state + + // WaitOnDependencies controls whether resources will be removed after their dependencies. It is important to note + // that it does not currently track direct dependencies but instead dependent resources. For example if ResourceA + // depends on ResourceB, all ResourceB has to be in a completed state (removed or failed) before ResourceA will be + // processed + WaitOnDependencies bool + + // Includes is a list of resource types that are to be included during the nuke process. If a resource type is + // listed in both the Includes and Excludes fields then the Excludes field will take precedence. + Includes []string + + // Excludes is a list of resource types that are to be excluded during the nuke process. If a resource type is + // listed in both the Includes and Excludes fields then the Excludes field will take precedence. + Excludes []string + + // Alternatives is a list of resource types that are to be used instead of the default resource. The primary use + // case for this is AWS Cloud Control API resources. + Alternatives []string +} + +type INuke interface { + Run() error + Scan() error + Filter(item *queue.Item) error + HandleQueue() + HandleRemove(item *queue.Item) + HandleWait(item *queue.Item, cache ListCache) +} + +// Nuke is the main struct for the library. It is used to register resource types, scanners, filters and validation +// handlers. +type Nuke struct { + Parameters *Parameters // Parameters is a collection of common variables used to configure the before of the Nuke instance. + Filters filter.Filters // Filters is the collection of filters that will be used to filter resources + Settings *libsettings.Settings // Settings is the collection of settings that will be used to control resource behavior + + ValidateHandlers []func() error + ResourceTypes map[registry.Scope]types.Collection + Scanners map[registry.Scope][]*scanner.Scanner + Queue *queue.Queue // Queue is the queue of resources that will be processed + + scannerHashes []string // scannerHashes is used to track if a scanner has already been registered + prompt func() error // prompt is what is shown to the user for confirmation + version string // version is what is shown at the beginning of a run + log *logrus.Entry // log is the logger that is used for the library + runSleep time.Duration // runSleep is how long to sleep between runs of the queue + + failedCount int // failedCount is used to track how many times we've retried all failed resources + waitingCount int // waitingCount is used to track how many times we've waiting for resources to move states +} + +// New returns an instance of nuke that is properly configured for initial use +func New(params *Parameters, filters filter.Filters, settings *libsettings.Settings) *Nuke { + logger := logrus.New() + logger.SetOutput(io.Discard) + + n := &Nuke{ + Parameters: params, + Filters: filters, + Queue: queue.New(), + Settings: settings, + log: logger.WithField("component", "nuke"), + runSleep: 5 * time.Second, + } + + if n.Settings == nil { + n.Settings = &libsettings.Settings{} + } + + return n +} + +// SetLogger allows the tool instantiating the library to set the logger that is used for the library. It is optional. +func (n *Nuke) SetLogger(logger *logrus.Entry) { + n.log = logger +} + +// SetRunSleep allows the tool instantiating the library to set the sleep duration between runs of the queue. +// It is optional. +func (n *Nuke) SetRunSleep(duration time.Duration) { + n.runSleep = duration +} + +// RegisterVersion allows the tool instantiating the library to register its version so there's consist output +// of the version information across all tools. It is optional. +func (n *Nuke) RegisterVersion(version string) { + n.version = version +} + +// RegisterValidateHandler allows the tool instantiating the library to register a validation handler. It is optional. +func (n *Nuke) RegisterValidateHandler(handler func() error) { + if n.ValidateHandlers == nil { + n.ValidateHandlers = make([]func() error, 0) + } + + n.ValidateHandlers = append(n.ValidateHandlers, handler) +} + +// RegisterResourceTypes is used to register resource types against a scope. A scope is a string that is used to +// group resource types together. For example, you could have a scope of "aws" and register all AWS resource types. +// For Azure, you have to register resources by tenant or subscription or even resource group. +func (n *Nuke) RegisterResourceTypes(scope registry.Scope, resourceTypes ...string) { + if n.ResourceTypes == nil { + n.ResourceTypes = make(map[registry.Scope]types.Collection) + } + + n.ResourceTypes[scope] = append(n.ResourceTypes[scope], resourceTypes...) +} + +// RegisterScanner is used to register a scanner against a scope. A scope is a string that is used to group resource +// types together. A scanner is what is responsible for actually querying the API for resources and adding them to +// the queue for processing. +func (n *Nuke) RegisterScanner(scope registry.Scope, instance *scanner.Scanner) error { + if n.Scanners == nil { + n.Scanners = make(map[registry.Scope][]*scanner.Scanner) + } + + hashString := fmt.Sprintf("%s-%s", scope, instance.Owner) + n.log.Debugf("hash: %s", hashString) + if slices.Contains(n.scannerHashes, hashString) { + return fmt.Errorf("scanner is already registered, you cannot register it twice") + } + + if n.scannerHashes == nil { + n.scannerHashes = make([]string, 0) + } + + n.scannerHashes = append(n.scannerHashes, hashString) + n.Scanners[scope] = append(n.Scanners[scope], instance) + + return nil +} + +// RegisterPrompt is used to register the prompt function that used to prompt the user for input, usually to confirm +// if the nuke process should continue or not. +func (n *Nuke) RegisterPrompt(prompt func() error) { + n.prompt = prompt +} + +// Prompt actually calls the registered prompt function as part of the run +func (n *Nuke) Prompt() error { + if n.prompt != nil { + return n.prompt() + } + + return nil +} + +// Run is the main entry point for the library. It will run the validation handlers, prompt the user, scan for +// resources, filter them and then process them. +func (n *Nuke) Run(ctx context.Context) error { + n.Version() + + if err := n.Validate(); err != nil { + return err + } + + if err := n.Prompt(); err != nil { + return err + } + + if err := n.Scan(ctx); err != nil { + return err + } + + if n.Queue.Count(queue.ItemStateNew) == 0 { + fmt.Println("No resource to delete.") + return nil + } + + if !n.Parameters.NoDryRun { + fmt.Println("The above resources would be deleted with the supplied configuration. Provide --no-dry-run to actually destroy resources.") + return nil + } + + if err := n.Prompt(); err != nil { + return err + } + + if err := n.run(ctx); err != nil { + return err + } + + fmt.Printf("Nuke complete: %d failed, %d skipped, %d finished.\n\n", + n.Queue.Count(queue.ItemStateFailed), n.Queue.Count(queue.ItemStateFiltered), n.Queue.Count(queue.ItemStateFinished)) + + return nil +} + +// handleFailure is used to handle the failure state of resources. It will determine if there have been too many +// failures and exit accordingly, writing to screen the failure state of each resource +func (n *Nuke) handleFailure() error { + // processingCount is used to determine if there are any resources that are not in the failed state + processingCount := n.Queue.Count(queue.ItemStatePending, queue.ItemStatePendingDependency, queue.ItemStateHold, + queue.ItemStateWaiting, queue.ItemStateNew, queue.ItemStateNewDependency) + + // failedCount is used to determine if there are any resources that are in the failed state + failedCount := n.Queue.Count(queue.ItemStateFailed) + + // if there are no resources being processed and there are resources in the failed state, then we enter this + // loop to determine how many times we've tried the failed resources + if processingCount == 0 && failedCount > 0 { + // if failCount is greater than 2, then we are done, print status and return failed error + if n.failedCount >= 2 { + logrus.Errorf("There are resources in failed state, but none are ready for deletion, anymore.") + fmt.Println() + + for _, item := range n.Queue.GetItems() { + if item.GetState() != queue.ItemStateFailed { + continue + } + + item.Print() + logrus.Error(item.GetReason()) + } + + return fmt.Errorf("failed") + } + + n.failedCount++ + } else { + n.failedCount = 0 + } + + return nil +} + +// handleWaiting is used to handle the waiting state of resources. It will determine if there have been too many +// wait retries and exit accordingly. +func (n *Nuke) handleWaiting() error { + // if MaxWaitRetries is set to 0, then we do not need to do anything, we will retry indefinitely + if n.Parameters.MaxWaitRetries == 0 { + return nil + } + + // pendingCount is used to determine if there are any resources that are still in a pending or hold + pendingCount := n.Queue.Count(queue.ItemStateWaiting, queue.ItemStatePending, + queue.ItemStatePendingDependency, queue.ItemStateHold) + + // newCount is used to determine if there are any resources that are still in a new state + newCount := n.Queue.Count(queue.ItemStateNew, queue.ItemStateNewDependency) + + // If MaxWaitRetries is set, then we need to know if all resources have been moved from new to a pending state. + // If there are pending, then we need to know how many times to retry before giving up, otherwise we try + // indefinitely. + if pendingCount > 0 && newCount == 0 { + if n.waitingCount >= n.Parameters.MaxWaitRetries { + return fmt.Errorf("max wait retries of %d exceeded", n.Parameters.MaxWaitRetries) + } + n.waitingCount++ + } else { + n.waitingCount = 0 + } + + return nil +} + +// run handles the processing and loop of the queue of items +func (n *Nuke) run(ctx context.Context) error { + if n.runSleep == 0 { + n.runSleep = 5 * time.Second + } + + for { + // HandleQueue is used to handle the queue of resources. It will iterate over the queue and trigger the + // appropriate handlers based on the state of the resource. + n.HandleQueue(ctx) + + // handleFailure will check to see if we are in a final failure state and should error out and exit + if err := n.handleFailure(); err != nil { + return err + } + + // handleWaiting will check to see if we have waited to long for resources to retry and error and exit + if err := n.handleWaiting(); err != nil { + return err + } + + // unfinishedCount is used to determine if there are any resources that are still in a state + // that is not the finished state + unfinishedCount := n.Queue.Count(queue.ItemStateNew, queue.ItemStateNewDependency, + queue.ItemStatePending, queue.ItemStatePendingDependency, queue.ItemStateFailed, + queue.ItemStateWaiting, queue.ItemStateHold, + ) + + // If there are no resources in the queue that are in a state that is not finished, then we are done + if unfinishedCount == 0 { + break + } + + time.Sleep(n.runSleep) + } + + return nil +} + +// Version prints the version that was registered with the library by the invoking tool. +func (n *Nuke) Version() { + fmt.Println(n.version) +} + +// Validate is used to run the validation handlers that were registered with the library by the invoking tool. +func (n *Nuke) Validate() error { + if n.Parameters.ForceSleep < 3 { + return fmt.Errorf("value for --force-sleep cannot be less than 3 seconds. This is for your own protection") + } + + if err := n.Filters.Validate(); err != nil { + return err + } + + for _, handler := range n.ValidateHandlers { + if err := handler(); err != nil { + return err + } + } + + return nil +} + +// getScanners is used to condense the scanners down to a single list +func (n *Nuke) getScanners() []*scanner.Scanner { + var allScanners []*scanner.Scanner + for _, scanners := range n.Scanners { + allScanners = append(allScanners, scanners...) + } + return allScanners +} + +// runScanner is used to run a scanner and process the items that are returned from the scanner +func (n *Nuke) runScanner(ctx context.Context, resourceScanner *scanner.Scanner, itemQueue *queue.Queue) error { + if err := resourceScanner.Run(ctx); err != nil { + return err + } + + for item := range resourceScanner.Items { + // Experimental Feature + if n.Parameters.WaitOnDependencies { + reg := registry.GetRegistration(item.Type) + if len(reg.DependsOn) > 0 { + item.State = queue.ItemStateNewDependency + } + } + + sGetter, ok := item.Resource.(resource.SettingsGetter) + if ok { + sGetter.Settings(n.Settings.Get(item.Type)) + } + + itemQueue.Items = append(itemQueue.Items, item) + if err := n.Filter(item); err != nil { + return err + } + + // If quiet and filtered, skip printing to screen + if n.Parameters.Quiet && item.State == queue.ItemStateFiltered { + continue + } + + // MODIFICATION: This change is covered by the Apache License. + // item.Print() + } + + return nil +} + +// Scan is used to scan for resources. It will run the scanners that were registered with the library by the invoking +// tool. It will also filter the resources based on the filters that were registered. It will also print the current +// status of the resources. +func (n *Nuke) Scan(ctx context.Context) error { + itemQueue := queue.New() + + scanners := n.getScanners() + + // Iterate over scanners and run them then process their items. + for _, actualScanner := range scanners { + if err := n.runScanner(ctx, actualScanner, itemQueue); err != nil { + return err + } + } + + // MODIFICATION: This change is covered by the Apache License. + // fmt.Printf("Scan complete: %d total, %d nukeable, %d filtered.\n\n", + // itemQueue.Total(), itemQueue.Count(queue.ItemStateNew, queue.ItemStateNewDependency), itemQueue.Count(queue.ItemStateFiltered)) + + n.Queue = itemQueue + + return nil +} + +// Filter is used to filter resources. It will run the filters that were registered with the instance of Nuke +// and set the state of the resource to filtered if it matches the filter. +func (n *Nuke) Filter(item *queue.Item) error { + log := n.log. + WithField("handler", "Filter"). + WithField("type", item.Type) + + if r, ok := item.Resource.(resource.LegacyStringer); ok { + log = log.WithField("item", r.String()) + } + + checker, ok := item.Resource.(resource.Filter) + if ok { + log.Trace("resource had filter function") + err := checker.Filter() + if err != nil { + log.Trace("resource was filtered by resource filter") + item.State = queue.ItemStateFiltered + item.Reason = err.Error() + + // Not returning the error, since it could be because of a failed + // request to the API. We do not want to block the whole nuking, + // because of an issue on AWS side. + return nil + } + } + + itemFilters := n.Filters.Get(item.Type) + if itemFilters == nil { + log.Tracef("no filters found for type: %s", item.Type) + return nil + } + + for _, f := range itemFilters { + log. + WithField("prop", f.Property). + WithField("type", f.Type). + WithField("value", f.Value). + Trace("filter details") + + prop, err := item.GetProperty(f.Property) + if err != nil { + log.WithError(err).Warnf("unable to get property: %s", f.Property) + continue + } + + log.Tracef("property: %s", prop) + + match, err := f.Match(prop) + if err != nil { + return err + } + + log.Tracef("match: %t", match) + + if utils.IsTrue(f.Invert) { + log.WithField("orig", match).WithField("new", !match).Trace("filter inverted") + match = !match + } + + if match { + log.Trace("filter matched") + item.State = queue.ItemStateFiltered + item.Reason = "filtered by config" + return nil + } + } + + return nil +} + +// HandleQueue is used to handle the queue of resources. It will iterate over the queue and trigger the appropriate +// handlers based on the state of the resource. +func (n *Nuke) HandleQueue(ctx context.Context) { + listCache := make(map[string]map[string][]resource.Resource) + + for _, item := range n.Queue.GetItems() { + switch item.GetState() { + case queue.ItemStateNew, queue.ItemStateHold: + n.HandleRemove(ctx, item) + item.Print() + case queue.ItemStateNewDependency, queue.ItemStatePendingDependency: + n.HandleWaitDependency(ctx, item) + item.Print() + case queue.ItemStateFailed: + n.HandleRemove(ctx, item) + n.HandleWait(ctx, item, listCache) + item.Print() + case queue.ItemStatePending: + n.HandleWait(ctx, item, listCache) + item.State = queue.ItemStateWaiting + item.Print() + case queue.ItemStateWaiting: + n.HandleWait(ctx, item, listCache) + item.Print() + } + } + + countWaiting := n.Queue.Count( + queue.ItemStateWaiting, + queue.ItemStatePending, + queue.ItemStatePendingDependency, + queue.ItemStateNewDependency, + queue.ItemStateHold, + ) + countFailed := n.Queue.Count(queue.ItemStateFailed) + countSkipped := n.Queue.Count(queue.ItemStateFiltered) + countFinished := n.Queue.Count(queue.ItemStateFinished) + + fmt.Println() + fmt.Printf("Removal requested: %d waiting, %d failed, %d skipped, %d finished\n\n", + countWaiting, countFailed, countSkipped, countFinished) +} + +// HandleRemove is used to handle the removal of a resource. It will remove the resource and set the state of the +// resource to pending if it was successful or failed if it was not. +func (n *Nuke) HandleRemove(ctx context.Context, item *queue.Item) { + err := item.Resource.Remove(ctx) + if err != nil { + var resErr liberrors.ErrHoldResource + if errors.As(err, &resErr) { + item.State = queue.ItemStateHold + item.Reason = resErr.Error() + return + } + + item.State = queue.ItemStateFailed + item.Reason = err.Error() + return + } + + item.State = queue.ItemStatePending + item.Reason = "" +} + +// HandleWaitDependency is used to handle the waiting of a resource. It will check if the resource has any dependencies +// and if it does, it will check if the dependencies have been removed. If they have, it will trigger the remove handler. +func (n *Nuke) HandleWaitDependency(ctx context.Context, item *queue.Item) { + reg := registry.GetRegistration(item.Type) + depCount := 0 + for _, dep := range reg.DependsOn { + cnt := n.Queue.CountByType(dep, + queue.ItemStateNew, queue.ItemStateNewDependency, + queue.ItemStatePending, queue.ItemStatePendingDependency, + queue.ItemStateWaiting, queue.ItemStateHold) + depCount += cnt + } + + if depCount == 0 { + n.HandleRemove(ctx, item) + return + } + + item.State = queue.ItemStatePendingDependency + item.Reason = fmt.Sprintf("left: %d", depCount) +} + +// HandleWait is used to handle the waiting of a resource. It will check if the resource has been removed. If it has, +// it will set the state of the resource to finished. If it has not, it will set the state of the resource to waiting. +func (n *Nuke) HandleWait(ctx context.Context, item *queue.Item, cache ListCache) { + var err error + + waitHook, hookOk := item.Resource.(resource.HandleWaitHook) + if hookOk { + if hookErr := waitHook.HandleWait(ctx); hookErr != nil { + var waitErr liberrors.ErrWaitResource + if errors.As(hookErr, &waitErr) { + item.State = queue.ItemStateWaiting + return + } + + item.State = queue.ItemStateFailed + item.Reason = hookErr.Error() + return + } + } + + ownerID := item.Owner + _, ok := cache[ownerID] + if !ok { + cache[ownerID] = make(map[string][]resource.Resource) + } + + left, ok := cache[ownerID][item.Type] + if !ok { + left, err = item.List(ctx, item.Opts) + if err != nil { + item.State = queue.ItemStateFailed + item.Reason = err.Error() + return + } + cache[ownerID][item.Type] = left + } + + for _, r := range left { + if !item.Equals(r) { + continue + } + + rSet, okSet := r.(resource.SettingsGetter) + if okSet { + rSet.Settings(n.Settings.Get(item.Type)) + } + + checker, filterOk := r.(resource.Filter) + if filterOk { + if filterErr := checker.Filter(); filterErr != nil { + break + } + } + + return + } + + item.State = queue.ItemStateFinished + item.Reason = "" +} diff --git a/tools/reaper/libnuke.go b/tools/reaper/libnuke.go new file mode 100644 index 0000000..f3f1601 --- /dev/null +++ b/tools/reaper/libnuke.go @@ -0,0 +1,188 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "strings" + + awsnukeconfig "github.com/ekristen/aws-nuke/v3/pkg/config" + libnukeconfig "github.com/ekristen/libnuke/pkg/config" + "github.com/ekristen/libnuke/pkg/filter" + "github.com/ekristen/libnuke/pkg/queue" + libnukeresource "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/fluxcd/test-infra/tftestenv" + "github.com/fluxcd/test-infra/tools/reaper/internal/libnukemod" +) + +// getAWSAccountID returns the AWS account ID of the target aws account. +func getAWSAccountID(ctx context.Context, cliPath string) (string, error) { + output, err := tftestenv.RunCommandWithOutput(ctx, "./", + fmt.Sprintf(`%s sts get-caller-identity --query "Account" --output text`, cliPath), + tftestenv.RunCommandOptions{StdoutOnly: true}, + ) + if err != nil { + return "", fmt.Errorf("%s execution failed to get account ID: %w", cliPath, err) + } + id := strings.TrimSpace(string(output)) + if len(id) == 0 { + return "", fmt.Errorf("could not get aws account ID") + } + return id, nil +} + +// getLibNukeAWSConfig returns the aws-nuke configuration to be used against flux +// test-infra. +func getLibNukeAWSConfig(accountID string, regions []string) *libnukeconfig.Config { + nukeRegions := []string{"global"} + nukeRegions = append(nukeRegions, regions...) + + tagFilter := filter.Filter{ + Property: fmt.Sprintf("tag:%s", tagKey), + Value: tagVal, + Invert: "true", + } + tagRoleFilter := filter.Filter{ + Property: fmt.Sprintf("tag:role:%s", tagKey), + Value: tagVal, + Invert: "true", + } + tagIGWFilter := filter.Filter{ + Property: fmt.Sprintf("tag:igw:%s", tagKey), + Value: tagVal, + Invert: "true", + } + + return &libnukeconfig.Config{ + Regions: nukeRegions, + // Set a fake account in the blocklist to suppress this validation + // https://github.com/rebuy-de/aws-nuke/blob/v2.25.0/pkg/config/config.go#L121-L125. + // It is a requirement to set a production account ID in blocklist. + Blocklist: []string{"999999999999"}, + Accounts: map[string]*libnukeconfig.Account{ + accountID: { + ResourceTypes: libnukeconfig.ResourceTypes{ + Includes: types.Collection{ + "EC2VPC", + "EC2SecurityGroup", + "EC2LaunchTemplate", + "EC2RouteTable", + "EC2NetworkInterface", + "ECRRepository", + "EC2Volume", + "EKSNodegroup", + "EC2Subnet", + "AutoScalingGroup", + "EC2Address", + "EKSCluster", + "EC2InternetGatewayAttachment", + "EC2InternetGateway", + "EC2Instance", + "EC2NATGateway", + "IAMRole", + "IAMRolePolicy", + "IAMRolePolicyAttachment", + "IAMPolicy", + "IAMOpenIDConnectProvider", + }, + }, + Filters: filter.Filters{ + "EC2VPC": []filter.Filter{tagFilter}, + "EC2SecurityGroup": []filter.Filter{tagFilter}, + "EC2LaunchTemplate": []filter.Filter{tagFilter}, + "EC2RouteTable": []filter.Filter{tagFilter}, + "EC2NetworkInterface": []filter.Filter{tagFilter}, + "ECRRepository": []filter.Filter{tagFilter}, + "EC2Volume": []filter.Filter{tagFilter}, + "EKSNodegroup": []filter.Filter{tagFilter}, + "EC2Subnet": []filter.Filter{tagFilter}, + "AutoScalingGroup": []filter.Filter{tagFilter}, + "EC2Address": []filter.Filter{tagFilter}, + "EKSCluster": []filter.Filter{tagFilter}, + "EC2InternetGatewayAttachment": []filter.Filter{tagIGWFilter}, + "EC2InternetGateway": []filter.Filter{tagFilter}, + "EC2Instance": []filter.Filter{tagFilter}, + "EC2NATGateway": []filter.Filter{tagFilter}, + "IAMRole": []filter.Filter{tagFilter}, + "IAMRolePolicy": []filter.Filter{tagFilter}, + "IAMRolePolicyAttachment": []filter.Filter{tagRoleFilter}, + "IAMPolicy": []filter.Filter{tagFilter}, + "IAMOpenIDConnectProvider": []filter.Filter{tagFilter}, + }, + }, + }, + } +} + +// libnukeAWSScan configures and scans the target aws account with aws-nuke, and +// returns an instance of libnuke Nuke. +func libnukeAWSScan(ctx context.Context, accountID string) (*libnukemod.Nuke, error) { + // Parse the regions and set the default region. + regions := strings.Split(*awsRegions, ",") + // Use the first region as the default. + defaultRegion := regions[0] + + parsedConfig := awsnukeconfig.Config{ + Config: getLibNukeAWSConfig(accountID, regions), + BypassAliasCheckAccounts: []string{accountID}, + // TODO: Maybe set custom endpoints? + } + + n, err := libnukemod.SetUpLibnukeAWS(ctx, accountID, defaultRegion, parsedConfig) + if err != nil { + return nil, err + } + + if err := n.Scan(ctx); err != nil { + return nil, err + } + + return n, nil +} + +// libnukeItemsToResources converts the items which would be removed to resource +// type. +func libnukeItemsToResources(items []*queue.Item) []resource { + resources := []resource{} + + for _, item := range items { + // Only consider items that would be removed. + if item.State != queue.ItemStateNew { + continue + } + + r := resource{} + rString, ok := item.Resource.(libnukeresource.LegacyStringer) + if ok { + r.Name = rString.String() + } + r.Location = item.Owner + r.Type = item.Type + r.Tags = map[string]string{} + rProp, ok := item.Resource.(libnukeresource.PropertyGetter) + if ok { + r.Tags = rProp.Properties() + } + + resources = append(resources, r) + } + + return resources +} diff --git a/tools/reaper/libnuke_test.go b/tools/reaper/libnuke_test.go new file mode 100644 index 0000000..3d318cc --- /dev/null +++ b/tools/reaper/libnuke_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" + + "github.com/ekristen/libnuke/pkg/queue" + . "github.com/onsi/gomega" + + "github.com/fluxcd/test-infra/tools/reaper/internal/libnukemod" +) + +func Test_libnukeItemsToResources(t *testing.T) { + fakeRegion1 := "aa" + fakeRegion2 := "bb" + fakeResourceType := "Foo" + + tests := []struct { + name string + items []*queue.Item + want []resource + }{ + { + name: "only converts the items to be deleted", + items: []*queue.Item{ + { + Resource: libnukemod.MockResource{ARN: "a1"}, + State: queue.ItemStateFailed, + Owner: fakeRegion1, + Type: fakeResourceType, + }, + { + Resource: libnukemod.MockResource{ARN: "a2"}, + State: queue.ItemStateNew, + Owner: fakeRegion1, + Type: fakeResourceType, + }, + { + Resource: libnukemod.MockResourceWithTags("a3", map[string]string{"o": "p"}), + State: queue.ItemStateFiltered, + Owner: fakeRegion1, + Type: fakeResourceType, + }, + { + Resource: libnukemod.MockResource{ARN: "a4"}, + State: queue.ItemStateFinished, + Owner: fakeRegion1, + Type: fakeResourceType, + }, + { + Resource: libnukemod.MockResource{ARN: "a5"}, + State: queue.ItemStatePending, + Owner: fakeRegion1, + Type: fakeResourceType, + }, + { + Resource: libnukemod.MockResource{ARN: "a6"}, + State: queue.ItemStatePending, + Owner: fakeRegion1, + Type: fakeResourceType, + }, + { + Resource: libnukemod.MockResourceWithTags("a7", map[string]string{"m": "n"}), + State: queue.ItemStateNew, + Owner: fakeRegion2, + Type: fakeResourceType, + }, + }, + want: []resource{ + { + Name: "a2", + Type: fakeResourceType, + Location: fakeRegion1, + Tags: nil, + }, + { + Name: "a7", + Type: fakeResourceType, + Location: fakeRegion2, + Tags: map[string]string{"m": "n"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + got := libnukeItemsToResources(tt.items) + g.Expect(got).To(Equal(tt.want)) + }) + } +} diff --git a/tools/reaper/main.go b/tools/reaper/main.go index 3c26b10..113369f 100644 --- a/tools/reaper/main.go +++ b/tools/reaper/main.go @@ -26,12 +26,17 @@ import ( "os/exec" "strings" "time" + + "github.com/ekristen/libnuke/pkg/queue" + + "github.com/fluxcd/test-infra/tools/reaper/internal/libnukemod" ) const ( - aws = "aws" - azure = "azure" - gcp = "gcp" + aws = "aws" + azure = "azure" + gcp = "gcp" + awsnuke = "aws-nuke" ) // registryTypes maps the registry type resources with their resource.Type value @@ -63,9 +68,10 @@ type resource struct { } var ( - supportedProviders = []string{aws, azure, gcp} + supportedProviders = []string{aws, azure, gcp, awsnuke} targetProvider = flag.String("provider", "", fmt.Sprintf("name of the provider %v", supportedProviders)) gcpProject = flag.String("gcpproject", "", "GCP project name") + awsRegions = flag.String("awsregions", "", "Comma separated list of aws regions for aws-nuke (e.g.: us-east-1,us-east-2). The first entry is used as the default region") tags = flag.String("tags", "", "key-value pair of tag to query with. Only single pair supported at present ('environment=dev')") retentionPeriod = flag.String("retention-period", "", "period for which the resources should be retained (e.g.: 1d, 1h)") jsonoutput = flag.Bool("ojson", false, "JSON output") @@ -85,6 +91,8 @@ func main() { // Paths of the cloud provider CLI binaries. var awsPath, azPath, gcloudPath string + var awsNuker *libnukemod.Nuke + t, err := time.ParseDuration(*timeout) if err != nil { log.Fatalf("Failed parsing timeout: %v", err) @@ -150,19 +158,53 @@ func main() { *gcpProject = p } resources, queryErr = getGCPResources(ctx, gcloudPath, jqBinPath) + case awsnuke: + // Get the account ID of the IAM principal using AWS CLI. Since aws-nuke + // can work on multiple accounts, it explicitly needs the target account + // ID. + awsPath, err = exec.LookPath("aws") + if err != nil { + log.Fatalln(err) + } + awsAccountID, err := getAWSAccountID(ctx, awsPath) + if err != nil { + log.Fatalln(err) + } + if *awsRegions == "" { + log.Fatalf("-awsregions flag unset. AWS regions must be set for aws-nuke") + } + + // Query aws resources using aws-nuke. + awsNuker, queryErr = libnukeAWSScan(ctx, awsAccountID) + if queryErr == nil { + resources = libnukeItemsToResources(awsNuker.Queue.GetItems()) + } } if queryErr != nil { log.Fatalf("Query error: %v", queryErr) } - // Print only the result to stdout. + // Apply the retention period filter. if *retentionPeriod != "" && len(resources) > 0 { - resources, err = applyRetentionFilter(resources, *retentionPeriod) - if err != nil { - log.Fatalf("Failed to filter resources with retention-period: %v", err) + switch *targetProvider { + case awsnuke: + if err := libnukemod.ApplyRetentionFilter(awsNuker, *retentionPeriod); err != nil { + log.Fatalf("Failed to filter resources with retention-period: %v", err) + } + // Update the resources if the number of items to be removed has + // changed. + if awsNuker.Queue.Count(queue.ItemStateNew) != len(resources) { + resources = libnukeItemsToResources(awsNuker.Queue.GetItems()) + } + default: + resources, err = applyRetentionFilter(resources, *retentionPeriod) + if err != nil { + log.Fatalf("Failed to filter resources with retention-period: %v", err) + } } } + // Print only the result to stdout. if *jsonoutput { out, err := json.MarshalIndent(resources, "", " ") if err != nil { @@ -204,6 +246,10 @@ func main() { log.Fatalf("Failed to delete cluster: %v", err) } } + case awsnuke: + if err := awsNuker.Delete(ctx); err != nil { + log.Fatalf("Failed to delete resources: %v", err) + } } } else if !*delete && len(resources) > 0 { // Exit with non-zero exit code when resources are found but not