sigs.k8s.io/cluster-api-provider-aws@v1.5.5/exp/instancestate/awsinstancestate_controller_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  	"context"
    21  	"fmt"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/aws/aws-sdk-go/aws"
    26  	"github.com/aws/aws-sdk-go/service/sqs"
    27  	"github.com/aws/aws-sdk-go/service/sqs/sqsiface"
    28  	"github.com/golang/mock/gomock"
    29  	. "github.com/onsi/gomega"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/utils/pointer"
    33  	ctrl "sigs.k8s.io/controller-runtime"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	"sigs.k8s.io/controller-runtime/pkg/controller"
    36  
    37  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    38  	"sigs.k8s.io/cluster-api-provider-aws/controllers"
    39  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/instancestate/mock_sqsiface"
    40  )
    41  
    42  func TestAWSInstanceStateController(t *testing.T) {
    43  	mockCtrl := gomock.NewController(t)
    44  	sqsSvs = mock_sqsiface.NewMockSQSAPI(mockCtrl)
    45  	instanceStateReconciler = &AwsInstanceStateReconciler{
    46  		Client: testEnv.Client,
    47  		Log:    ctrl.Log.WithName("controllers").WithName("AWSInstanceState"),
    48  		sqsServiceFactory: func() sqsiface.SQSAPI {
    49  			return sqsSvs
    50  		},
    51  	}
    52  	defer mockCtrl.Finish()
    53  
    54  	t.Run("should maintain list of cluster queue URLs and reconcile failing machines", func(t *testing.T) {
    55  		g := NewWithT(t)
    56  
    57  		failingMachineMeta := metav1.ObjectMeta{
    58  			Name:      "aws-cluster-1-instance-1",
    59  			Namespace: "default",
    60  		}
    61  		sqsSvs.EXPECT().GetQueueUrl(&sqs.GetQueueUrlInput{QueueName: aws.String("aws-cluster-1-queue")}).AnyTimes().
    62  			Return(&sqs.GetQueueUrlOutput{QueueUrl: aws.String("aws-cluster-1-url")}, nil)
    63  		sqsSvs.EXPECT().GetQueueUrl(&sqs.GetQueueUrlInput{QueueName: aws.String("aws-cluster-2-queue")}).AnyTimes().
    64  			Return(&sqs.GetQueueUrlOutput{QueueUrl: aws.String("aws-cluster-2-url")}, nil)
    65  		sqsSvs.EXPECT().GetQueueUrl(&sqs.GetQueueUrlInput{QueueName: aws.String("aws-cluster-3-queue")}).AnyTimes().
    66  			Return(&sqs.GetQueueUrlOutput{QueueUrl: aws.String("aws-cluster-3-url")}, nil)
    67  		sqsSvs.EXPECT().ReceiveMessage(&sqs.ReceiveMessageInput{QueueUrl: aws.String("aws-cluster-1-url")}).AnyTimes().
    68  			DoAndReturn(func(arg *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) {
    69  				m := &infrav1.AWSMachine{}
    70  				lookupKey := types.NamespacedName{
    71  					Namespace: failingMachineMeta.Namespace,
    72  					Name:      failingMachineMeta.Name,
    73  				}
    74  				err := k8sClient.Get(context.TODO(), lookupKey, m)
    75  				// start returning a message once the AWSMachine is available
    76  				if err == nil {
    77  					return &sqs.ReceiveMessageOutput{
    78  						Messages: []*sqs.Message{{
    79  							ReceiptHandle: aws.String("message-receipt-handle"),
    80  							Body:          aws.String(messageBodyJSON),
    81  						}},
    82  					}, nil
    83  				}
    84  
    85  				return &sqs.ReceiveMessageOutput{Messages: []*sqs.Message{}}, nil
    86  			})
    87  
    88  		sqsSvs.EXPECT().ReceiveMessage(&sqs.ReceiveMessageInput{QueueUrl: aws.String("aws-cluster-2-url")}).AnyTimes().
    89  			Return(&sqs.ReceiveMessageOutput{Messages: []*sqs.Message{}}, nil)
    90  		sqsSvs.EXPECT().ReceiveMessage(&sqs.ReceiveMessageInput{QueueUrl: aws.String("aws-cluster-3-url")}).AnyTimes().
    91  			Return(&sqs.ReceiveMessageOutput{Messages: []*sqs.Message{}}, nil)
    92  		sqsSvs.EXPECT().DeleteMessage(&sqs.DeleteMessageInput{QueueUrl: aws.String("aws-cluster-1-url"), ReceiptHandle: aws.String("message-receipt-handle")}).AnyTimes().
    93  			Return(nil, nil)
    94  
    95  		g.Expect(testEnv.Manager.GetFieldIndexer().IndexField(context.Background(), &infrav1.AWSMachine{},
    96  			controllers.InstanceIDIndex,
    97  			func(o client.Object) []string {
    98  				m := o.(*infrav1.AWSMachine)
    99  				if m.Spec.InstanceID != nil {
   100  					return []string{*m.Spec.InstanceID}
   101  				}
   102  				return nil
   103  			},
   104  		)).ToNot(HaveOccurred())
   105  
   106  		err := instanceStateReconciler.SetupWithManager(context.Background(), testEnv.Manager, controller.Options{})
   107  		g.Expect(err).ToNot(HaveOccurred())
   108  		go func() {
   109  			fmt.Println("Starting the manager")
   110  			if err := testEnv.StartManager(ctx); err != nil {
   111  				panic(fmt.Sprintf("Failed to start the envtest manager: %v", err))
   112  			}
   113  		}()
   114  		testEnv.WaitForWebhooks()
   115  
   116  		k8sClient = testEnv.GetClient()
   117  
   118  		persistObject(g, createAWSCluster("aws-cluster-1"))
   119  		persistObject(g, createAWSCluster("aws-cluster-2"))
   120  
   121  		machine1 := &infrav1.AWSMachine{
   122  			Spec: infrav1.AWSMachineSpec{
   123  				InstanceID:   pointer.StringPtr("i-failing-instance-1"),
   124  				InstanceType: "test",
   125  			},
   126  			ObjectMeta: failingMachineMeta,
   127  		}
   128  		persistObject(g, machine1)
   129  
   130  		t.Log("Ensuring queue URLs are up-to-date")
   131  		g.Eventually(func() bool {
   132  			exist := true
   133  			for _, cluster := range []string{"aws-cluster-1", "aws-cluster-2"} {
   134  				_, ok := instanceStateReconciler.queueURLs.Load(cluster)
   135  				exist = exist && ok
   136  			}
   137  			return exist
   138  		}, 10*time.Second).Should(Equal(true))
   139  
   140  		deleteAWSCluster(g, "aws-cluster-2")
   141  		t.Log("Ensuring we stop tracking deleted queue")
   142  		g.Eventually(func() bool {
   143  			_, ok := instanceStateReconciler.queueURLs.Load("aws-cluster-2")
   144  			return ok
   145  		}, 10*time.Second).Should(Equal(false))
   146  
   147  		persistObject(g, createAWSCluster("aws-cluster-3"))
   148  		t.Log("Ensuring newly created cluster is added to tracked clusters")
   149  		g.Eventually(func() bool {
   150  			exist := true
   151  			for _, cluster := range []string{"aws-cluster-1", "aws-cluster-3"} {
   152  				_, ok := instanceStateReconciler.queueURLs.Load(cluster)
   153  				exist = exist && ok
   154  			}
   155  			return exist
   156  		}, 10*time.Second).Should(Equal(true))
   157  
   158  		t.Log("Ensuring machine is labelled with correct instance state")
   159  		g.Eventually(func() bool {
   160  			m := &infrav1.AWSMachine{}
   161  			key := types.NamespacedName{
   162  				Namespace: failingMachineMeta.Namespace,
   163  				Name:      failingMachineMeta.Name,
   164  			}
   165  			g.Expect(k8sClient.Get(context.TODO(), key, m)).NotTo(HaveOccurred())
   166  			labels := m.GetLabels()
   167  			val := labels[Ec2InstanceStateLabelKey]
   168  			return val == "shutting-down"
   169  		}, 10*time.Second).Should(Equal(true))
   170  	})
   171  }
   172  
   173  const messageBodyJSON = `{
   174  	"source": "aws.ec2",
   175  	"detail-type": "EC2 Instance State-change Notification",
   176  	"detail": {
   177  		"instance-id": "i-failing-instance-1",
   178  		"state": "shutting-down"
   179  	}
   180  }`