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

     1  /*
     2  Copyright 2022 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 ssm
    18  
    19  import (
    20  	"math/rand"
    21  	"sort"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/aws/aws-sdk-go/aws"
    26  	"github.com/aws/aws-sdk-go/service/ssm"
    27  	"github.com/golang/mock/gomock"
    28  	"github.com/google/go-cmp/cmp"
    29  	. "github.com/onsi/gomega"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    34  
    35  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    36  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
    37  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
    38  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/ssm/mock_ssmiface"
    39  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    40  )
    41  
    42  func TestService_Create(t *testing.T) {
    43  	mockCtrl := gomock.NewController(t)
    44  	defer mockCtrl.Finish()
    45  
    46  	generateBytes := func(count int64) []byte {
    47  		token := make([]byte, count)
    48  		_, err := rand.Read(token)
    49  		if err != nil {
    50  			t.Fatalf("error while creating data: %v", err)
    51  		}
    52  		return token
    53  	}
    54  
    55  	check := func(actualPrefix string, expectedPrefix string, err error, IsErrorExpected bool) {
    56  		if !strings.HasPrefix(actualPrefix, expectedPrefix) {
    57  			t.Fatalf("Prefix is not as expected: %v", actualPrefix)
    58  		}
    59  		if (err != nil) != IsErrorExpected {
    60  			t.Fatalf("Unexpected error value, error = %v, expectedError %v", err, IsErrorExpected)
    61  		}
    62  	}
    63  
    64  	sortTagsByKey := func(tags []*ssm.Tag) {
    65  		sort.Slice(tags, func(i, j int) bool {
    66  			return *(tags[i].Key) < *(tags[j].Key)
    67  		})
    68  	}
    69  
    70  	expectedTags := []*ssm.Tag{
    71  		{
    72  			Key:   aws.String("Name"),
    73  			Value: aws.String("infra-cluster"),
    74  		},
    75  		{
    76  			Key:   aws.String("kubernetes.io/cluster/test"),
    77  			Value: aws.String("owned"),
    78  		},
    79  		{
    80  			Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test"),
    81  			Value: aws.String("owned"),
    82  		},
    83  		{
    84  			Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
    85  			Value: aws.String("node"),
    86  		},
    87  	}
    88  
    89  	tests := []struct {
    90  		name           string
    91  		bytesCount     int64
    92  		secretPrefix   string
    93  		expectedPrefix string
    94  		wantErr        bool
    95  		expect         func(m *mock_ssmiface.MockSSMAPIMockRecorder)
    96  	}{
    97  		{
    98  			name:           "Should not store data in SSM if data is having zero bytes",
    99  			bytesCount:     0,
   100  			secretPrefix:   "/awsprefix",
   101  			expectedPrefix: "/prefix",
   102  		},
   103  		{
   104  			name:           "Should store data in SSM if data is having non-zero bytes",
   105  			bytesCount:     10000,
   106  			secretPrefix:   "prefix",
   107  			expectedPrefix: "/prefix",
   108  			expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) {
   109  				m.PutParameter(gomock.AssignableToTypeOf(&ssm.PutParameterInput{})).MinTimes(1).Return(&ssm.PutParameterOutput{}, nil).Do(
   110  					func(putParameterInput *ssm.PutParameterInput) {
   111  						if !strings.HasPrefix(*(putParameterInput.Name), "/prefix/") {
   112  							t.Fatalf("Prefix is not as expected: %v", putParameterInput.Name)
   113  						}
   114  						sortTagsByKey(putParameterInput.Tags)
   115  						if !cmp.Equal(putParameterInput.Tags, expectedTags) {
   116  							t.Fatalf("Tags are not as expected, actual: %v, expected: %v", putParameterInput.Tags, expectedTags)
   117  						}
   118  					},
   119  				)
   120  			},
   121  		},
   122  		{
   123  			name:           "Should not retry if non-retryable error occurred while storing data in SSM",
   124  			bytesCount:     10,
   125  			secretPrefix:   "/prefix",
   126  			expectedPrefix: "/prefix",
   127  			wantErr:        true,
   128  			expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) {
   129  				m.PutParameter(gomock.AssignableToTypeOf(&ssm.PutParameterInput{})).Return(nil, &ssm.ParameterAlreadyExists{}).Do(
   130  					func(putParameterInput *ssm.PutParameterInput) {
   131  						if !strings.HasPrefix(*(putParameterInput.Name), "/prefix/") {
   132  							t.Fatalf("Prefix is not as expected: %v", putParameterInput.Name)
   133  						}
   134  						sortTagsByKey(putParameterInput.Tags)
   135  						if !cmp.Equal(putParameterInput.Tags, expectedTags) {
   136  							t.Fatalf("Tags are not as expected, actual: %v, expected: %v", putParameterInput.Tags, expectedTags)
   137  						}
   138  					},
   139  				)
   140  			},
   141  		},
   142  		{
   143  			name:           "Should retry if retryable error occurred while storing data in SSM",
   144  			bytesCount:     10,
   145  			secretPrefix:   "",
   146  			expectedPrefix: "/cluster.x-k8s.io",
   147  			expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) {
   148  				m.PutParameter(gomock.AssignableToTypeOf(&ssm.PutParameterInput{})).Return(nil, &ssm.ParameterLimitExceeded{})
   149  				m.PutParameter(gomock.AssignableToTypeOf(&ssm.PutParameterInput{})).Return(&ssm.PutParameterOutput{}, nil)
   150  			},
   151  		},
   152  	}
   153  	for _, tt := range tests {
   154  		t.Run(tt.name, func(t *testing.T) {
   155  			g := NewWithT(t)
   156  			scheme := runtime.NewScheme()
   157  			_ = infrav1.AddToScheme(scheme)
   158  			client := fake.NewClientBuilder().WithScheme(scheme).Build()
   159  
   160  			clusterScope, err := getClusterScope(client)
   161  			g.Expect(err).NotTo(HaveOccurred())
   162  			ssmClientMock := mock_ssmiface.NewMockSSMAPI(mockCtrl)
   163  			if tt.expect != nil {
   164  				tt.expect(ssmClientMock.EXPECT())
   165  			}
   166  			s := NewService(clusterScope)
   167  			s.SSMClient = ssmClientMock
   168  
   169  			ms, err := getMachineScope(client, clusterScope)
   170  			g.Expect(err).NotTo(HaveOccurred())
   171  			ms.SetSecretPrefix(tt.secretPrefix)
   172  			data := generateBytes(tt.bytesCount)
   173  
   174  			prefix, _, err := s.Create(ms, data)
   175  			check(prefix, tt.expectedPrefix, err, tt.wantErr)
   176  		})
   177  	}
   178  }
   179  
   180  func TestService_Delete(t *testing.T) {
   181  	mockCtrl := gomock.NewController(t)
   182  	defer mockCtrl.Finish()
   183  
   184  	tests := []struct {
   185  		name        string
   186  		secretCount int32
   187  		expect      func(m *mock_ssmiface.MockSSMAPIMockRecorder)
   188  		wantErr     bool
   189  		check       func(error)
   190  	}{
   191  		{
   192  			name:        "Should not call AWS when secret count has zero value",
   193  			secretCount: 0,
   194  			expect:      func(m *mock_ssmiface.MockSSMAPIMockRecorder) {},
   195  		},
   196  		{
   197  			name:        "Should not return error when delete is successful",
   198  			secretCount: 1,
   199  			expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) {
   200  				m.DeleteParameter(gomock.Eq(&ssm.DeleteParameterInput{
   201  					Name: aws.String("prefix/0"),
   202  				})).Return(&ssm.DeleteParameterOutput{}, nil)
   203  			},
   204  		},
   205  		{
   206  			name:        "Should return all errors except not found errors",
   207  			secretCount: 3,
   208  			expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) {
   209  				m.DeleteParameter(gomock.Eq(&ssm.DeleteParameterInput{
   210  					Name: aws.String("prefix/0"),
   211  				})).Return(nil, awserrors.NewFailedDependency("failed dependency"))
   212  				m.DeleteParameter(gomock.Eq(&ssm.DeleteParameterInput{
   213  					Name: aws.String("prefix/1"),
   214  				})).Return(nil, awserrors.NewNotFound("not found"))
   215  				m.DeleteParameter(gomock.Eq(&ssm.DeleteParameterInput{
   216  					Name: aws.String("prefix/2"),
   217  				})).Return(nil, awserrors.NewConflict("new conflict"))
   218  			},
   219  			wantErr: true,
   220  			check: func(err error) {
   221  				if err.Error() != "[failed dependency, new conflict]" {
   222  					t.Fatalf("Unexpected error: %v", err)
   223  				}
   224  			},
   225  		},
   226  	}
   227  	for _, tt := range tests {
   228  		t.Run(tt.name, func(t *testing.T) {
   229  			g := NewWithT(t)
   230  			scheme := runtime.NewScheme()
   231  			_ = infrav1.AddToScheme(scheme)
   232  			client := fake.NewClientBuilder().WithScheme(scheme).Build()
   233  
   234  			clusterScope, err := getClusterScope(client)
   235  			g.Expect(err).NotTo(HaveOccurred())
   236  
   237  			ssmClientMock := mock_ssmiface.NewMockSSMAPI(mockCtrl)
   238  			tt.expect(ssmClientMock.EXPECT())
   239  			s := NewService(clusterScope)
   240  			s.SSMClient = ssmClientMock
   241  			ms, err := getMachineScope(client, clusterScope)
   242  			g.Expect(err).NotTo(HaveOccurred())
   243  
   244  			ms.SetSecretPrefix("prefix")
   245  			ms.SetSecretCount(tt.secretCount)
   246  
   247  			err = s.Delete(ms)
   248  			if tt.wantErr {
   249  				g.Expect(err).To(HaveOccurred())
   250  				if tt.check != nil {
   251  					tt.check(err)
   252  				}
   253  				return
   254  			}
   255  			g.Expect(err).NotTo(HaveOccurred())
   256  		})
   257  	}
   258  }
   259  
   260  func getClusterScope(client client.Client) (*scope.ClusterScope, error) {
   261  	cluster := &clusterv1.Cluster{
   262  		ObjectMeta: metav1.ObjectMeta{
   263  			Name: "test",
   264  		},
   265  	}
   266  	return scope.NewClusterScope(scope.ClusterScopeParams{
   267  		Client:     client,
   268  		Cluster:    cluster,
   269  		AWSCluster: &infrav1.AWSCluster{},
   270  	})
   271  }
   272  
   273  func getMachineScope(client client.Client, clusterScope *scope.ClusterScope) (*scope.MachineScope, error) {
   274  	return scope.NewMachineScope(scope.MachineScopeParams{
   275  		Client:       client,
   276  		Cluster:      clusterScope.Cluster,
   277  		Machine:      &clusterv1.Machine{},
   278  		InfraCluster: clusterScope,
   279  		AWSMachine: &infrav1.AWSMachine{
   280  			ObjectMeta: metav1.ObjectMeta{
   281  				Name: "infra-cluster",
   282  			},
   283  		},
   284  	})
   285  }