sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/network/routetables.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  	"strings"
    21  
    22  	"github.com/aws/aws-sdk-go/aws"
    23  	"github.com/aws/aws-sdk-go/service/ec2"
    24  	"github.com/pkg/errors"
    25  
    26  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    27  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
    28  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/converters"
    29  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/filter"
    30  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services"
    31  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/wait"
    32  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/tags"
    33  	"sigs.k8s.io/cluster-api-provider-aws/pkg/record"
    34  	"sigs.k8s.io/cluster-api/util/conditions"
    35  )
    36  
    37  const (
    38  	mainRouteTableInVPCKey = "main"
    39  )
    40  
    41  func (s *Service) reconcileRouteTables() error {
    42  	if s.scope.VPC().IsUnmanaged(s.scope.Name()) {
    43  		s.scope.V(4).Info("Skipping routing tables reconcile in unmanaged mode")
    44  		return nil
    45  	}
    46  
    47  	s.scope.V(2).Info("Reconciling routing tables")
    48  
    49  	subnetRouteMap, err := s.describeVpcRouteTablesBySubnet()
    50  	if err != nil {
    51  		return err
    52  	}
    53  
    54  	subnets := s.scope.Subnets()
    55  	for i := range subnets {
    56  		sn := subnets[i]
    57  		// We need to compile the minimum routes for this subnet first, so we can compare it or create them.
    58  		var routes []*ec2.Route
    59  		if sn.IsPublic {
    60  			if s.scope.VPC().InternetGatewayID == nil {
    61  				return errors.Errorf("failed to create routing tables: internet gateway for %q is nil", s.scope.VPC().ID)
    62  			}
    63  			routes = append(routes, s.getGatewayPublicRoute())
    64  		} else {
    65  			natGatewayID, err := s.getNatGatewayForSubnet(&sn)
    66  			if err != nil {
    67  				return err
    68  			}
    69  			routes = append(routes, s.getNatGatewayPrivateRoute(natGatewayID))
    70  		}
    71  
    72  		if rt, ok := subnetRouteMap[sn.ID]; ok {
    73  			s.scope.V(2).Info("Subnet is already associated with route table", "subnet-id", sn.ID, "route-table-id", *rt.RouteTableId)
    74  			// TODO(vincepri): check that everything is in order, e.g. routes match the subnet type.
    75  
    76  			// For managed environments we need to reconcile the routes of our tables if there is a mistmatch.
    77  			// For example, a gateway can be deleted and our controller will re-create it, then we replace the route
    78  			// for the subnet to allow traffic to flow.
    79  			for _, currentRoute := range rt.Routes {
    80  				for i := range routes {
    81  					// Routes destination cidr blocks must be unique within a routing table.
    82  					// If there is a mistmatch, we replace the routing association.
    83  					specRoute := routes[i]
    84  					if (currentRoute.DestinationCidrBlock != nil && // Manually-created routes can have .DestinationIpv6CidrBlock or .DestinationPrefixListId set instead.
    85  						*currentRoute.DestinationCidrBlock == *specRoute.DestinationCidrBlock) &&
    86  						((currentRoute.GatewayId != nil && *currentRoute.GatewayId != *specRoute.GatewayId) ||
    87  							(currentRoute.NatGatewayId != nil && *currentRoute.NatGatewayId != *specRoute.NatGatewayId)) {
    88  						if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
    89  							if _, err := s.EC2Client.ReplaceRoute(&ec2.ReplaceRouteInput{
    90  								RouteTableId:         rt.RouteTableId,
    91  								DestinationCidrBlock: specRoute.DestinationCidrBlock,
    92  								GatewayId:            specRoute.GatewayId,
    93  								NatGatewayId:         specRoute.NatGatewayId,
    94  							}); err != nil {
    95  								return false, err
    96  							}
    97  							return true, nil
    98  						}); err != nil {
    99  							record.Warnf(s.scope.InfraCluster(), "FailedReplaceRoute", "Failed to replace outdated route on managed RouteTable %q: %v", *rt.RouteTableId, err)
   100  							return errors.Wrapf(err, "failed to replace outdated route on route table %q", *rt.RouteTableId)
   101  						}
   102  					}
   103  				}
   104  			}
   105  
   106  			// Make sure tags are up to date.
   107  			if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
   108  				buildParams := s.getRouteTableTagParams(*rt.RouteTableId, sn.IsPublic, sn.AvailabilityZone)
   109  				tagsBuilder := tags.New(&buildParams, tags.WithEC2(s.EC2Client))
   110  				if err := tagsBuilder.Ensure(converters.TagsToMap(rt.Tags)); err != nil {
   111  					return false, err
   112  				}
   113  				return true, nil
   114  			}, awserrors.RouteTableNotFound); err != nil {
   115  				record.Warnf(s.scope.InfraCluster(), "FailedTagRouteTable", "Failed to tag managed RouteTable %q: %v", *rt.RouteTableId, err)
   116  				return errors.Wrapf(err, "failed to ensure tags on route table %q", *rt.RouteTableId)
   117  			}
   118  
   119  			// Not recording "SuccessfulTagRouteTable" here as we don't know if this was a no-op or an actual change
   120  			continue
   121  		}
   122  
   123  		// For each subnet that doesn't have a routing table associated with it,
   124  		// create a new table with the appropriate default routes and associate it to the subnet.
   125  		rt, err := s.createRouteTableWithRoutes(routes, sn.IsPublic, sn.AvailabilityZone)
   126  		if err != nil {
   127  			return err
   128  		}
   129  
   130  		if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
   131  			if err := s.associateRouteTable(rt, sn.ID); err != nil {
   132  				s.scope.Error(err, "trying to associate route table", "subnet_id", sn.ID)
   133  				return false, err
   134  			}
   135  			return true, nil
   136  		}, awserrors.RouteTableNotFound, awserrors.SubnetNotFound); err != nil {
   137  			return err
   138  		}
   139  
   140  		s.scope.V(2).Info("Subnet has been associated with route table", "subnet-id", sn.ID, "route-table-id", rt.ID)
   141  		sn.RouteTableID = aws.String(rt.ID)
   142  	}
   143  	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.RouteTablesReadyCondition)
   144  	return nil
   145  }
   146  
   147  func (s *Service) describeVpcRouteTablesBySubnet() (map[string]*ec2.RouteTable, error) {
   148  	rts, err := s.describeVpcRouteTables()
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	// Amazon allows a subnet to be associated only with a single routing table
   154  	// https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Route_Tables.html.
   155  	res := make(map[string]*ec2.RouteTable)
   156  	for _, rt := range rts {
   157  		for _, as := range rt.Associations {
   158  			if as.Main != nil && *as.Main {
   159  				res[mainRouteTableInVPCKey] = rt
   160  			}
   161  			if as.SubnetId == nil {
   162  				continue
   163  			}
   164  
   165  			res[*as.SubnetId] = rt
   166  		}
   167  	}
   168  
   169  	return res, nil
   170  }
   171  
   172  func (s *Service) deleteRouteTables() error {
   173  	if s.scope.VPC().IsUnmanaged(s.scope.Name()) {
   174  		s.scope.V(4).Info("Skipping routing tables deletion in unmanaged mode")
   175  		return nil
   176  	}
   177  
   178  	rts, err := s.describeVpcRouteTables()
   179  	if err != nil {
   180  		return errors.Wrapf(err, "failed to describe route tables in vpc %q", s.scope.VPC().ID)
   181  	}
   182  
   183  	for _, rt := range rts {
   184  		for _, as := range rt.Associations {
   185  			if as.SubnetId == nil {
   186  				continue
   187  			}
   188  
   189  			if _, err := s.EC2Client.DisassociateRouteTable(&ec2.DisassociateRouteTableInput{AssociationId: as.RouteTableAssociationId}); err != nil {
   190  				record.Warnf(s.scope.InfraCluster(), "FailedDisassociateRouteTable", "Failed to disassociate managed RouteTable %q from Subnet %q: %v", *rt.RouteTableId, *as.SubnetId, err)
   191  				return errors.Wrapf(err, "failed to disassociate route table %q from subnet %q", *rt.RouteTableId, *as.SubnetId)
   192  			}
   193  
   194  			record.Eventf(s.scope.InfraCluster(), "SuccessfulDisassociateRouteTable", "Disassociated managed RouteTable %q from subnet %q", *rt.RouteTableId, *as.SubnetId)
   195  			s.scope.V(2).Info("Deleted association between route table and subnet", "route-table-id", *rt.RouteTableId, "subnet-id", *as.SubnetId)
   196  		}
   197  
   198  		if _, err := s.EC2Client.DeleteRouteTable(&ec2.DeleteRouteTableInput{RouteTableId: rt.RouteTableId}); err != nil {
   199  			record.Warnf(s.scope.InfraCluster(), "FailedDeleteRouteTable", "Failed to delete managed RouteTable %q: %v", *rt.RouteTableId, err)
   200  			return errors.Wrapf(err, "failed to delete route table %q", *rt.RouteTableId)
   201  		}
   202  
   203  		record.Eventf(s.scope.InfraCluster(), "SuccessfulDeleteRouteTable", "Deleted managed RouteTable %q", *rt.RouteTableId)
   204  		s.scope.Info("Deleted route table", "route-table-id", *rt.RouteTableId)
   205  	}
   206  	return nil
   207  }
   208  
   209  func (s *Service) describeVpcRouteTables() ([]*ec2.RouteTable, error) {
   210  	filters := []*ec2.Filter{
   211  		filter.EC2.VPC(s.scope.VPC().ID),
   212  	}
   213  
   214  	if !s.scope.VPC().IsUnmanaged(s.scope.Name()) {
   215  		filters = append(filters, filter.EC2.Cluster(s.scope.Name()))
   216  	}
   217  
   218  	out, err := s.EC2Client.DescribeRouteTables(&ec2.DescribeRouteTablesInput{
   219  		Filters: filters,
   220  	})
   221  	if err != nil {
   222  		record.Eventf(s.scope.InfraCluster(), "FailedDescribeVPCRouteTable", "Failed to describe route tables in vpc %q: %v", s.scope.VPC().ID, err)
   223  		return nil, errors.Wrapf(err, "failed to describe route tables in vpc %q", s.scope.VPC().ID)
   224  	}
   225  
   226  	return out.RouteTables, nil
   227  }
   228  
   229  func (s *Service) createRouteTableWithRoutes(routes []*ec2.Route, isPublic bool, zone string) (*infrav1.RouteTable, error) {
   230  	out, err := s.EC2Client.CreateRouteTable(&ec2.CreateRouteTableInput{
   231  		VpcId: aws.String(s.scope.VPC().ID),
   232  		TagSpecifications: []*ec2.TagSpecification{
   233  			tags.BuildParamsToTagSpecification(ec2.ResourceTypeRouteTable, s.getRouteTableTagParams(services.TemporaryResourceID, isPublic, zone))},
   234  	})
   235  	if err != nil {
   236  		record.Warnf(s.scope.InfraCluster(), "FailedCreateRouteTable", "Failed to create managed RouteTable: %v", err)
   237  		return nil, errors.Wrapf(err, "failed to create route table in vpc %q", s.scope.VPC().ID)
   238  	}
   239  	record.Eventf(s.scope.InfraCluster(), "SuccessfulCreateRouteTable", "Created managed RouteTable %q", *out.RouteTable.RouteTableId)
   240  	s.scope.Info("Created route table", "route-table-id", *out.RouteTable.RouteTableId)
   241  
   242  	for i := range routes {
   243  		route := routes[i]
   244  		if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
   245  			if _, err := s.EC2Client.CreateRoute(&ec2.CreateRouteInput{
   246  				RouteTableId:                out.RouteTable.RouteTableId,
   247  				DestinationCidrBlock:        route.DestinationCidrBlock,
   248  				DestinationIpv6CidrBlock:    route.DestinationIpv6CidrBlock,
   249  				EgressOnlyInternetGatewayId: route.EgressOnlyInternetGatewayId,
   250  				GatewayId:                   route.GatewayId,
   251  				InstanceId:                  route.InstanceId,
   252  				NatGatewayId:                route.NatGatewayId,
   253  				NetworkInterfaceId:          route.NetworkInterfaceId,
   254  				VpcPeeringConnectionId:      route.VpcPeeringConnectionId,
   255  			}); err != nil {
   256  				return false, err
   257  			}
   258  			return true, nil
   259  		}, awserrors.RouteTableNotFound, awserrors.NATGatewayNotFound, awserrors.GatewayNotFound); err != nil {
   260  			// TODO(vincepri): cleanup the route table if this fails.
   261  			record.Warnf(s.scope.InfraCluster(), "FailedCreateRoute", "Failed to create route %s for RouteTable %q: %v", route.GoString(), *out.RouteTable.RouteTableId, err)
   262  			return nil, errors.Wrapf(err, "failed to create route in route table %q: %s", *out.RouteTable.RouteTableId, route.GoString())
   263  		}
   264  		record.Eventf(s.scope.InfraCluster(), "SuccessfulCreateRoute", "Created route %s for RouteTable %q", route.GoString(), *out.RouteTable.RouteTableId)
   265  	}
   266  
   267  	return &infrav1.RouteTable{
   268  		ID: *out.RouteTable.RouteTableId,
   269  	}, nil
   270  }
   271  
   272  func (s *Service) associateRouteTable(rt *infrav1.RouteTable, subnetID string) error {
   273  	_, err := s.EC2Client.AssociateRouteTable(&ec2.AssociateRouteTableInput{
   274  		RouteTableId: aws.String(rt.ID),
   275  		SubnetId:     aws.String(subnetID),
   276  	})
   277  	if err != nil {
   278  		record.Warnf(s.scope.InfraCluster(), "FailedAssociateRouteTable", "Failed to associate managed RouteTable %q with Subnet %q: %v", rt.ID, subnetID, err)
   279  		return errors.Wrapf(err, "failed to associate route table %q to subnet %q", rt.ID, subnetID)
   280  	}
   281  
   282  	record.Eventf(s.scope.InfraCluster(), "SuccessfulAssociateRouteTable", "Associated managed RouteTable %q with subnet %q", rt.ID, subnetID)
   283  
   284  	return nil
   285  }
   286  
   287  func (s *Service) getNatGatewayPrivateRoute(natGatewayID string) *ec2.Route {
   288  	return &ec2.Route{
   289  		DestinationCidrBlock: aws.String(services.AnyIPv4CidrBlock),
   290  		NatGatewayId:         aws.String(natGatewayID),
   291  	}
   292  }
   293  
   294  func (s *Service) getGatewayPublicRoute() *ec2.Route {
   295  	return &ec2.Route{
   296  		DestinationCidrBlock: aws.String(services.AnyIPv4CidrBlock),
   297  		GatewayId:            aws.String(*s.scope.VPC().InternetGatewayID),
   298  	}
   299  }
   300  
   301  func (s *Service) getRouteTableTagParams(id string, public bool, zone string) infrav1.BuildParams {
   302  	var name strings.Builder
   303  
   304  	name.WriteString(s.scope.Name())
   305  	name.WriteString("-rt-")
   306  	if public {
   307  		name.WriteString("public")
   308  	} else {
   309  		name.WriteString("private")
   310  	}
   311  	name.WriteString("-")
   312  	name.WriteString(zone)
   313  
   314  	return infrav1.BuildParams{
   315  		ClusterName: s.scope.Name(),
   316  		ResourceID:  id,
   317  		Lifecycle:   infrav1.ResourceLifecycleOwned,
   318  		Name:        aws.String(name.String()),
   319  		Role:        aws.String(infrav1.CommonRoleTagValue),
   320  		Additional:  s.scope.AdditionalTags(),
   321  	}
   322  }