sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/instancestate/rule.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 "fmt" 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/pkg/errors" 28 29 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 30 ) 31 32 // Ec2StateChangeNotification defines the EC2 instance's state change notification. 33 const Ec2StateChangeNotification = "EC2 Instance State-change Notification" 34 35 // reconcileRules creates rules and attaches the queue as a target. 36 func (s Service) reconcileRules() error { 37 var ruleNotFound bool 38 ruleResp, err := s.EventBridgeClient.DescribeRule(&eventbridge.DescribeRuleInput{ 39 Name: aws.String(s.getEC2RuleName()), 40 }) 41 if err != nil { 42 if resourceNotFoundError(err) { 43 ruleNotFound = true 44 } else { 45 return errors.Wrapf(err, "unable to describe rule %s", s.getEC2RuleName()) 46 } 47 } 48 49 if ruleNotFound { 50 err = s.createRule() 51 if err != nil { 52 return errors.Wrap(err, "unable to create rule") 53 } 54 // fetch newly created rule 55 ruleResp, err = s.EventBridgeClient.DescribeRule(&eventbridge.DescribeRuleInput{ 56 Name: aws.String(s.getEC2RuleName()), 57 }) 58 59 if err != nil { 60 return errors.Wrapf(err, "unable to describe new rule %s", s.getEC2RuleName()) 61 } 62 } 63 64 queueURLResp, err := s.SQSClient.GetQueueUrl(&sqs.GetQueueUrlInput{ 65 QueueName: aws.String(GenerateQueueName(s.scope.Name())), 66 }) 67 68 if err != nil { 69 return errors.Wrap(err, "unable to get queue URL") 70 } 71 queueAttrs, err := s.SQSClient.GetQueueAttributes(&sqs.GetQueueAttributesInput{ 72 AttributeNames: aws.StringSlice([]string{sqs.QueueAttributeNameQueueArn, sqs.QueueAttributeNamePolicy}), 73 QueueUrl: queueURLResp.QueueUrl, 74 }) 75 76 if err != nil { 77 return errors.Wrap(err, "unable to get queue attributes") 78 } 79 80 targetsResp, err := s.EventBridgeClient.ListTargetsByRule(&eventbridge.ListTargetsByRuleInput{ 81 Rule: aws.String(s.getEC2RuleName()), 82 }) 83 if err != nil { 84 return errors.Wrapf(err, "unable to list targets for rule %s", s.getEC2RuleName()) 85 } 86 87 targetFound := false 88 for _, target := range targetsResp.Targets { 89 // check if queue is already added as a target 90 if *target.Id == GenerateQueueName(s.scope.Name()) && *target.Arn == *queueAttrs.Attributes[sqs.QueueAttributeNameQueueArn] { 91 targetFound = true 92 } 93 } 94 95 if !targetFound { 96 _, err = s.EventBridgeClient.PutTargets(&eventbridge.PutTargetsInput{ 97 Rule: ruleResp.Name, 98 Targets: []*eventbridge.Target{{ 99 Arn: queueAttrs.Attributes[sqs.QueueAttributeNameQueueArn], 100 Id: aws.String(GenerateQueueName(s.scope.Name())), 101 }}, 102 }) 103 104 if err != nil { 105 return errors.Wrapf(err, "unable to add SQS target %s to rule %s", GenerateQueueName(s.scope.Name()), s.getEC2RuleName()) 106 } 107 } 108 109 if queueAttrs.Attributes[sqs.QueueAttributeNamePolicy] == nil { 110 // add a policy for the rule so the rule is authorized to emit messages to the queue 111 err = s.createPolicyForRule(&createPolicyForRuleInput{ 112 QueueArn: *queueAttrs.Attributes[sqs.QueueAttributeNameQueueArn], 113 QueueURL: *queueURLResp.QueueUrl, 114 RuleArn: *ruleResp.Arn, 115 }) 116 if err != nil { 117 return err 118 } 119 } 120 121 return nil 122 } 123 124 func (s Service) createRule() error { 125 eventPattern := eventPattern{ 126 Source: []string{"aws.ec2"}, 127 DetailType: []string{Ec2StateChangeNotification}, 128 EventDetail: &eventDetail{ 129 States: []infrav1.InstanceState{infrav1.InstanceStateShuttingDown, infrav1.InstanceStateTerminated}, 130 }, 131 } 132 data, err := json.Marshal(eventPattern) 133 if err != nil { 134 return err 135 } 136 // create in disabled state so the rule doesn't pick up all EC2 instances. As machines get created, 137 // the rule will get updated to track those machines 138 _, err = s.EventBridgeClient.PutRule(&eventbridge.PutRuleInput{ 139 Name: aws.String(s.getEC2RuleName()), 140 EventPattern: aws.String(string(data)), 141 State: aws.String(eventbridge.RuleStateDisabled), 142 }) 143 144 return err 145 } 146 147 func (s Service) deleteRules() error { 148 _, err := s.EventBridgeClient.RemoveTargets(&eventbridge.RemoveTargetsInput{ 149 Rule: aws.String(s.getEC2RuleName()), 150 Ids: aws.StringSlice([]string{GenerateQueueName(s.scope.Name())}), 151 }) 152 if err != nil && !resourceNotFoundError(err) { 153 return errors.Wrapf(err, "unable to remove target %s for rule %s", GenerateQueueName(s.scope.Name()), s.getEC2RuleName()) 154 } 155 _, err = s.EventBridgeClient.DeleteRule(&eventbridge.DeleteRuleInput{ 156 Name: aws.String(s.getEC2RuleName()), 157 }) 158 159 if err != nil && resourceNotFoundError(err) { 160 return nil 161 } 162 163 return err 164 } 165 166 // AddInstanceToEventPattern will add an instance to an event pattern. 167 func (s Service) AddInstanceToEventPattern(instanceID string) error { 168 ruleResp, err := s.EventBridgeClient.DescribeRule(&eventbridge.DescribeRuleInput{ 169 Name: aws.String(s.getEC2RuleName()), 170 }) 171 if err != nil { 172 return errors.Wrapf(err, "unable to describe rule %s", s.getEC2RuleName()) 173 } 174 e := eventPattern{} 175 err = json.Unmarshal([]byte(*ruleResp.EventPattern), &e) 176 if err != nil { 177 return err 178 } 179 e.DetailType = []string{Ec2StateChangeNotification} 180 181 for _, r := range e.EventDetail.InstanceIDs { 182 if r == instanceID { 183 // instance is already tracked by rule 184 return nil 185 } 186 } 187 188 e.EventDetail.InstanceIDs = append(e.EventDetail.InstanceIDs, instanceID) 189 eventData, err := json.Marshal(e) 190 if err != nil { 191 return err 192 } 193 _, err = s.EventBridgeClient.PutRule(&eventbridge.PutRuleInput{ 194 Name: aws.String(s.getEC2RuleName()), 195 EventPattern: aws.String(string(eventData)), 196 State: aws.String(eventbridge.RuleStateEnabled), 197 }) 198 return err 199 } 200 201 // RemoveInstanceFromEventPattern attempts a best effort update to the event rule to remove the instance. 202 // Any errors encountered won't be blocking. 203 func (s Service) RemoveInstanceFromEventPattern(instanceID string) { 204 ruleResp, err := s.EventBridgeClient.DescribeRule(&eventbridge.DescribeRuleInput{ 205 Name: aws.String(s.getEC2RuleName()), 206 }) 207 if err != nil { 208 return 209 } 210 e := eventPattern{} 211 err = json.Unmarshal([]byte(*ruleResp.EventPattern), &e) 212 if err != nil { 213 return 214 } 215 e.DetailType = []string{Ec2StateChangeNotification} 216 217 found := false 218 for i, r := range e.EventDetail.InstanceIDs { 219 if r == instanceID { 220 found = true 221 e.EventDetail.InstanceIDs = append(e.EventDetail.InstanceIDs[:i], e.EventDetail.InstanceIDs[i+1:]...) 222 break 223 } 224 } 225 226 if found { 227 eventData, err := json.Marshal(e) 228 if err != nil { 229 return 230 } 231 input := &eventbridge.PutRuleInput{ 232 Name: aws.String(s.getEC2RuleName()), 233 EventPattern: aws.String(string(eventData)), 234 State: aws.String(eventbridge.RuleStateEnabled), 235 } 236 237 if len(e.EventDetail.InstanceIDs) == 0 { 238 input.State = aws.String(eventbridge.RuleStateDisabled) 239 } 240 _, _ = s.EventBridgeClient.PutRule(input) 241 } 242 } 243 244 func (s Service) getEC2RuleName() string { 245 return fmt.Sprintf("%s-ec2-rule", s.scope.Name()) 246 } 247 248 func resourceNotFoundError(err error) bool { 249 if aerr, ok := err.(awserr.Error); ok && aerr.Code() == eventbridge.ErrCodeResourceNotFoundException { 250 return true 251 } 252 return false 253 } 254 255 type eventPattern struct { 256 Source []string `json:"source"` 257 DetailType []string `json:"detail-type,omitempty"` 258 EventDetail *eventDetail `json:"detail,omitempty"` 259 } 260 261 type eventDetail struct { 262 InstanceIDs []string `json:"instance-id,omitempty"` 263 States []infrav1.InstanceState `json:"state,omitempty"` 264 }