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

     1  /*
     2  Copyright 2018 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 network
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/aws/aws-sdk-go/aws"
    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  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    32  
    33  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    34  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
    35  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
    36  	"sigs.k8s.io/cluster-api-provider-aws/test/mocks"
    37  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    38  )
    39  
    40  func TestReconcileRouteTables(t *testing.T) {
    41  	mockCtrl := gomock.NewController(t)
    42  	defer mockCtrl.Finish()
    43  
    44  	testCases := []struct {
    45  		name   string
    46  		input  *infrav1.NetworkSpec
    47  		expect func(m *mocks.MockEC2APIMockRecorder)
    48  		err    error
    49  	}{
    50  		{
    51  			name: "no routes existing, single private and single public, same AZ",
    52  			input: &infrav1.NetworkSpec{
    53  				VPC: infrav1.VPCSpec{
    54  					ID:                "vpc-routetables",
    55  					InternetGatewayID: aws.String("igw-01"),
    56  					Tags: infrav1.Tags{
    57  						infrav1.ClusterTagKey("test-cluster"): "owned",
    58  					},
    59  				},
    60  				Subnets: infrav1.Subnets{
    61  					infrav1.SubnetSpec{
    62  						ID:               "subnet-routetables-private",
    63  						IsPublic:         false,
    64  						AvailabilityZone: "us-east-1a",
    65  					},
    66  					infrav1.SubnetSpec{
    67  						ID:               "subnet-routetables-public",
    68  						IsPublic:         true,
    69  						NatGatewayID:     aws.String("nat-01"),
    70  						AvailabilityZone: "us-east-1a",
    71  					},
    72  				},
    73  			},
    74  			expect: func(m *mocks.MockEC2APIMockRecorder) {
    75  				m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})).
    76  					Return(&ec2.DescribeRouteTablesOutput{}, nil)
    77  
    78  				privateRouteTable := m.CreateRouteTable(matchRouteTableInput(&ec2.CreateRouteTableInput{VpcId: aws.String("vpc-routetables")})).
    79  					Return(&ec2.CreateRouteTableOutput{RouteTable: &ec2.RouteTable{RouteTableId: aws.String("rt-1")}}, nil)
    80  
    81  				m.CreateRoute(gomock.Eq(&ec2.CreateRouteInput{
    82  					NatGatewayId:         aws.String("nat-01"),
    83  					DestinationCidrBlock: aws.String("0.0.0.0/0"),
    84  					RouteTableId:         aws.String("rt-1"),
    85  				})).
    86  					After(privateRouteTable)
    87  
    88  				m.AssociateRouteTable(gomock.Eq(&ec2.AssociateRouteTableInput{
    89  					RouteTableId: aws.String("rt-1"),
    90  					SubnetId:     aws.String("subnet-routetables-private"),
    91  				})).
    92  					Return(&ec2.AssociateRouteTableOutput{}, nil).
    93  					After(privateRouteTable)
    94  
    95  				publicRouteTable := m.CreateRouteTable(matchRouteTableInput(&ec2.CreateRouteTableInput{VpcId: aws.String("vpc-routetables")})).
    96  					Return(&ec2.CreateRouteTableOutput{RouteTable: &ec2.RouteTable{RouteTableId: aws.String("rt-2")}}, nil)
    97  
    98  				m.CreateRoute(gomock.Eq(&ec2.CreateRouteInput{
    99  					GatewayId:            aws.String("igw-01"),
   100  					DestinationCidrBlock: aws.String("0.0.0.0/0"),
   101  					RouteTableId:         aws.String("rt-2"),
   102  				})).
   103  					After(publicRouteTable)
   104  
   105  				m.AssociateRouteTable(gomock.Eq(&ec2.AssociateRouteTableInput{
   106  					RouteTableId: aws.String("rt-2"),
   107  					SubnetId:     aws.String("subnet-routetables-public"),
   108  				})).
   109  					Return(&ec2.AssociateRouteTableOutput{}, nil).
   110  					After(publicRouteTable)
   111  			},
   112  		},
   113  		{
   114  			name: "subnets in different availability zones, returns error",
   115  			input: &infrav1.NetworkSpec{
   116  				VPC: infrav1.VPCSpec{
   117  					InternetGatewayID: aws.String("igw-01"),
   118  					ID:                "vpc-routetables",
   119  					Tags: infrav1.Tags{
   120  						infrav1.ClusterTagKey("test-cluster"): "owned",
   121  					},
   122  				},
   123  				Subnets: infrav1.Subnets{
   124  					infrav1.SubnetSpec{
   125  						ID:               "subnet-routetables-private",
   126  						IsPublic:         false,
   127  						AvailabilityZone: "us-east-1a",
   128  					},
   129  					infrav1.SubnetSpec{
   130  						ID:               "subnet-routetables-public",
   131  						IsPublic:         true,
   132  						NatGatewayID:     aws.String("nat-01"),
   133  						AvailabilityZone: "us-east-1b",
   134  					},
   135  				},
   136  			},
   137  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   138  				m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})).
   139  					Return(&ec2.DescribeRouteTablesOutput{}, nil)
   140  			},
   141  			err: errors.New(`no nat gateways available in "us-east-1a"`),
   142  		},
   143  		{
   144  			name: "routes exist, but the nat gateway ID is incorrect, replaces it",
   145  			input: &infrav1.NetworkSpec{
   146  				VPC: infrav1.VPCSpec{
   147  					InternetGatewayID: aws.String("igw-01"),
   148  					ID:                "vpc-routetables",
   149  					Tags: infrav1.Tags{
   150  						infrav1.ClusterTagKey("test-cluster"): "owned",
   151  					},
   152  				},
   153  				Subnets: infrav1.Subnets{
   154  					infrav1.SubnetSpec{
   155  						ID:               "subnet-routetables-private",
   156  						IsPublic:         false,
   157  						AvailabilityZone: "us-east-1a",
   158  					},
   159  					infrav1.SubnetSpec{
   160  						ID:               "subnet-routetables-public",
   161  						IsPublic:         true,
   162  						NatGatewayID:     aws.String("nat-01"),
   163  						AvailabilityZone: "us-east-1a",
   164  						RouteTableID:     aws.String("route-table-1"),
   165  					},
   166  				},
   167  			},
   168  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   169  				m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})).
   170  					Return(&ec2.DescribeRouteTablesOutput{
   171  						RouteTables: []*ec2.RouteTable{
   172  							{
   173  								RouteTableId: aws.String("route-table-private"),
   174  								Associations: []*ec2.RouteTableAssociation{
   175  									{
   176  										SubnetId: aws.String("subnet-routetables-private"),
   177  									},
   178  								},
   179  								Routes: []*ec2.Route{
   180  									{
   181  										DestinationCidrBlock: aws.String("0.0.0.0/0"),
   182  										NatGatewayId:         aws.String("outdated-nat-01"),
   183  									},
   184  								},
   185  								Tags: []*ec2.Tag{
   186  									{
   187  										Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   188  										Value: aws.String("common"),
   189  									},
   190  									{
   191  										Key:   aws.String("Name"),
   192  										Value: aws.String("test-cluster-rt-private-us-east-1a"),
   193  									},
   194  									{
   195  										Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   196  										Value: aws.String("owned"),
   197  									},
   198  								},
   199  							},
   200  							{
   201  								RouteTableId: aws.String("route-table-public"),
   202  								Associations: []*ec2.RouteTableAssociation{
   203  									{
   204  										SubnetId: aws.String("subnet-routetables-public"),
   205  									},
   206  								},
   207  								Routes: []*ec2.Route{
   208  									{
   209  										DestinationCidrBlock: aws.String("0.0.0.0/0"),
   210  										GatewayId:            aws.String("igw-01"),
   211  									},
   212  								},
   213  								Tags: []*ec2.Tag{
   214  									{
   215  										Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   216  										Value: aws.String("common"),
   217  									},
   218  									{
   219  										Key:   aws.String("Name"),
   220  										Value: aws.String("test-cluster-rt-public-us-east-1a"),
   221  									},
   222  									{
   223  										Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   224  										Value: aws.String("owned"),
   225  									},
   226  								},
   227  							},
   228  						},
   229  					}, nil)
   230  
   231  				m.ReplaceRoute(gomock.Eq(
   232  					&ec2.ReplaceRouteInput{
   233  						DestinationCidrBlock: aws.String("0.0.0.0/0"),
   234  						RouteTableId:         aws.String("route-table-private"),
   235  						NatGatewayId:         aws.String("nat-01"),
   236  					},
   237  				)).
   238  					Return(nil, nil)
   239  			},
   240  		},
   241  		{
   242  			name: "extra routes exist, do nothing",
   243  			input: &infrav1.NetworkSpec{
   244  				VPC: infrav1.VPCSpec{
   245  					InternetGatewayID: aws.String("igw-01"),
   246  					ID:                "vpc-routetables",
   247  					Tags: infrav1.Tags{
   248  						infrav1.ClusterTagKey("test-cluster"): "owned",
   249  					},
   250  				},
   251  				Subnets: infrav1.Subnets{
   252  					infrav1.SubnetSpec{
   253  						ID:               "subnet-routetables-private",
   254  						IsPublic:         false,
   255  						AvailabilityZone: "us-east-1a",
   256  					},
   257  					infrav1.SubnetSpec{
   258  						ID:               "subnet-routetables-public",
   259  						IsPublic:         true,
   260  						NatGatewayID:     aws.String("nat-01"),
   261  						AvailabilityZone: "us-east-1a",
   262  					},
   263  				},
   264  			},
   265  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   266  				m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})).
   267  					Return(&ec2.DescribeRouteTablesOutput{
   268  						RouteTables: []*ec2.RouteTable{
   269  							{
   270  								RouteTableId: aws.String("route-table-private"),
   271  								Associations: []*ec2.RouteTableAssociation{
   272  									{
   273  										SubnetId: aws.String("subnet-routetables-private"),
   274  									},
   275  								},
   276  								Routes: []*ec2.Route{
   277  									{
   278  										DestinationCidrBlock: aws.String("0.0.0.0/0"),
   279  										NatGatewayId:         aws.String("nat-01"),
   280  									},
   281  									// Extra (managed outside of CAPA) route with Managed Prefix List destination.
   282  									{
   283  										DestinationPrefixListId: aws.String("pl-foobar"),
   284  									},
   285  								},
   286  								Tags: []*ec2.Tag{
   287  									{
   288  										Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   289  										Value: aws.String("common"),
   290  									},
   291  									{
   292  										Key:   aws.String("Name"),
   293  										Value: aws.String("test-cluster-rt-private-us-east-1a"),
   294  									},
   295  									{
   296  										Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   297  										Value: aws.String("owned"),
   298  									},
   299  								},
   300  							},
   301  							{
   302  								RouteTableId: aws.String("route-table-public"),
   303  								Associations: []*ec2.RouteTableAssociation{
   304  									{
   305  										SubnetId: aws.String("subnet-routetables-public"),
   306  									},
   307  								},
   308  								Routes: []*ec2.Route{
   309  									{
   310  										DestinationCidrBlock: aws.String("0.0.0.0/0"),
   311  										GatewayId:            aws.String("igw-01"),
   312  									},
   313  								},
   314  								Tags: []*ec2.Tag{
   315  									{
   316  										Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   317  										Value: aws.String("common"),
   318  									},
   319  									{
   320  										Key:   aws.String("Name"),
   321  										Value: aws.String("test-cluster-rt-public-us-east-1a"),
   322  									},
   323  									{
   324  										Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   325  										Value: aws.String("owned"),
   326  									},
   327  								},
   328  							},
   329  						},
   330  					}, nil)
   331  			},
   332  		},
   333  	}
   334  
   335  	for _, tc := range testCases {
   336  		t.Run(tc.name, func(t *testing.T) {
   337  			ec2Mock := mocks.NewMockEC2API(mockCtrl)
   338  
   339  			scheme := runtime.NewScheme()
   340  			_ = infrav1.AddToScheme(scheme)
   341  			client := fake.NewClientBuilder().WithScheme(scheme).Build()
   342  			scope, err := scope.NewClusterScope(scope.ClusterScopeParams{
   343  				Client: client,
   344  				Cluster: &clusterv1.Cluster{
   345  					ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"},
   346  				},
   347  				AWSCluster: &infrav1.AWSCluster{
   348  					ObjectMeta: metav1.ObjectMeta{Name: "test"},
   349  					Spec: infrav1.AWSClusterSpec{
   350  						NetworkSpec: *tc.input,
   351  					},
   352  				},
   353  			})
   354  			if err != nil {
   355  				t.Fatalf("Failed to create test context: %v", err)
   356  			}
   357  
   358  			tc.expect(ec2Mock.EXPECT())
   359  
   360  			s := NewService(scope)
   361  			s.EC2Client = ec2Mock
   362  
   363  			if err := s.reconcileRouteTables(); err != nil && tc.err != nil {
   364  				if !strings.Contains(err.Error(), tc.err.Error()) {
   365  					t.Fatalf("was expecting error to look like '%v', but got '%v'", tc.err, err)
   366  				}
   367  			} else if err != nil {
   368  				t.Fatalf("got an unexpected error: %v", err)
   369  			}
   370  		})
   371  	}
   372  }
   373  
   374  func TestDeleteRouteTables(t *testing.T) {
   375  	mockCtrl := gomock.NewController(t)
   376  	defer mockCtrl.Finish()
   377  
   378  	describeRouteTableOutput := &ec2.DescribeRouteTablesOutput{
   379  		RouteTables: []*ec2.RouteTable{
   380  			{
   381  				RouteTableId: aws.String("route-table-private"),
   382  				Associations: []*ec2.RouteTableAssociation{
   383  					{
   384  						SubnetId: nil,
   385  					},
   386  				},
   387  				Routes: []*ec2.Route{
   388  					{
   389  						DestinationCidrBlock: aws.String("0.0.0.0/0"),
   390  						NatGatewayId:         aws.String("outdated-nat-01"),
   391  					},
   392  				},
   393  			},
   394  			{
   395  				RouteTableId: aws.String("route-table-public"),
   396  				Associations: []*ec2.RouteTableAssociation{
   397  					{
   398  						SubnetId:                aws.String("subnet-routetables-public"),
   399  						RouteTableAssociationId: aws.String("route-table-public"),
   400  					},
   401  				},
   402  				Routes: []*ec2.Route{
   403  					{
   404  						DestinationCidrBlock: aws.String("0.0.0.0/0"),
   405  						GatewayId:            aws.String("igw-01"),
   406  					},
   407  				},
   408  				Tags: []*ec2.Tag{
   409  					{
   410  						Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   411  						Value: aws.String("common"),
   412  					},
   413  					{
   414  						Key:   aws.String("Name"),
   415  						Value: aws.String("test-cluster-rt-public-us-east-1a"),
   416  					},
   417  					{
   418  						Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
   419  						Value: aws.String("owned"),
   420  					},
   421  				},
   422  			},
   423  		},
   424  	}
   425  
   426  	testCases := []struct {
   427  		name    string
   428  		input   *infrav1.NetworkSpec
   429  		expect  func(m *mocks.MockEC2APIMockRecorder)
   430  		wantErr bool
   431  	}{
   432  		{
   433  			name: "Should skip deletion if vpc is unmanaged",
   434  			input: &infrav1.NetworkSpec{
   435  				VPC: infrav1.VPCSpec{
   436  					ID:   "vpc-routetables",
   437  					Tags: infrav1.Tags{},
   438  				},
   439  			},
   440  		},
   441  		{
   442  			name:  "Should delete route table successfully",
   443  			input: &infrav1.NetworkSpec{},
   444  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   445  				m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})).
   446  					Return(describeRouteTableOutput, nil)
   447  
   448  				m.DeleteRouteTable(gomock.Eq(&ec2.DeleteRouteTableInput{
   449  					RouteTableId: aws.String("route-table-private"),
   450  				})).Return(&ec2.DeleteRouteTableOutput{}, nil)
   451  
   452  				m.DisassociateRouteTable(gomock.Eq(&ec2.DisassociateRouteTableInput{
   453  					AssociationId: aws.String("route-table-public"),
   454  				})).Return(&ec2.DisassociateRouteTableOutput{}, nil)
   455  
   456  				m.DeleteRouteTable(gomock.Eq(&ec2.DeleteRouteTableInput{
   457  					RouteTableId: aws.String("route-table-public"),
   458  				})).Return(&ec2.DeleteRouteTableOutput{}, nil)
   459  			},
   460  		},
   461  		{
   462  			name:  "Should return error if describe route table fails",
   463  			input: &infrav1.NetworkSpec{},
   464  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   465  				m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})).
   466  					Return(nil, awserrors.NewFailedDependency("failed dependency"))
   467  			},
   468  			wantErr: true,
   469  		},
   470  		{
   471  			name:  "Should return error if delete route table fails",
   472  			input: &infrav1.NetworkSpec{},
   473  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   474  				m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})).
   475  					Return(describeRouteTableOutput, nil)
   476  
   477  				m.DeleteRouteTable(gomock.Eq(&ec2.DeleteRouteTableInput{
   478  					RouteTableId: aws.String("route-table-private"),
   479  				})).Return(nil, awserrors.NewNotFound("not found"))
   480  			},
   481  			wantErr: true,
   482  		},
   483  		{
   484  			name:  "Should return error if disassociate route table fails",
   485  			input: &infrav1.NetworkSpec{},
   486  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   487  				m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})).
   488  					Return(describeRouteTableOutput, nil)
   489  
   490  				m.DeleteRouteTable(gomock.Eq(&ec2.DeleteRouteTableInput{
   491  					RouteTableId: aws.String("route-table-private"),
   492  				})).Return(&ec2.DeleteRouteTableOutput{}, nil)
   493  
   494  				m.DisassociateRouteTable(gomock.Eq(&ec2.DisassociateRouteTableInput{
   495  					AssociationId: aws.String("route-table-public"),
   496  				})).Return(nil, awserrors.NewNotFound("not found"))
   497  			},
   498  			wantErr: true,
   499  		},
   500  	}
   501  
   502  	for _, tc := range testCases {
   503  		t.Run(tc.name, func(t *testing.T) {
   504  			g := NewWithT(t)
   505  			ec2Mock := mocks.NewMockEC2API(mockCtrl)
   506  
   507  			scheme := runtime.NewScheme()
   508  			_ = infrav1.AddToScheme(scheme)
   509  			client := fake.NewClientBuilder().WithScheme(scheme).Build()
   510  			scope, err := scope.NewClusterScope(scope.ClusterScopeParams{
   511  				Client: client,
   512  				Cluster: &clusterv1.Cluster{
   513  					ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"},
   514  				},
   515  				AWSCluster: &infrav1.AWSCluster{
   516  					ObjectMeta: metav1.ObjectMeta{Name: "test"},
   517  					Spec: infrav1.AWSClusterSpec{
   518  						NetworkSpec: *tc.input,
   519  					},
   520  				},
   521  			})
   522  			g.Expect(err).NotTo(HaveOccurred())
   523  			if tc.expect != nil {
   524  				tc.expect(ec2Mock.EXPECT())
   525  			}
   526  
   527  			s := NewService(scope)
   528  			s.EC2Client = ec2Mock
   529  
   530  			err = s.deleteRouteTables()
   531  			if tc.wantErr {
   532  				g.Expect(err).To(HaveOccurred())
   533  				return
   534  			}
   535  			g.Expect(err).NotTo(HaveOccurred())
   536  		})
   537  	}
   538  }
   539  
   540  type routeTableInputMatcher struct {
   541  	routeTableInput *ec2.CreateRouteTableInput
   542  }
   543  
   544  func (r routeTableInputMatcher) Matches(x interface{}) bool {
   545  	actual, ok := x.(*ec2.CreateRouteTableInput)
   546  	if !ok {
   547  		fmt.Println("heeeeyy")
   548  		return false
   549  	}
   550  	if *actual.VpcId != *r.routeTableInput.VpcId {
   551  		return false
   552  	}
   553  
   554  	return true
   555  }
   556  
   557  func (r routeTableInputMatcher) String() string {
   558  	return fmt.Sprintf("partially matches %v", r.routeTableInput)
   559  }
   560  
   561  func matchRouteTableInput(input *ec2.CreateRouteTableInput) gomock.Matcher {
   562  	return routeTableInputMatcher{routeTableInput: input}
   563  }