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 }