diff --git a/README.md b/README.md index f8a3da71..7048e552 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ The currently supported functionality includes: - Inspecting and deleting all GuardDuty Detectors in an AWS Account - Inspecting and deleting all Macie member accounts in an AWS account - as long as those accounts were created by Invitation - and not via AWS Organizations - Inspecting and deleting all SageMaker Notebook Instances in an AWS account +- Inspecting and deleting all Kinesis Streams in an AWS account ### BEWARE! @@ -358,6 +359,9 @@ The following resources support the Config file: Notes: * no configuration options for KMS customer keys, since keys are created with auto-generated identifier +- Kinesis Streams + - Resource type: `kinesis-stream` + - Config key: `KinesisStream` #### Example @@ -466,6 +470,7 @@ To find out what we options are supported in the config file today, consult this | eip | none | ✅ | none | none | | ec2 | none | ✅ | none | none | | eks | none | ✅ | none | none | +| kinesis-stream | none | ✅ | none | none | | acmpca | none | none | none | none | | iam role | none | none | none | none | | sagemaker-notebook-instances| none| ✅ | none | none | diff --git a/aws/aws.go b/aws/aws.go index 6ef7593c..e9fd020c 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -747,6 +747,20 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp } // End SageMaker Notebook Instances + // Kinesis Streams + kinesisStreams := KinesisStreams{} + if IsNukeable(kinesisStreams.ResourceName(), resourceTypes) { + streams, err := getAllKinesisStreams(cloudNukeSession, configObj) + if err != nil { + return nil, errors.WithStackTrace(err) + } + if len(streams) > 0 { + kinesisStreams.Names = awsgo.StringValueSlice(streams) + resourcesInRegion.Resources = append(resourcesInRegion.Resources, kinesisStreams) + } + } + // End Kinesis Streams + if len(resourcesInRegion.Resources) > 0 { account.Resources[region] = resourcesInRegion } @@ -861,6 +875,7 @@ func ListResourceTypes() []string { GuardDuty{}.ResourceName(), MacieMember{}.ResourceName(), SageMakerNotebookInstances{}.ResourceName(), + KinesisStreams{}.ResourceName(), } sort.Strings(resourceTypes) return resourceTypes diff --git a/aws/kinesis_stream.go b/aws/kinesis_stream.go new file mode 100644 index 00000000..a8c6c382 --- /dev/null +++ b/aws/kinesis_stream.go @@ -0,0 +1,122 @@ +package aws + +import ( + "sync" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kinesis" + "github.com/gruntwork-io/cloud-nuke/config" + "github.com/gruntwork-io/cloud-nuke/logging" + "github.com/gruntwork-io/go-commons/errors" + "github.com/hashicorp/go-multierror" +) + +func getAllKinesisStreams(session *session.Session, configObj config.Config) ([]*string, error) { + svc := kinesis.New(session) + + allStreams := []*string{} + err := svc.ListStreamsPages( + &kinesis.ListStreamsInput{}, + func(page *kinesis.ListStreamsOutput, lastPage bool) bool { + for _, streamName := range page.StreamNames { + if shouldIncludeKinesisStream(streamName, configObj) { + allStreams = append(allStreams, streamName) + } + } + return !lastPage + }, + ) + if err != nil { + return nil, errors.WithStackTrace(err) + } + return allStreams, nil +} + +func shouldIncludeKinesisStream(streamName *string, configObj config.Config) bool { + if streamName == nil { + return false + } + + return config.ShouldInclude( + aws.StringValue(streamName), + configObj.KinesisStream.IncludeRule.NamesRegExp, + configObj.KinesisStream.ExcludeRule.NamesRegExp, + ) +} + +func nukeAllKinesisStreams(session *session.Session, identifiers []*string) error { + region := aws.StringValue(session.Config.Region) + svc := kinesis.New(session) + + if len(identifiers) == 0 { + logging.Logger.Infof("No Kinesis Streams to nuke in region: %s", region) + } + + // NOTE: we don't need to do pagination here, because the pagination is handled by the caller to this function, + // based on KinesisStream.MaxBatchSize, however we add a guard here to warn users when the batching fails and + // has a chance of throttling AWS. Since we concurrently make one call for each identifier, we pick 100 for the + // limit here because many APIs in AWS have a limit of 100 requests per second. + if len(identifiers) > 100 { + logging.Logger.Errorf("Nuking too many Kinesis Streams at once (100): halting to avoid hitting AWS API rate limiting") + return TooManyStreamsErr{} + } + + // There is no bulk delete Kinesis Stream API, so we delete the batch of Kinesis Streams concurrently + // using go routines. + logging.Logger.Infof("Deleting Kinesis Streams in region: %s", region) + wg := new(sync.WaitGroup) + wg.Add(len(identifiers)) + errChans := make([]chan error, len(identifiers)) + for i, streamName := range identifiers { + errChans[i] = make(chan error, 1) + go deleteKinesisStreamAsync(wg, errChans[i], svc, streamName, region) + } + wg.Wait() + + // Collect all the errors from the async delete calls into a single error struct. + // NOTE: We ignore OperationAbortedException which is thrown when there is an eventual consistency issue, where + // cloud-nuke picks up a Stream that is already requested to be deleted. + var allErrs *multierror.Error + for _, errChan := range errChans { + if err := <-errChan; err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() != "OperationAbortedException" { + allErrs = multierror.Append(allErrs, err) + } + } + } + finalErr := allErrs.ErrorOrNil() + if finalErr != nil { + return errors.WithStackTrace(finalErr) + } + return nil +} + +func deleteKinesisStreamAsync( + wg *sync.WaitGroup, + errChan chan error, + svc *kinesis.Kinesis, + streamName *string, + region string, +) { + defer wg.Done() + input := &kinesis.DeleteStreamInput{StreamName: streamName} + _, err := svc.DeleteStream(input) + errChan <- err + + streamNameStr := aws.StringValue(streamName) + if err == nil { + logging.Logger.Infof("[OK] Kinesis Stream %s delete in %s", streamNameStr, region) + } else { + logging.Logger.Errorf("[Failed] Error deleting Kinesis Stream %s in %s: %s", streamNameStr, region, err) + } +} + +// Custom errors + +type TooManyStreamsErr struct{} + +func (err TooManyStreamsErr) Error() string { + return "Too many Streams requested at once." +} diff --git a/aws/kinesis_stream_test.go b/aws/kinesis_stream_test.go new file mode 100644 index 00000000..c6bdcab6 --- /dev/null +++ b/aws/kinesis_stream_test.go @@ -0,0 +1,126 @@ +package aws + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kinesis" + "github.com/gruntwork-io/cloud-nuke/config" + "github.com/gruntwork-io/cloud-nuke/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListKinesisStreams(t *testing.T) { + t.Parallel() + + region, err := getRandomRegion() + require.NoError(t, err) + + session, err := session.NewSession(&aws.Config{Region: aws.String(region)}) + require.NoError(t, err) + svc := kinesis.New(session) + + sName := createKinesisStream(t, svc) + defer deleteKinesisStream(t, svc, sName, true) + + sNames, err := getAllKinesisStreams(session, config.Config{}) + require.NoError(t, err) + assert.Contains(t, aws.StringValueSlice(sNames), aws.StringValue(sName)) +} + +func TestNukeKinesisStreamOne(t *testing.T) { + t.Parallel() + + region, err := getRandomRegion() + require.NoError(t, err) + + session, err := session.NewSession(&aws.Config{Region: aws.String(region)}) + require.NoError(t, err) + svc := kinesis.New(session) + + // We ignore errors in the delete call here, because it is intended to be a stop gap in case there is a bug in nuke. + sName := createKinesisStream(t, svc) + defer deleteKinesisStream(t, svc, sName, true) + identifiers := []*string{sName} + + require.NoError( + t, + nukeAllKinesisStreams(session, identifiers), + ) + + assertKinesisStreamsDeleted(t, svc, identifiers) +} + +func TestNukeKinesisStreamMoreThanOne(t *testing.T) { + t.Parallel() + + region, err := getRandomRegion() + require.NoError(t, err) + + session, err := session.NewSession(&aws.Config{Region: aws.String(region)}) + require.NoError(t, err) + svc := kinesis.New(session) + + sNames := []*string{} + for i := 0; i < 3; i++ { + // We ignore errors in the delete call here, because it is intended to be a stop gap in case there is a bug in nuke. + sName := createKinesisStream(t, svc) + defer deleteKinesisStream(t, svc, sName, true) + sNames = append(sNames, sName) + } + + require.NoError( + t, + nukeAllKinesisStreams(session, sNames), + ) + + assertKinesisStreamsDeleted(t, svc, sNames) +} + +func createKinesisStream(t *testing.T, svc *kinesis.Kinesis) *string { + uniqueID := util.UniqueID() + name := fmt.Sprintf("cloud-nuke-test-%s", strings.ToLower(uniqueID)) + + _, err := svc.CreateStream(&kinesis.CreateStreamInput{ + ShardCount: aws.Int64(1), + StreamName: aws.String(name), + }) + require.NoError(t, err) + + // Add an arbitrary sleep to account for eventual consistency + time.Sleep(15 * time.Second) + return &name +} + +func deleteKinesisStream(t *testing.T, svc *kinesis.Kinesis, name *string, checkErr bool) { + _, err := svc.DeleteStream(&kinesis.DeleteStreamInput{ + StreamName: name, + }) + if checkErr { + require.NoError(t, err) + } +} + +func assertKinesisStreamsDeleted(t *testing.T, svc *kinesis.Kinesis, identifiers []*string) { + for _, name := range identifiers { + stream, err := svc.DescribeStream(&kinesis.DescribeStreamInput{ + StreamName: name, + }) + + // There is an error returned, assert it's because the Stream cannot be found because it's + // been deleted. Otherwise assert that the stream status is DELETING. + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() != "ResourceNotFoundException" { + t.Fatalf("Stream %s is not deleted", aws.StringValue(name)) + } + } else { + require.Equal(t, "DELETING", *stream.StreamDescription.StreamStatus) + } + } +} diff --git a/aws/kinesis_stream_types.go b/aws/kinesis_stream_types.go new file mode 100644 index 00000000..7c61946a --- /dev/null +++ b/aws/kinesis_stream_types.go @@ -0,0 +1,38 @@ +package aws + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/gruntwork-io/go-commons/errors" +) + +// KinesisStreams - represents all Kinesis streams +type KinesisStreams struct { + Names []string +} + +// ResourceName - The simple name of the AWS resource +func (k KinesisStreams) ResourceName() string { + return "kinesis-stream" +} + +// ResourceIdentifiers - The names of the Kinesis Streams +func (k KinesisStreams) ResourceIdentifiers() []string { + return k.Names +} + +func (k KinesisStreams) MaxBatchSize() int { + // Tentative batch size to ensure AWS doesn't throttle. Note that Kinesis Streams does not support bulk delete, so + // we will be deleting this many in parallel using go routines. We pick 35 here, which is half of what the AWS web + // console will do. We pick a conservative number here to avoid hitting AWS API rate limits. + return 35 +} + +// Nuke - nuke 'em all!!! +func (k KinesisStreams) Nuke(session *session.Session, identifiers []string) error { + if err := nukeAllKinesisStreams(session, aws.StringSlice(identifiers)); err != nil { + return errors.WithStackTrace(err) + } + + return nil +} diff --git a/config/config.go b/config/config.go index d16c42a1..842ce21e 100644 --- a/config/config.go +++ b/config/config.go @@ -35,6 +35,7 @@ type Config struct { KMSCustomerKeys ResourceType `yaml:"KMSCustomerKeys"` EKSCluster ResourceType `yaml:"EKSCluster"` SageMakerNotebook ResourceType `yaml:"SageMakerNotebook"` + KinesisStream ResourceType `yaml:"KinesisStream"` } type ResourceType struct { diff --git a/config/config_test.go b/config/config_test.go index af0dbcaf..0f5c5df7 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -36,6 +36,7 @@ func emptyConfig() *Config { ResourceType{FilterRule{}, FilterRule{}}, ResourceType{FilterRule{}, FilterRule{}}, ResourceType{FilterRule{}, FilterRule{}}, + ResourceType{FilterRule{}, FilterRule{}}, } } diff --git a/go.mod b/go.mod index eb4e031d..517f75ee 100644 --- a/go.mod +++ b/go.mod @@ -12,11 +12,9 @@ require ( github.com/hashicorp/go-multierror v1.1.0 github.com/pquerna/otp v1.3.0 github.com/sirupsen/logrus v1.6.0 - github.com/stretchr/objx v0.4.0 // indirect github.com/stretchr/testify v1.7.1 github.com/urfave/cli v1.22.4 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bcbc7ee7..56b30443 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,6 @@ github.com/aws/aws-sdk-go v1.27.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.30.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.34.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.38.28/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.42.4 h1:L3gadqlmmdWCDE7aD52l3A5TKVG9jPBHZG1/65x9GVw= -github.com/aws/aws-sdk-go v1.42.4/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.44.46 h1:BsKENvu24eXg7CWQ2wJAjKbDFkGP+hBtxKJIR3UdcB8= github.com/aws/aws-sdk-go v1.44.46/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -404,14 +402,11 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y 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= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -514,8 +509,6 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -568,23 +561,20 @@ golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/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= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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= @@ -689,7 +679,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=