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 }