Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix s3 notificationConfiguration #917

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 66 additions & 18 deletions pkg/controller/s3/bucket/notificationConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package bucket

import (
"context"
"sort"

"github.com/aws/smithy-go/document"
"github.com/google/go-cmp/cmp/cmpopts"

"github.com/aws/aws-sdk-go-v2/aws"
awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/crossplane/crossplane-runtime/pkg/meta"
Expand Down Expand Up @@ -54,24 +56,79 @@ func (in *NotificationConfigurationClient) Observe(ctx context.Context, bucket *
return NeedsUpdate, awsclient.Wrap(err, notificationGetFailed)
}

config := bucket.Spec.ForProvider.NotificationConfiguration
status := bucketStatus(config, external)
switch status { // nolint:exhaustive
case Updated, NeedsDeletion:
return status, nil
return IsNotificationConfigurationUpToDate(bucket.Spec.ForProvider.NotificationConfiguration, external)
}

// IsNotificationConfigurationUpToDate determines whether a notification configuration needs to be updated
func IsNotificationConfigurationUpToDate(cr *v1beta1.NotificationConfiguration, external *awss3.GetBucketNotificationConfigurationOutput) (ResourceStatus, error) { // nolint:gocyclo
// Note - aws API treats nil configuration different than empty configuration
// As such, we can't prealloc this due to the API
// If no configuration is defined but there is one in aws, we must delete it
if cr == nil && (len(external.QueueConfigurations) != 0 || len(external.LambdaFunctionConfigurations) != 0 || len(external.TopicConfigurations) != 0) {
return NeedsDeletion, nil
}
// We can't prealloc this for the API but we can to make comparison easier
if cr == nil {
cr = &v1beta1.NotificationConfiguration{
LambdaFunctionConfigurations: []v1beta1.LambdaFunctionConfiguration{},
QueueConfigurations: []v1beta1.QueueConfiguration{},
TopicConfigurations: []v1beta1.TopicConfiguration{},
}
}
// If any of the lengths in aws are different then there is something to delete
if len(cr.LambdaFunctionConfigurations) < len(external.LambdaFunctionConfigurations) || len(cr.QueueConfigurations) < len(external.QueueConfigurations) || len(cr.TopicConfigurations) < len(external.TopicConfigurations) {
return NeedsDeletion, nil
}

generated := GenerateConfiguration(config)
// Convert to a comparable object
generated := GenerateConfiguration(cr)

if cmp.Equal(external.LambdaFunctionConfigurations, generated.LambdaFunctionConfigurations, cmpopts.IgnoreTypes(document.NoSerde{})) &&
cmp.Equal(external.QueueConfigurations, generated.QueueConfigurations, cmpopts.IgnoreTypes(document.NoSerde{})) &&
cmp.Equal(external.TopicConfigurations, generated.TopicConfigurations, cmpopts.IgnoreTypes(document.NoSerde{})) {
// Sort everything
sortLambda(generated.LambdaFunctionConfigurations)
sortLambda(external.LambdaFunctionConfigurations)

sortQueue(generated.QueueConfigurations)
sortQueue(external.QueueConfigurations)

sortTopic(generated.TopicConfigurations)
sortTopic(external.TopicConfigurations)

if cmp.Equal(external.LambdaFunctionConfigurations, generated.LambdaFunctionConfigurations, cmpopts.IgnoreTypes(document.NoSerde{}, types.LambdaFunctionConfiguration{}.Id), cmpopts.EquateEmpty()) &&
cmp.Equal(external.QueueConfigurations, generated.QueueConfigurations, cmpopts.IgnoreTypes(document.NoSerde{}, types.QueueConfiguration{}.Id), cmpopts.EquateEmpty()) &&
cmp.Equal(external.TopicConfigurations, generated.TopicConfigurations, cmpopts.IgnoreTypes(document.NoSerde{}, types.TopicConfiguration{}.Id), cmpopts.EquateEmpty()) {
return Updated, nil
}

return NeedsUpdate, nil
}

func sortLambda(configs []types.LambdaFunctionConfiguration) {
sort.Slice(configs, func(i, j int) bool {
if a, b := configs[i].LambdaFunctionArn, configs[j].LambdaFunctionArn; a != b {
return aws.ToString(a) < aws.ToString(b)
}
return true
})
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is totally fine as is, but for future reference you could also pass in a couple ofcmpopts.SortSlices options, each with a less function for a slice of the relevant type of struct.


func sortQueue(configs []types.QueueConfiguration) {
sort.Slice(configs, func(i, j int) bool {
if a, b := configs[i].QueueArn, configs[j].QueueArn; a != b {
return aws.ToString(a) < aws.ToString(b)
}
return true
})
}

func sortTopic(configs []types.TopicConfiguration) {
sort.Slice(configs, func(i, j int) bool {
if a, b := configs[i].TopicArn, configs[j].TopicArn; a != b {
return aws.ToString(a) < aws.ToString(b)
}
return true
})
}

// GenerateLambdaConfiguration creates []awss3.LambdaFunctionConfiguration from the local NotificationConfiguration
func GenerateLambdaConfiguration(config *v1beta1.NotificationConfiguration) []types.LambdaFunctionConfiguration {
// NOTE(muvaf): We skip prealloc because the behavior of AWS SDK differs when
Expand Down Expand Up @@ -328,12 +385,3 @@ func LateInitializeTopic(external []types.TopicConfiguration, local []v1beta1.To
func emptyConfiguration(external *awss3.GetBucketNotificationConfigurationOutput) bool {
return (external == nil) || (len(external.TopicConfigurations) == 0 && len(external.QueueConfigurations) == 0 && len(external.LambdaFunctionConfigurations) == 0)
}

func bucketStatus(config *v1beta1.NotificationConfiguration, external *awss3.GetBucketNotificationConfigurationOutput) ResourceStatus { // nolint:gocyclo
if config == nil && len(external.QueueConfigurations) == 0 && len(external.LambdaFunctionConfigurations) == 0 && len(external.TopicConfigurations) == 0 {
return Updated
} else if config == nil && (len(external.QueueConfigurations) != 0 || len(external.LambdaFunctionConfigurations) != 0 || len(external.TopicConfigurations) != 0) {
return NeedsDeletion
}
return NeedsUpdate
}
233 changes: 233 additions & 0 deletions pkg/controller/s3/bucket/notificationConfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
. "github.com/onsi/gomega"

"github.com/crossplane/provider-aws/apis/s3/v1beta1"
awsclient "github.com/crossplane/provider-aws/pkg/clients"
Expand Down Expand Up @@ -378,3 +379,235 @@ func TestNotifLateInit(t *testing.T) {
})
}
}

func TestIsNotificationConfigurationUpToDate(t *testing.T) {
type args struct {
cr *v1beta1.NotificationConfiguration
b *s3.GetBucketNotificationConfigurationOutput
}

type want struct {
isUpToDate ResourceStatus
}
cases := map[string]struct {
args
want
}{
"IsUpToDate": {
args: args{
cr: &v1beta1.NotificationConfiguration{
LambdaFunctionConfigurations: []v1beta1.LambdaFunctionConfiguration{{
Events: generateNotificationEvents(),
Filter: generateNotificationFilter(),
ID: &id,
LambdaFunctionArn: lambdaArn,
}},
QueueConfigurations: []v1beta1.QueueConfiguration{{
Events: generateNotificationEvents(),
Filter: generateNotificationFilter(),
ID: &id,
QueueArn: queueArn,
}},
TopicConfigurations: []v1beta1.TopicConfiguration{{
Events: generateNotificationEvents(),
Filter: generateNotificationFilter(),
ID: &id,
TopicArn: &topicArn,
}},
},
b: &s3.GetBucketNotificationConfigurationOutput{
LambdaFunctionConfigurations: []s3types.LambdaFunctionConfiguration{{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: &id,
LambdaFunctionArn: &lambdaArn,
}},
QueueConfigurations: []s3types.QueueConfiguration{{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: &id,
QueueArn: &queueArn,
}},
TopicConfigurations: []s3types.TopicConfiguration{{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: &id,
TopicArn: &topicArn,
}},
},
},
want: want{
isUpToDate: 0,
},
},
"IsUpToDateIgnoreIds": {
args: args{
cr: &v1beta1.NotificationConfiguration{
LambdaFunctionConfigurations: []v1beta1.LambdaFunctionConfiguration{{
Events: generateNotificationEvents(),
Filter: generateNotificationFilter(),
ID: awsclient.String("lambda-id-1"),
LambdaFunctionArn: lambdaArn,
}},
QueueConfigurations: []v1beta1.QueueConfiguration{{
Events: generateNotificationEvents(),
Filter: generateNotificationFilter(),
ID: awsclient.String("queue-id-1"),
QueueArn: queueArn,
}},
TopicConfigurations: []v1beta1.TopicConfiguration{{
Events: generateNotificationEvents(),
Filter: generateNotificationFilter(),
ID: awsclient.String("topic-id-1"),
TopicArn: &topicArn,
}},
},
b: &s3.GetBucketNotificationConfigurationOutput{
LambdaFunctionConfigurations: []s3types.LambdaFunctionConfiguration{{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: awsclient.String("lambda-id-2"),
LambdaFunctionArn: &lambdaArn,
}},
QueueConfigurations: []s3types.QueueConfiguration{{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: awsclient.String("queue-id-2"),
QueueArn: &queueArn,
}},
TopicConfigurations: []s3types.TopicConfiguration{{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: awsclient.String("topic-id-2"),
TopicArn: &topicArn,
}},
},
},
want: want{
isUpToDate: 0,
},
},
"IsUpToDateRulesOutOfOrder": {
args: args{
cr: &v1beta1.NotificationConfiguration{
LambdaFunctionConfigurations: []v1beta1.LambdaFunctionConfiguration{{
Events: generateNotificationEvents(),
Filter: generateNotificationFilter(),
ID: &id,
LambdaFunctionArn: lambdaArn,
},
{
Events: generateNotificationEvents(),
Filter: generateNotificationFilter(),
ID: awsclient.String("test-id-2"),
LambdaFunctionArn: "lambda:321",
}},
},
b: &s3.GetBucketNotificationConfigurationOutput{
LambdaFunctionConfigurations: []s3types.LambdaFunctionConfiguration{
{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: awsclient.String("test-id-2"),
LambdaFunctionArn: awsclient.String("lambda:321"),
},
{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: &id,
LambdaFunctionArn: &lambdaArn,
}},
},
},
want: want{
isUpToDate: 0,
},
},
"IsUpToDateEmpty": {
args: args{
cr: &v1beta1.NotificationConfiguration{},
b: &s3.GetBucketNotificationConfigurationOutput{
LambdaFunctionConfigurations: []s3types.LambdaFunctionConfiguration{},
QueueConfigurations: []s3types.QueueConfiguration{},
TopicConfigurations: []s3types.TopicConfiguration{},
},
},
want: want{
isUpToDate: 0,
},
},
"IsUpToDateNilInput": {
args: args{
cr: nil,
b: &s3.GetBucketNotificationConfigurationOutput{
LambdaFunctionConfigurations: []s3types.LambdaFunctionConfiguration{},
QueueConfigurations: []s3types.QueueConfiguration{},
TopicConfigurations: []s3types.TopicConfiguration{},
},
},
want: want{
isUpToDate: 0,
},
},
"IsUpToDateNilInputNeedsDelete": {
args: args{
cr: nil,
b: &s3.GetBucketNotificationConfigurationOutput{
LambdaFunctionConfigurations: []s3types.LambdaFunctionConfiguration{{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: &id,
LambdaFunctionArn: &lambdaArn,
}},
},
},
want: want{
isUpToDate: 2,
},
},
"IsUpToDateFalseMissing": {
args: args{
cr: &v1beta1.NotificationConfiguration{
LambdaFunctionConfigurations: []v1beta1.LambdaFunctionConfiguration{{
Events: generateNotificationEvents(),
Filter: generateNotificationFilter(),
ID: &id,
LambdaFunctionArn: lambdaArn,
}},
},
b: &s3.GetBucketNotificationConfigurationOutput{},
},
want: want{
isUpToDate: 1,
},
},
"IsUpToDateExtraNeedsDeletion": {
args: args{
cr: &v1beta1.NotificationConfiguration{},
b: &s3.GetBucketNotificationConfigurationOutput{
LambdaFunctionConfigurations: []s3types.LambdaFunctionConfiguration{{
Events: generateNotificationAWSEvents(),
Filter: generateAWSNotificationFilter(),
Id: &id,
LambdaFunctionArn: &lambdaArn,
}},
},
},
want: want{
isUpToDate: 2,
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
g := NewGomegaWithT(t)
actual, err := IsNotificationConfigurationUpToDate(tc.args.cr, tc.args.b)
g.Expect(err).NotTo(HaveOccurred())
Comment on lines +604 to +606
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try to avoid Gomega usage in our tests. I want to include this with the v0.21.1 release today so I plan to merge and quickly follow up to remove this.


if diff := cmp.Diff(tc.want.isUpToDate, actual, test.EquateConditions()); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
})
}
}