sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/instancestate/rule_test.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package instancestate
    18  
    19  import (
    20  	"encoding/json"
    21  	"testing"
    22  
    23  	"github.com/aws/aws-sdk-go/aws"
    24  	"github.com/aws/aws-sdk-go/aws/awserr"
    25  	"github.com/aws/aws-sdk-go/service/eventbridge"
    26  	"github.com/aws/aws-sdk-go/service/sqs"
    27  	"github.com/golang/mock/gomock"
    28  	. "github.com/onsi/gomega"
    29  	"github.com/pkg/errors"
    30  
    31  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    32  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/instancestate/mock_eventbridgeiface"
    33  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/instancestate/mock_sqsiface"
    34  )
    35  
    36  func TestReconcileRules(t *testing.T) {
    37  	mockCtrl := gomock.NewController(t)
    38  	defer mockCtrl.Finish()
    39  	ruleName := "test-cluster-ec2-rule"
    40  
    41  	testCases := []struct {
    42  		name                        string
    43  		eventBridgeExpect           func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder)
    44  		postCreateEventBridgeExpect func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder)
    45  		sqsExpect                   func(m *mock_sqsiface.MockSQSAPIMockRecorder)
    46  		expectErr                   bool
    47  	}{
    48  		{
    49  			name: "successfully creates missing rule and target",
    50  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
    51  				m.DescribeRule(gomock.Eq(&eventbridge.DescribeRuleInput{
    52  					Name: aws.String(ruleName),
    53  				})).Return(nil, awserr.New(eventbridge.ErrCodeResourceNotFoundException, "", nil))
    54  				e := &eventPattern{
    55  					Source:     []string{"aws.ec2"},
    56  					DetailType: []string{Ec2StateChangeNotification},
    57  					EventDetail: &eventDetail{
    58  						States: []infrav1.InstanceState{infrav1.InstanceStateShuttingDown, infrav1.InstanceStateTerminated},
    59  					},
    60  				}
    61  				data, err := json.Marshal(e)
    62  				if err != nil {
    63  					t.Fatalf("got an unexpected error: %v", err)
    64  				}
    65  				m.PutRule(gomock.Eq(&eventbridge.PutRuleInput{
    66  					Name:         aws.String(ruleName),
    67  					State:        aws.String(eventbridge.RuleStateDisabled),
    68  					EventPattern: aws.String(string(data)),
    69  				}))
    70  			},
    71  			postCreateEventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
    72  				m.DescribeRule(gomock.Eq(&eventbridge.DescribeRuleInput{
    73  					Name: aws.String(ruleName),
    74  				})).Return(&eventbridge.DescribeRuleOutput{Name: aws.String(ruleName), Arn: aws.String("rule-arn")}, nil)
    75  				m.ListTargetsByRule(&eventbridge.ListTargetsByRuleInput{
    76  					Rule: aws.String(ruleName),
    77  				}).Return(&eventbridge.ListTargetsByRuleOutput{
    78  					Targets: []*eventbridge.Target{{
    79  						Id:  aws.String("another-queue"),
    80  						Arn: aws.String("another-queue-arn"),
    81  					}},
    82  				}, nil)
    83  				m.PutTargets(gomock.Eq(&eventbridge.PutTargetsInput{
    84  					Rule: aws.String(ruleName),
    85  					Targets: []*eventbridge.Target{{
    86  						Arn: aws.String("test-cluster-queue-arn"),
    87  						Id:  aws.String("test-cluster-queue"),
    88  					}},
    89  				}))
    90  			},
    91  			sqsExpect: func(m *mock_sqsiface.MockSQSAPIMockRecorder) {
    92  				m.GetQueueUrl(gomock.Eq(&sqs.GetQueueUrlInput{
    93  					QueueName: aws.String("test-cluster-queue"),
    94  				})).Return(&sqs.GetQueueUrlOutput{QueueUrl: aws.String("test-cluster-queue-url")}, nil)
    95  				attrs := make(map[string]string)
    96  				attrs[sqs.QueueAttributeNameQueueArn] = "test-cluster-queue-arn"
    97  				m.GetQueueAttributes(gomock.Eq(&sqs.GetQueueAttributesInput{
    98  					AttributeNames: aws.StringSlice([]string{sqs.QueueAttributeNameQueueArn, sqs.QueueAttributeNamePolicy}),
    99  					QueueUrl:       aws.String("test-cluster-queue-url"),
   100  				})).Return(&sqs.GetQueueAttributesOutput{Attributes: aws.StringMap(attrs)}, nil)
   101  				m.SetQueueAttributes(gomock.AssignableToTypeOf(&sqs.SetQueueAttributesInput{})).Return(nil, nil)
   102  			},
   103  			expectErr: false,
   104  		},
   105  		{
   106  			name: "skips creating target and queue policy if they already exist",
   107  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   108  				m.DescribeRule(gomock.Eq(&eventbridge.DescribeRuleInput{
   109  					Name: aws.String(ruleName),
   110  				})).Return(&eventbridge.DescribeRuleOutput{Name: aws.String(ruleName), Arn: aws.String("rule-arn")}, nil)
   111  				m.ListTargetsByRule(gomock.AssignableToTypeOf(&eventbridge.ListTargetsByRuleInput{})).Return(&eventbridge.ListTargetsByRuleOutput{
   112  					Targets: []*eventbridge.Target{{
   113  						Id:  aws.String("test-cluster-queue"),
   114  						Arn: aws.String("test-cluster-queue-arn"),
   115  					}},
   116  				}, nil)
   117  			},
   118  			postCreateEventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {},
   119  			sqsExpect: func(m *mock_sqsiface.MockSQSAPIMockRecorder) {
   120  				m.GetQueueUrl(gomock.AssignableToTypeOf(&sqs.GetQueueUrlInput{})).Return(&sqs.GetQueueUrlOutput{QueueUrl: aws.String("test-cluster-queue-url")}, nil)
   121  				attrs := make(map[string]string)
   122  				attrs[sqs.QueueAttributeNameQueueArn] = "test-cluster-queue-arn"
   123  				attrs[sqs.QueueAttributeNamePolicy] = "some policy"
   124  				m.GetQueueAttributes(gomock.AssignableToTypeOf(&sqs.GetQueueAttributesInput{})).Return(&sqs.GetQueueAttributesOutput{Attributes: aws.StringMap(attrs)}, nil)
   125  			},
   126  		},
   127  		{
   128  			name: "returns error if DescribeRule runs into unexpected error",
   129  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   130  				m.DescribeRule(gomock.Eq(&eventbridge.DescribeRuleInput{
   131  					Name: aws.String(ruleName),
   132  				})).Return(nil, errors.New("some error"))
   133  			},
   134  			postCreateEventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {},
   135  			sqsExpect:                   func(m *mock_sqsiface.MockSQSAPIMockRecorder) {},
   136  			expectErr:                   true,
   137  		},
   138  	}
   139  
   140  	for _, tc := range testCases {
   141  		t.Run(tc.name, func(t *testing.T) {
   142  			g := NewWithT(t)
   143  			eventbridgeMock := mock_eventbridgeiface.NewMockEventBridgeAPI(mockCtrl)
   144  			sqsMock := mock_sqsiface.NewMockSQSAPI(mockCtrl)
   145  			clusterScope, err := setupCluster("test-cluster")
   146  			g.Expect(err).To(Not(HaveOccurred()))
   147  			tc.sqsExpect(sqsMock.EXPECT())
   148  			tc.eventBridgeExpect(eventbridgeMock.EXPECT())
   149  			tc.postCreateEventBridgeExpect(eventbridgeMock.EXPECT())
   150  
   151  			s := NewService(clusterScope)
   152  			s.EventBridgeClient = eventbridgeMock
   153  			s.SQSClient = sqsMock
   154  
   155  			err = s.reconcileRules()
   156  			if tc.expectErr {
   157  				g.Expect(err).NotTo(BeNil())
   158  			} else {
   159  				g.Expect(err).To(BeNil())
   160  			}
   161  		})
   162  	}
   163  }
   164  
   165  func TestDeleteRules(t *testing.T) {
   166  	mockCtrl := gomock.NewController(t)
   167  	defer mockCtrl.Finish()
   168  
   169  	testCases := []struct {
   170  		name              string
   171  		eventBridgeExpect func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder)
   172  		expectErr         bool
   173  	}{
   174  		{
   175  			name: "removes target and ec2 rule successfully when they both exist",
   176  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   177  				m.RemoveTargets(gomock.Eq(&eventbridge.RemoveTargetsInput{
   178  					Rule: aws.String("test-cluster-ec2-rule"),
   179  					Ids:  aws.StringSlice([]string{"test-cluster-queue"}),
   180  				})).Return(nil, nil)
   181  				m.DeleteRule(gomock.Eq(&eventbridge.DeleteRuleInput{
   182  					Name: aws.String("test-cluster-ec2-rule"),
   183  				})).Return(nil, nil)
   184  			},
   185  			expectErr: false,
   186  		},
   187  		{
   188  			name: "continues to remove rule when target doesn't exist",
   189  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   190  				m.RemoveTargets(gomock.AssignableToTypeOf(&eventbridge.RemoveTargetsInput{})).
   191  					Return(nil, awserr.New(eventbridge.ErrCodeResourceNotFoundException, "", nil))
   192  				m.DeleteRule(gomock.Eq(&eventbridge.DeleteRuleInput{
   193  					Name: aws.String("test-cluster-ec2-rule"),
   194  				})).Return(nil, nil)
   195  			},
   196  			expectErr: false,
   197  		},
   198  		{
   199  			name: "returns error when remove target fails unexpectedly",
   200  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   201  				m.RemoveTargets(gomock.AssignableToTypeOf(&eventbridge.RemoveTargetsInput{})).Return(nil, errors.New("some error"))
   202  			},
   203  			expectErr: true,
   204  		},
   205  		{
   206  			name: "returns error when delete rule fails unexpectedly",
   207  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   208  				m.RemoveTargets(gomock.AssignableToTypeOf(&eventbridge.RemoveTargetsInput{})).Return(nil, nil)
   209  				m.DeleteRule(gomock.AssignableToTypeOf(&eventbridge.DeleteRuleInput{})).Return(nil, errors.New("some error"))
   210  			},
   211  			expectErr: true,
   212  		},
   213  	}
   214  
   215  	for _, tc := range testCases {
   216  		t.Run(tc.name, func(t *testing.T) {
   217  			g := NewWithT(t)
   218  			eventbridgeMock := mock_eventbridgeiface.NewMockEventBridgeAPI(mockCtrl)
   219  			clusterScope, err := setupCluster("test-cluster")
   220  			g.Expect(err).To(Not(HaveOccurred()))
   221  			tc.eventBridgeExpect(eventbridgeMock.EXPECT())
   222  
   223  			s := NewService(clusterScope)
   224  			s.EventBridgeClient = eventbridgeMock
   225  
   226  			err = s.deleteRules()
   227  			if tc.expectErr {
   228  				g.Expect(err).NotTo(BeNil())
   229  			} else {
   230  				g.Expect(err).To(BeNil())
   231  			}
   232  		})
   233  	}
   234  }
   235  
   236  func TestAddInstanceToRule(t *testing.T) {
   237  	mockCtrl := gomock.NewController(t)
   238  	defer mockCtrl.Finish()
   239  	pattern := eventPattern{
   240  		DetailType: []string{Ec2StateChangeNotification},
   241  		Source:     []string{"aws.ec2"},
   242  		EventDetail: &eventDetail{
   243  			InstanceIDs: []string{"instance-a"},
   244  		},
   245  	}
   246  	patternData, err := json.Marshal(pattern)
   247  	if err != nil {
   248  		t.Fatalf("got an unexpected error: %v", err)
   249  	}
   250  
   251  	testCases := []struct {
   252  		name              string
   253  		eventBridgeExpect func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder)
   254  		newInstanceID     string
   255  		expectErr         bool
   256  	}{
   257  		{
   258  			name: "adds instance to event pattern when it doesn't exist",
   259  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   260  				m.DescribeRule(&eventbridge.DescribeRuleInput{
   261  					Name: aws.String("test-cluster-ec2-rule"),
   262  				}).Return(&eventbridge.DescribeRuleOutput{
   263  					EventPattern: aws.String(string(patternData)),
   264  				}, nil)
   265  				expectedPattern := pattern
   266  				expectedPattern.EventDetail.InstanceIDs = append(expectedPattern.EventDetail.InstanceIDs, "instance-b")
   267  				expectedData, err := json.Marshal(expectedPattern)
   268  				if err != nil {
   269  					t.Fatalf("got an unexpected error: %v", err)
   270  				}
   271  				m.PutRule(&eventbridge.PutRuleInput{
   272  					Name:         aws.String("test-cluster-ec2-rule"),
   273  					EventPattern: aws.String(string(expectedData)),
   274  					State:        aws.String(eventbridge.RuleStateEnabled),
   275  				}).Return(nil, nil)
   276  			},
   277  			newInstanceID: "instance-b",
   278  			expectErr:     false,
   279  		},
   280  		{
   281  			name: "does nothing if instance is already tracked in event pattern",
   282  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   283  				m.DescribeRule(&eventbridge.DescribeRuleInput{
   284  					Name: aws.String("test-cluster-ec2-rule"),
   285  				}).Return(&eventbridge.DescribeRuleOutput{
   286  					EventPattern: aws.String(string(patternData)),
   287  				}, nil)
   288  			},
   289  			newInstanceID: "instance-a",
   290  			expectErr:     false,
   291  		},
   292  	}
   293  
   294  	for _, tc := range testCases {
   295  		t.Run(tc.name, func(t *testing.T) {
   296  			g := NewWithT(t)
   297  			eventbridgeMock := mock_eventbridgeiface.NewMockEventBridgeAPI(mockCtrl)
   298  			clusterScope, err := setupCluster("test-cluster")
   299  			g.Expect(err).To(Not(HaveOccurred()))
   300  			tc.eventBridgeExpect(eventbridgeMock.EXPECT())
   301  
   302  			s := NewService(clusterScope)
   303  			s.EventBridgeClient = eventbridgeMock
   304  
   305  			err = s.AddInstanceToEventPattern(tc.newInstanceID)
   306  			if tc.expectErr {
   307  				g.Expect(err).NotTo(BeNil())
   308  			} else {
   309  				g.Expect(err).To(BeNil())
   310  			}
   311  		})
   312  	}
   313  }
   314  
   315  func TestRemoveInstanceStateFromEventPattern(t *testing.T) {
   316  	mockCtrl := gomock.NewController(t)
   317  	defer mockCtrl.Finish()
   318  	pattern := eventPattern{
   319  		DetailType: []string{Ec2StateChangeNotification},
   320  		Source:     []string{"aws.ec2"},
   321  		EventDetail: &eventDetail{
   322  			InstanceIDs: []string{"instance-a", "instance-b", "instance-c"},
   323  		},
   324  	}
   325  	patternData, err := json.Marshal(pattern)
   326  	if err != nil {
   327  		t.Fatalf("got an unexpected error: %v", err)
   328  	}
   329  
   330  	testCases := []struct {
   331  		name              string
   332  		eventBridgeExpect func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder)
   333  		instanceID        string
   334  	}{
   335  		{
   336  			name: "remove instance from instance IDs and disables rule when no instances are tracked",
   337  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   338  				singleInstanceEventPattern := pattern
   339  				singleInstanceEventPattern.EventDetail.InstanceIDs = []string{"instance-a"}
   340  				patternData, err := json.Marshal(pattern)
   341  				if err != nil {
   342  					t.Fatalf("got an unexpected error: %v", err)
   343  				}
   344  				m.DescribeRule(&eventbridge.DescribeRuleInput{
   345  					Name: aws.String("test-cluster-ec2-rule"),
   346  				}).Return(&eventbridge.DescribeRuleOutput{
   347  					EventPattern: aws.String(string(patternData)),
   348  				}, nil)
   349  				expectedPattern := pattern
   350  				expectedPattern.EventDetail.InstanceIDs = []string{}
   351  				expectedData, err := json.Marshal(expectedPattern)
   352  				if err != nil {
   353  					t.Fatalf("got an unexpected error: %v", err)
   354  				}
   355  
   356  				m.PutRule(&eventbridge.PutRuleInput{
   357  					Name:         aws.String("test-cluster-ec2-rule"),
   358  					EventPattern: aws.String(string(expectedData)),
   359  					State:        aws.String(eventbridge.RuleStateDisabled),
   360  				}).Return(nil, nil)
   361  			},
   362  			instanceID: "instance-a",
   363  		},
   364  		{
   365  			name: "remove instance from instance IDs and rule remains enabled when other instances are tracked",
   366  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   367  				m.DescribeRule(&eventbridge.DescribeRuleInput{
   368  					Name: aws.String("test-cluster-ec2-rule"),
   369  				}).Return(&eventbridge.DescribeRuleOutput{
   370  					EventPattern: aws.String(string(patternData)),
   371  				}, nil)
   372  				expectedPattern := pattern
   373  				expectedPattern.EventDetail.InstanceIDs = []string{"instance-a", "instance-c"}
   374  				expectedData, err := json.Marshal(expectedPattern)
   375  				if err != nil {
   376  					t.Fatalf("got an unexpected error: %v", err)
   377  				}
   378  				m.PutRule(&eventbridge.PutRuleInput{
   379  					Name:         aws.String("test-cluster-ec2-rule"),
   380  					EventPattern: aws.String(string(expectedData)),
   381  					State:        aws.String(eventbridge.RuleStateEnabled),
   382  				}).Return(nil, nil)
   383  			},
   384  			instanceID: "instance-b",
   385  		},
   386  		{
   387  			name: "does nothing when instanceID is not tracked",
   388  			eventBridgeExpect: func(m *mock_eventbridgeiface.MockEventBridgeAPIMockRecorder) {
   389  				m.DescribeRule(&eventbridge.DescribeRuleInput{
   390  					Name: aws.String("test-cluster-ec2-rule"),
   391  				}).Return(&eventbridge.DescribeRuleOutput{
   392  					EventPattern: aws.String(string(patternData)),
   393  				}, nil)
   394  			},
   395  			instanceID: "instance-d",
   396  		},
   397  	}
   398  
   399  	for _, tc := range testCases {
   400  		t.Run(tc.name, func(t *testing.T) {
   401  			g := NewWithT(t)
   402  			eventbridgeMock := mock_eventbridgeiface.NewMockEventBridgeAPI(mockCtrl)
   403  			clusterScope, err := setupCluster("test-cluster")
   404  			g.Expect(err).To(Not(HaveOccurred()))
   405  			tc.eventBridgeExpect(eventbridgeMock.EXPECT())
   406  
   407  			s := NewService(clusterScope)
   408  			s.EventBridgeClient = eventbridgeMock
   409  
   410  			s.RemoveInstanceFromEventPattern(tc.instanceID)
   411  		})
   412  	}
   413  }