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

     1  /*
     2  Copyright 2019 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 securitygroup
    18  
    19  import (
    20  	"strings"
    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/ec2"
    26  	"github.com/golang/mock/gomock"
    27  	. "github.com/onsi/gomega"
    28  	"github.com/pkg/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    33  
    34  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    35  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
    36  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
    37  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services"
    38  	"sigs.k8s.io/cluster-api-provider-aws/test/mocks"
    39  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    40  )
    41  
    42  var (
    43  	testSecurityGroupRoles = []infrav1.SecurityGroupRole{
    44  		infrav1.SecurityGroupBastion,
    45  		infrav1.SecurityGroupAPIServerLB,
    46  		infrav1.SecurityGroupLB,
    47  		infrav1.SecurityGroupControlPlane,
    48  		infrav1.SecurityGroupNode,
    49  	}
    50  )
    51  
    52  func TestReconcileSecurityGroups(t *testing.T) {
    53  	mockCtrl := gomock.NewController(t)
    54  	defer mockCtrl.Finish()
    55  
    56  	testCases := []struct {
    57  		name   string
    58  		input  *infrav1.NetworkSpec
    59  		expect func(m *mocks.MockEC2APIMockRecorder)
    60  		err    error
    61  	}{
    62  		{
    63  			name: "no existing",
    64  			input: &infrav1.NetworkSpec{
    65  				VPC: infrav1.VPCSpec{
    66  					ID:                "vpc-securitygroups",
    67  					InternetGatewayID: aws.String("igw-01"),
    68  					Tags: infrav1.Tags{
    69  						infrav1.ClusterTagKey("test-cluster"): "owned",
    70  					},
    71  				},
    72  				Subnets: infrav1.Subnets{
    73  					infrav1.SubnetSpec{
    74  						ID:               "subnet-securitygroups-private",
    75  						IsPublic:         false,
    76  						AvailabilityZone: "us-east-1a",
    77  					},
    78  					infrav1.SubnetSpec{
    79  						ID:               "subnet-securitygroups-public",
    80  						IsPublic:         true,
    81  						NatGatewayID:     aws.String("nat-01"),
    82  						AvailabilityZone: "us-east-1a",
    83  					},
    84  				},
    85  			},
    86  			expect: func(m *mocks.MockEC2APIMockRecorder) {
    87  				m.DescribeSecurityGroups(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{})).
    88  					Return(&ec2.DescribeSecurityGroupsOutput{}, nil)
    89  
    90  				securityGroupBastion := m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
    91  					VpcId:       aws.String("vpc-securitygroups"),
    92  					GroupName:   aws.String("test-cluster-bastion"),
    93  					Description: aws.String("Kubernetes cluster test-cluster: bastion"),
    94  					TagSpecifications: []*ec2.TagSpecification{
    95  						{
    96  							ResourceType: aws.String("security-group"),
    97  							Tags: []*ec2.Tag{
    98  								{
    99  									Key:   aws.String("Name"),
   100  									Value: aws.String("test-cluster-bastion"),
   101  								},
   102  								{
   103  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   104  									Value: aws.String("owned"),
   105  								},
   106  								{
   107  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   108  									Value: aws.String("bastion"),
   109  								},
   110  							},
   111  						},
   112  					},
   113  				})).
   114  					Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-bastion")}, nil)
   115  
   116  				m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
   117  					GroupId: aws.String("sg-bastion"),
   118  				})).
   119  					Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
   120  					After(securityGroupBastion)
   121  
   122  				securityGroupAPIServerLb := m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
   123  					VpcId:       aws.String("vpc-securitygroups"),
   124  					GroupName:   aws.String("test-cluster-apiserver-lb"),
   125  					Description: aws.String("Kubernetes cluster test-cluster: apiserver-lb"),
   126  					TagSpecifications: []*ec2.TagSpecification{
   127  						{
   128  							ResourceType: aws.String("security-group"),
   129  							Tags: []*ec2.Tag{
   130  								{
   131  									Key:   aws.String("Name"),
   132  									Value: aws.String("test-cluster-apiserver-lb"),
   133  								},
   134  								{
   135  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   136  									Value: aws.String("owned"),
   137  								},
   138  								{
   139  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   140  									Value: aws.String("apiserver-lb"),
   141  								},
   142  							},
   143  						},
   144  					},
   145  				})).
   146  					Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-apiserver-lb")}, nil)
   147  
   148  				m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
   149  					GroupId: aws.String("sg-apiserver-lb"),
   150  				})).
   151  					Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
   152  					After(securityGroupAPIServerLb)
   153  
   154  				m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
   155  					VpcId:       aws.String("vpc-securitygroups"),
   156  					GroupName:   aws.String("test-cluster-lb"),
   157  					Description: aws.String("Kubernetes cluster test-cluster: lb"),
   158  					TagSpecifications: []*ec2.TagSpecification{
   159  						{
   160  							ResourceType: aws.String("security-group"),
   161  							Tags: []*ec2.Tag{
   162  								{
   163  									Key:   aws.String("Name"),
   164  									Value: aws.String("test-cluster-lb"),
   165  								},
   166  								{
   167  									Key:   aws.String("kubernetes.io/cluster/test-cluster"),
   168  									Value: aws.String("owned"),
   169  								},
   170  								{
   171  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   172  									Value: aws.String("owned"),
   173  								},
   174  								{
   175  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   176  									Value: aws.String("lb"),
   177  								},
   178  							},
   179  						},
   180  					},
   181  				})).
   182  					Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-lb")}, nil)
   183  
   184  				securityGroupControl := m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
   185  					VpcId:       aws.String("vpc-securitygroups"),
   186  					GroupName:   aws.String("test-cluster-controlplane"),
   187  					Description: aws.String("Kubernetes cluster test-cluster: controlplane"),
   188  					TagSpecifications: []*ec2.TagSpecification{
   189  						{
   190  							ResourceType: aws.String("security-group"),
   191  							Tags: []*ec2.Tag{
   192  								{
   193  									Key:   aws.String("Name"),
   194  									Value: aws.String("test-cluster-controlplane"),
   195  								},
   196  								{
   197  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   198  									Value: aws.String("owned"),
   199  								},
   200  								{
   201  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   202  									Value: aws.String("controlplane"),
   203  								},
   204  							},
   205  						},
   206  					},
   207  				})).
   208  					Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-control")}, nil)
   209  
   210  				m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
   211  					GroupId: aws.String("sg-control"),
   212  				})).
   213  					Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
   214  					After(securityGroupControl)
   215  
   216  				securityGroupNode := m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
   217  					VpcId:       aws.String("vpc-securitygroups"),
   218  					GroupName:   aws.String("test-cluster-node"),
   219  					Description: aws.String("Kubernetes cluster test-cluster: node"),
   220  					TagSpecifications: []*ec2.TagSpecification{
   221  						{
   222  							ResourceType: aws.String("security-group"),
   223  							Tags: []*ec2.Tag{
   224  								{
   225  									Key:   aws.String("Name"),
   226  									Value: aws.String("test-cluster-node"),
   227  								},
   228  								{
   229  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   230  									Value: aws.String("owned"),
   231  								},
   232  								{
   233  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   234  									Value: aws.String("node"),
   235  								},
   236  							},
   237  						},
   238  					},
   239  				})).
   240  					Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-node")}, nil)
   241  
   242  				m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
   243  					GroupId: aws.String("sg-node"),
   244  				})).
   245  					Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
   246  					After(securityGroupNode)
   247  			},
   248  		},
   249  		{
   250  			name: "all overridden, do not tag",
   251  			input: &infrav1.NetworkSpec{
   252  				VPC: infrav1.VPCSpec{
   253  					ID:                "vpc-securitygroups",
   254  					InternetGatewayID: aws.String("igw-01"),
   255  				},
   256  				Subnets: infrav1.Subnets{
   257  					infrav1.SubnetSpec{
   258  						ID:               "subnet-securitygroups-private",
   259  						IsPublic:         false,
   260  						AvailabilityZone: "us-east-1a",
   261  					},
   262  					infrav1.SubnetSpec{
   263  						ID:               "subnet-securitygroups-public",
   264  						IsPublic:         true,
   265  						NatGatewayID:     aws.String("nat-01"),
   266  						AvailabilityZone: "us-east-1a",
   267  					},
   268  				},
   269  				SecurityGroupOverrides: map[infrav1.SecurityGroupRole]string{
   270  					infrav1.SecurityGroupBastion:      "sg-bastion",
   271  					infrav1.SecurityGroupAPIServerLB:  "sg-apiserver-lb",
   272  					infrav1.SecurityGroupLB:           "sg-lb",
   273  					infrav1.SecurityGroupControlPlane: "sg-control",
   274  					infrav1.SecurityGroupNode:         "sg-node",
   275  				},
   276  			},
   277  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   278  				m.DescribeSecurityGroups(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{})).
   279  					Return(&ec2.DescribeSecurityGroupsOutput{
   280  						SecurityGroups: []*ec2.SecurityGroup{
   281  							{GroupId: aws.String("sg-bastion"), GroupName: aws.String("Bastion Security Group")},
   282  							{GroupId: aws.String("sg-apiserver-lb"), GroupName: aws.String("API load balancer Security Group")},
   283  							{GroupId: aws.String("sg-lb"), GroupName: aws.String("Load balancer Security Group")},
   284  							{GroupId: aws.String("sg-control"), GroupName: aws.String("Control plane Security Group")},
   285  							{GroupId: aws.String("sg-node"), GroupName: aws.String("Node Security Group")},
   286  						},
   287  					}, nil).AnyTimes()
   288  			},
   289  		},
   290  		{
   291  			name: "managed vpc with overrides, returns error",
   292  			input: &infrav1.NetworkSpec{
   293  				VPC: infrav1.VPCSpec{
   294  					ID:                "vpc-securitygroups",
   295  					InternetGatewayID: aws.String("igw-01"),
   296  					Tags: infrav1.Tags{
   297  						infrav1.ClusterTagKey("test-cluster"): "owned",
   298  					},
   299  				},
   300  				Subnets: infrav1.Subnets{
   301  					infrav1.SubnetSpec{
   302  						ID:               "subnet-securitygroups-private",
   303  						IsPublic:         false,
   304  						AvailabilityZone: "us-east-1a",
   305  					},
   306  					infrav1.SubnetSpec{
   307  						ID:               "subnet-securitygroups-public",
   308  						IsPublic:         true,
   309  						NatGatewayID:     aws.String("nat-01"),
   310  						AvailabilityZone: "us-east-1a",
   311  					},
   312  				},
   313  				SecurityGroupOverrides: map[infrav1.SecurityGroupRole]string{
   314  					infrav1.SecurityGroupBastion:      "sg-bastion",
   315  					infrav1.SecurityGroupAPIServerLB:  "sg-apiserver-lb",
   316  					infrav1.SecurityGroupLB:           "sg-lb",
   317  					infrav1.SecurityGroupControlPlane: "sg-control",
   318  					infrav1.SecurityGroupNode:         "sg-node",
   319  				},
   320  			},
   321  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   322  				m.DescribeSecurityGroups(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{})).
   323  					Return(&ec2.DescribeSecurityGroupsOutput{
   324  						SecurityGroups: []*ec2.SecurityGroup{
   325  							{GroupId: aws.String("sg-bastion"), GroupName: aws.String("Bastion Security Group")},
   326  							{GroupId: aws.String("sg-apiserver-lb"), GroupName: aws.String("API load balancer Security Group")},
   327  							{GroupId: aws.String("sg-lb"), GroupName: aws.String("Load balancer Security Group")},
   328  							{GroupId: aws.String("sg-control"), GroupName: aws.String("Control plane Security Group")},
   329  							{GroupId: aws.String("sg-node"), GroupName: aws.String("Node Security Group")},
   330  						},
   331  					}, nil).AnyTimes()
   332  			},
   333  			err: errors.New(`security group overrides provided for managed vpc "test-cluster"`),
   334  		},
   335  	}
   336  
   337  	for _, tc := range testCases {
   338  		t.Run(tc.name, func(t *testing.T) {
   339  			ec2Mock := mocks.NewMockEC2API(mockCtrl)
   340  
   341  			scheme := runtime.NewScheme()
   342  			_ = infrav1.AddToScheme(scheme)
   343  			client := fake.NewClientBuilder().WithScheme(scheme).Build()
   344  			cs, err := scope.NewClusterScope(scope.ClusterScopeParams{
   345  				Client: client,
   346  				Cluster: &clusterv1.Cluster{
   347  					ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"},
   348  				},
   349  				AWSCluster: &infrav1.AWSCluster{
   350  					ObjectMeta: metav1.ObjectMeta{Name: "test"},
   351  					Spec: infrav1.AWSClusterSpec{
   352  						NetworkSpec: *tc.input,
   353  					},
   354  				},
   355  			})
   356  			if err != nil {
   357  				t.Fatalf("Failed to create test context: %v", err)
   358  			}
   359  
   360  			tc.expect(ec2Mock.EXPECT())
   361  
   362  			s := NewService(cs, testSecurityGroupRoles)
   363  			s.EC2Client = ec2Mock
   364  
   365  			if err := s.ReconcileSecurityGroups(); err != nil && tc.err != nil {
   366  				if !strings.Contains(err.Error(), tc.err.Error()) {
   367  					t.Fatalf("was expecting error to look like '%v', but got '%v'", tc.err, err)
   368  				}
   369  			} else if err != nil {
   370  				t.Fatalf("got an unexpected error: %v", err)
   371  			}
   372  		})
   373  	}
   374  }
   375  
   376  func TestControlPlaneSecurityGroupNotOpenToAnyCIDR(t *testing.T) {
   377  	scheme := runtime.NewScheme()
   378  	_ = infrav1.AddToScheme(scheme)
   379  	client := fake.NewClientBuilder().WithScheme(scheme).Build()
   380  	cs, err := scope.NewClusterScope(scope.ClusterScopeParams{
   381  		Client: client,
   382  		Cluster: &clusterv1.Cluster{
   383  			ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"},
   384  		},
   385  		AWSCluster: &infrav1.AWSCluster{},
   386  	})
   387  	if err != nil {
   388  		t.Fatalf("Failed to create test context: %v", err)
   389  	}
   390  
   391  	s := NewService(cs, testSecurityGroupRoles)
   392  	rules, err := s.getSecurityGroupIngressRules(infrav1.SecurityGroupControlPlane)
   393  	if err != nil {
   394  		t.Fatalf("Failed to lookup controlplane security group ingress rules: %v", err)
   395  	}
   396  
   397  	for _, r := range rules {
   398  		if sets.NewString(r.CidrBlocks...).Has(services.AnyIPv4CidrBlock) {
   399  			t.Fatal("Ingress rule allows any CIDR block")
   400  		}
   401  	}
   402  }
   403  
   404  func TestDeleteSecurityGroups(t *testing.T) {
   405  	mockCtrl := gomock.NewController(t)
   406  	defer mockCtrl.Finish()
   407  
   408  	testCases := []struct {
   409  		name    string
   410  		input   *infrav1.NetworkSpec
   411  		expect  func(m *mocks.MockEC2APIMockRecorder)
   412  		wantErr bool
   413  	}{
   414  		{
   415  			name: "do not delete overridden security groups",
   416  			input: &infrav1.NetworkSpec{
   417  				VPC: infrav1.VPCSpec{
   418  					ID:                "vpc-securitygroups",
   419  					InternetGatewayID: aws.String("igw-01"),
   420  				},
   421  				Subnets: infrav1.Subnets{
   422  					infrav1.SubnetSpec{
   423  						ID:               "subnet-securitygroups-private",
   424  						IsPublic:         false,
   425  						AvailabilityZone: "us-east-1a",
   426  					},
   427  					infrav1.SubnetSpec{
   428  						ID:               "subnet-securitygroups-public",
   429  						IsPublic:         true,
   430  						NatGatewayID:     aws.String("nat-01"),
   431  						AvailabilityZone: "us-east-1a",
   432  					},
   433  				},
   434  				SecurityGroupOverrides: map[infrav1.SecurityGroupRole]string{
   435  					infrav1.SecurityGroupBastion:      "sg-bastion",
   436  					infrav1.SecurityGroupAPIServerLB:  "sg-apiserver-lb",
   437  					infrav1.SecurityGroupLB:           "sg-lb",
   438  					infrav1.SecurityGroupControlPlane: "sg-control",
   439  					infrav1.SecurityGroupNode:         "sg-node",
   440  				},
   441  			},
   442  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   443  				m.DescribeSecurityGroupsPages(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{}), gomock.Any()).Return(nil)
   444  			},
   445  		},
   446  		{
   447  			name: "Should skip SG deletion if VPC ID not present",
   448  			input: &infrav1.NetworkSpec{
   449  				VPC: infrav1.VPCSpec{},
   450  			},
   451  		},
   452  		{
   453  			name: "Should return error if unable to find cluster-owned security groups in vpc",
   454  			input: &infrav1.NetworkSpec{
   455  				VPC: infrav1.VPCSpec{ID: "vpc-id"},
   456  			},
   457  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   458  				m.DescribeSecurityGroupsPages(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{}), gomock.Any()).Return(awserrors.NewFailedDependency("dependency-failure"))
   459  			},
   460  			wantErr: true,
   461  		},
   462  		{
   463  			name: "Should return error if unable to describe any SG present in VPC and owned by cluster",
   464  			input: &infrav1.NetworkSpec{
   465  				VPC: infrav1.VPCSpec{ID: "vpc-id"},
   466  			},
   467  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   468  				m.DescribeSecurityGroupsPages(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{}), gomock.Any()).
   469  					Do(processSecurityGroupsPage).Return(nil)
   470  				m.DescribeSecurityGroups(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{})).Return(nil, awserr.New("dependency-failure", "dependency-failure", errors.Errorf("dependency-failure")))
   471  			},
   472  			wantErr: true,
   473  		},
   474  		{
   475  			name: "Should not revoke Ingress rules for a SG if IP permissions are not set and able to delete the SG",
   476  			input: &infrav1.NetworkSpec{
   477  				VPC: infrav1.VPCSpec{ID: "vpc-id"},
   478  			},
   479  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   480  				m.DescribeSecurityGroupsPages(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{}), gomock.Any()).
   481  					Do(processSecurityGroupsPage).Return(nil)
   482  				m.DescribeSecurityGroups(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{})).Return(&ec2.DescribeSecurityGroupsOutput{
   483  					SecurityGroups: []*ec2.SecurityGroup{
   484  						{
   485  							GroupId:   aws.String("group-id"),
   486  							GroupName: aws.String("group-name"),
   487  						},
   488  					},
   489  				}, nil)
   490  				m.DeleteSecurityGroup(gomock.AssignableToTypeOf(&ec2.DeleteSecurityGroupInput{})).Return(nil, nil)
   491  			},
   492  		},
   493  		{
   494  			name: "Should return error if failed to revoke Ingress rules for a SG",
   495  			input: &infrav1.NetworkSpec{
   496  				VPC: infrav1.VPCSpec{ID: "vpc-id"},
   497  			},
   498  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   499  				m.DescribeSecurityGroupsPages(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{}), gomock.Any()).
   500  					Do(processSecurityGroupsPage).Return(nil)
   501  				m.DescribeSecurityGroups(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{})).Return(&ec2.DescribeSecurityGroupsOutput{
   502  					SecurityGroups: []*ec2.SecurityGroup{
   503  						{
   504  							GroupId:   aws.String("group-id"),
   505  							GroupName: aws.String("group-name"),
   506  							IpPermissions: []*ec2.IpPermission{
   507  								{
   508  									ToPort: aws.Int64(4),
   509  								},
   510  							},
   511  						},
   512  					},
   513  				}, nil)
   514  				m.RevokeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.RevokeSecurityGroupIngressInput{})).Return(nil, awserr.New("failure", "failure", errors.Errorf("failure")))
   515  			},
   516  			wantErr: true,
   517  		},
   518  		{
   519  			name: "Should delete SG successfully",
   520  			input: &infrav1.NetworkSpec{
   521  				VPC: infrav1.VPCSpec{ID: "vpc-id"},
   522  			},
   523  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   524  				m.DescribeSecurityGroupsPages(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{}), gomock.Any()).
   525  					Do(processSecurityGroupsPage).Return(nil)
   526  				m.DescribeSecurityGroups(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{})).Return(&ec2.DescribeSecurityGroupsOutput{
   527  					SecurityGroups: []*ec2.SecurityGroup{
   528  						{
   529  							GroupId:   aws.String("group-id"),
   530  							GroupName: aws.String("group-name"),
   531  							IpPermissions: []*ec2.IpPermission{
   532  								{
   533  									ToPort: aws.Int64(4),
   534  								},
   535  							},
   536  						},
   537  					},
   538  				}, nil)
   539  				m.RevokeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.RevokeSecurityGroupIngressInput{})).Return(nil, nil)
   540  				m.DeleteSecurityGroup(gomock.AssignableToTypeOf(&ec2.DeleteSecurityGroupInput{})).Return(nil, nil)
   541  			},
   542  		},
   543  	}
   544  	for _, tc := range testCases {
   545  		t.Run(tc.name, func(t *testing.T) {
   546  			g := NewWithT(t)
   547  			ec2Mock := mocks.NewMockEC2API(mockCtrl)
   548  
   549  			scheme := runtime.NewScheme()
   550  			g.Expect(infrav1.AddToScheme(scheme)).NotTo(HaveOccurred())
   551  
   552  			awsCluster := &infrav1.AWSCluster{
   553  				TypeMeta: metav1.TypeMeta{
   554  					APIVersion: infrav1.GroupVersion.String(),
   555  					Kind:       "AWSCluster",
   556  				},
   557  				ObjectMeta: metav1.ObjectMeta{Name: "test"},
   558  				Spec: infrav1.AWSClusterSpec{
   559  					NetworkSpec: *tc.input,
   560  				},
   561  			}
   562  
   563  			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(awsCluster).Build()
   564  
   565  			cs, err := scope.NewClusterScope(scope.ClusterScopeParams{
   566  				Client: client,
   567  				Cluster: &clusterv1.Cluster{
   568  					ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"},
   569  				},
   570  				AWSCluster: awsCluster,
   571  			})
   572  			g.Expect(err).NotTo(HaveOccurred())
   573  
   574  			if tc.expect != nil {
   575  				tc.expect(ec2Mock.EXPECT())
   576  			}
   577  
   578  			s := NewService(cs, testSecurityGroupRoles)
   579  			s.EC2Client = ec2Mock
   580  
   581  			err = s.DeleteSecurityGroups()
   582  			if tc.wantErr {
   583  				g.Expect(err).To(HaveOccurred())
   584  				return
   585  			}
   586  			g.Expect(err).NotTo(HaveOccurred())
   587  		})
   588  	}
   589  }
   590  
   591  func TestIngressRulesFromSDKType(t *testing.T) {
   592  	tests := []struct {
   593  		name     string
   594  		input    *ec2.IpPermission
   595  		expected infrav1.IngressRules
   596  	}{
   597  		{
   598  			name: "Two group pairs",
   599  			input: &ec2.IpPermission{
   600  				IpProtocol: aws.String("tcp"),
   601  				FromPort:   aws.Int64(10250),
   602  				ToPort:     aws.Int64(10250),
   603  				UserIdGroupPairs: []*ec2.UserIdGroupPair{
   604  					{
   605  						Description: aws.String("Kubelet API"),
   606  						UserId:      aws.String("aws-user-id-1"),
   607  						GroupId:     aws.String("sg-source-1"),
   608  					},
   609  					{
   610  						Description: aws.String("Kubelet API"),
   611  						UserId:      aws.String("aws-user-id-1"),
   612  						GroupId:     aws.String("sg-source-2"),
   613  					},
   614  				},
   615  			},
   616  			expected: infrav1.IngressRules{
   617  				{
   618  					Description:            "Kubelet API",
   619  					Protocol:               "tcp",
   620  					FromPort:               10250,
   621  					ToPort:                 10250,
   622  					SourceSecurityGroupIDs: []string{"sg-source-1", "sg-source-2"},
   623  				},
   624  			},
   625  		},
   626  		{
   627  			name: "Mix of group pairs and cidr blocks",
   628  			input: &ec2.IpPermission{
   629  				IpProtocol: aws.String("tcp"),
   630  				FromPort:   aws.Int64(22),
   631  				ToPort:     aws.Int64(22),
   632  				IpRanges: []*ec2.IpRange{
   633  					{
   634  						CidrIp:      aws.String("0.0.0.0/0"),
   635  						Description: aws.String("MY-SSH"),
   636  					},
   637  				},
   638  				UserIdGroupPairs: []*ec2.UserIdGroupPair{
   639  					{
   640  						UserId:      aws.String("aws-user-id-1"),
   641  						GroupId:     aws.String("sg-source-1"),
   642  						Description: aws.String("SSH"),
   643  					},
   644  				},
   645  			},
   646  			expected: infrav1.IngressRules{
   647  				{
   648  					Description: "MY-SSH",
   649  					Protocol:    "tcp",
   650  					FromPort:    22,
   651  					ToPort:      22,
   652  					CidrBlocks:  []string{"0.0.0.0/0"},
   653  				},
   654  				{
   655  					Description:            "SSH",
   656  					Protocol:               "tcp",
   657  					FromPort:               22,
   658  					ToPort:                 22,
   659  					SourceSecurityGroupIDs: []string{"sg-source-1"},
   660  				},
   661  			},
   662  		},
   663  	}
   664  
   665  	for _, tc := range tests {
   666  		t.Run(tc.name, func(t *testing.T) {
   667  			g := NewGomegaWithT(t)
   668  			output := ingressRulesFromSDKType(tc.input)
   669  
   670  			g.Expect(output).To(Equal(tc.expected))
   671  		})
   672  	}
   673  }
   674  
   675  var processSecurityGroupsPage = func(_, y interface{}) {
   676  	funcType := y.(func(out *ec2.DescribeSecurityGroupsOutput, last bool) bool)
   677  	funcType(&ec2.DescribeSecurityGroupsOutput{
   678  		SecurityGroups: []*ec2.SecurityGroup{
   679  			{
   680  				GroupId:   aws.String("group-id"),
   681  				GroupName: aws.String("group-name"),
   682  			},
   683  		},
   684  	}, true)
   685  }