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  }