sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/network/subnets.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 "math/rand" 22 "sort" 23 "strings" 24 25 "github.com/aws/aws-sdk-go/aws" 26 "github.com/aws/aws-sdk-go/service/ec2" 27 "github.com/pkg/errors" 28 29 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 30 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors" 31 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/converters" 32 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/filter" 33 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services" 34 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/wait" 35 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/tags" 36 "sigs.k8s.io/cluster-api-provider-aws/pkg/internal/cidr" 37 "sigs.k8s.io/cluster-api-provider-aws/pkg/record" 38 "sigs.k8s.io/cluster-api/util/conditions" 39 ) 40 41 const ( 42 internalLoadBalancerTag = "kubernetes.io/role/internal-elb" 43 externalLoadBalancerTag = "kubernetes.io/role/elb" 44 defaultMaxNumAZs = 3 45 ) 46 47 func (s *Service) reconcileSubnets() error { 48 s.scope.Info("Reconciling subnets") 49 50 subnets := s.scope.Subnets() 51 defer func() { 52 s.scope.SetSubnets(subnets) 53 }() 54 55 // Describe subnets in the vpc. 56 existing, err := s.describeVpcSubnets() 57 if err != nil { 58 return err 59 } 60 61 unmanagedVPC := s.scope.VPC().IsUnmanaged(s.scope.Name()) 62 63 if len(subnets) == 0 { 64 if unmanagedVPC { 65 // If we have a unmanaged VPC then subnets must be specified 66 errMsg := "no subnets specified, you must specify the subnets when using an umanaged vpc" 67 record.Warnf(s.scope.InfraCluster(), "FailedNoSubnets", errMsg) 68 return errors.New(errMsg) 69 } 70 // If we a managed VPC and have no subnets then create subnets. There will be 1 public and 1 private subnet 71 // for each az in a region up to a maximum of 3 azs 72 s.scope.Info("no subnets specified, setting defaults") 73 subnets, err = s.getDefaultSubnets() 74 if err != nil { 75 record.Warnf(s.scope.InfraCluster(), "FailedDefaultSubnets", "Failed getting default subnets: %v", err) 76 return errors.Wrap(err, "failed getting default subnets") 77 } 78 // Persist the new default subnets to AWSCluster 79 if err := s.scope.PatchObject(); err != nil { 80 s.scope.Error(err, "failed to patch object to save subnets") 81 return err 82 } 83 } 84 85 if s.scope.SecondaryCidrBlock() != nil { 86 subnetCIDRs, err := cidr.SplitIntoSubnetsIPv4(*s.scope.SecondaryCidrBlock(), *s.scope.VPC().AvailabilityZoneUsageLimit) 87 if err != nil { 88 return err 89 } 90 91 zones, err := s.getAvailableZones() 92 if err != nil { 93 return err 94 } 95 96 for i, sub := range subnetCIDRs { 97 secondarySub := infrav1.SubnetSpec{ 98 CidrBlock: sub.String(), 99 AvailabilityZone: zones[i], 100 IsPublic: false, 101 Tags: infrav1.Tags{ 102 infrav1.NameAWSSubnetAssociation: infrav1.SecondarySubnetTagValue, 103 }, 104 } 105 existingSubnet := existing.FindEqual(&secondarySub) 106 if existingSubnet == nil { 107 subnets = append(subnets, secondarySub) 108 } 109 } 110 } 111 112 for i := range subnets { 113 sub := &subnets[i] 114 existingSubnet := existing.FindEqual(sub) 115 if existingSubnet != nil { 116 subnetTags := sub.Tags 117 // Make sure tags are up-to-date. 118 if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) { 119 buildParams := s.getSubnetTagParams(unmanagedVPC, existingSubnet.ID, existingSubnet.IsPublic, existingSubnet.AvailabilityZone, subnetTags) 120 tagsBuilder := tags.New(&buildParams, tags.WithEC2(s.EC2Client)) 121 if err := tagsBuilder.Ensure(existingSubnet.Tags); err != nil { 122 return false, err 123 } 124 return true, nil 125 }, awserrors.SubnetNotFound); err != nil { 126 if !unmanagedVPC { 127 record.Warnf(s.scope.InfraCluster(), "FailedTagSubnet", "Failed tagging managed Subnet %q: %v", existingSubnet.ID, err) 128 return errors.Wrapf(err, "failed to ensure tags on subnet %q", existingSubnet.ID) 129 } else { 130 // We may not have a permission to tag unmanaged subnets. 131 // When tagging unmanaged subnet fails, record an event and proceed. 132 record.Warnf(s.scope.InfraCluster(), "FailedTagSubnet", "Failed tagging unmanaged Subnet %q: %v", existingSubnet.ID, err) 133 break 134 } 135 } 136 137 // Update subnet spec with the existing subnet details 138 // TODO(vincepri): check if subnet needs to be updated. 139 existingSubnet.DeepCopyInto(sub) 140 } else if unmanagedVPC { 141 // If there is no existing subnet and we have an umanaged vpc report an error 142 record.Warnf(s.scope.InfraCluster(), "FailedMatchSubnet", "Using unmanaged VPC and failed to find existing subnet for specified subnet id %d, cidr %q", sub.ID, sub.CidrBlock) 143 return errors.New(fmt.Errorf("usign unmanaged vpc and subnet %s (cidr %s) specified but it doesn't exist in vpc %s", sub.ID, sub.CidrBlock, s.scope.VPC().ID).Error()) 144 } 145 } 146 147 if !unmanagedVPC { 148 // Check that we need at least 1 private and 1 public subnet after we have updated the metadata 149 if len(subnets.FilterPrivate()) < 1 { 150 record.Warnf(s.scope.InfraCluster(), "FailedNoPrivateSubnet", "Expected at least 1 private subnet but got 0") 151 return errors.New("expected at least 1 private subnet but got 0") 152 } 153 if len(subnets.FilterPublic()) < 1 { 154 record.Warnf(s.scope.InfraCluster(), "FailedNoPublicSubnet", "Expected at least 1 public subnet but got 0") 155 return errors.New("expected at least 1 public subnet but got 0") 156 } 157 } else if unmanagedVPC { 158 if len(subnets) < 1 { 159 record.Warnf(s.scope.InfraCluster(), "FailedNoSubnet", "Expected at least 1 subnet but got 0") 160 return errors.New("expected at least 1 subnet but got 0") 161 } 162 } 163 164 // Proceed to create the rest of the subnets that don't have an ID. 165 if !unmanagedVPC { 166 for i := range subnets { 167 subnet := &subnets[i] 168 if subnet.ID != "" { 169 continue 170 } 171 172 nsn, err := s.createSubnet(subnet) 173 if err != nil { 174 return err 175 } 176 nsn.DeepCopyInto(subnet) 177 } 178 } 179 180 s.scope.V(2).Info("reconciled subnets", "subnets", subnets) 181 conditions.MarkTrue(s.scope.InfraCluster(), infrav1.SubnetsReadyCondition) 182 return nil 183 } 184 185 func (s *Service) getDefaultSubnets() (infrav1.Subnets, error) { 186 zones, err := s.getAvailableZones() 187 if err != nil { 188 return nil, err 189 } 190 191 maxZones := defaultMaxNumAZs 192 if s.scope.VPC().AvailabilityZoneUsageLimit != nil { 193 maxZones = *s.scope.VPC().AvailabilityZoneUsageLimit 194 } 195 selectionScheme := infrav1.AZSelectionSchemeOrdered 196 if s.scope.VPC().AvailabilityZoneSelection != nil { 197 selectionScheme = *s.scope.VPC().AvailabilityZoneSelection 198 } 199 200 if len(zones) > maxZones { 201 s.scope.V(2).Info("region has more than AvailabilityZoneUsageLimit availability zones, picking zones to use", "region", s.scope.Region(), "AvailabilityZoneUsageLimit", maxZones) 202 if selectionScheme == infrav1.AZSelectionSchemeRandom { 203 rand.Shuffle(len(zones), func(i, j int) { 204 zones[i], zones[j] = zones[j], zones[i] 205 }) 206 } 207 if selectionScheme == infrav1.AZSelectionSchemeOrdered { 208 sort.Strings(zones) 209 } 210 zones = zones[:maxZones] 211 s.scope.V(2).Info("zones selected", "region", s.scope.Region(), "zones", zones) 212 } 213 214 // 1 private subnet for each AZ plus 1 other subnet that will be further sub-divided for the public subnets 215 numSubnets := len(zones) + 1 216 subnetCIDRs, err := cidr.SplitIntoSubnetsIPv4(s.scope.VPC().CidrBlock, numSubnets) 217 if err != nil { 218 return nil, errors.Wrapf(err, "failed splitting VPC CIDR %s into subnets", s.scope.VPC().CidrBlock) 219 } 220 221 publicSubnetCIDRs, err := cidr.SplitIntoSubnetsIPv4(subnetCIDRs[0].String(), len(zones)) 222 if err != nil { 223 return nil, errors.Wrapf(err, "failed splitting CIDR %s into public subnets", subnetCIDRs[0].String()) 224 } 225 privateSubnetCIDRs := append(subnetCIDRs[:0], subnetCIDRs[1:]...) 226 227 subnets := infrav1.Subnets{} 228 for i, zone := range zones { 229 subnets = append(subnets, infrav1.SubnetSpec{ 230 CidrBlock: publicSubnetCIDRs[i].String(), 231 AvailabilityZone: zone, 232 IsPublic: true, 233 }) 234 subnets = append(subnets, infrav1.SubnetSpec{ 235 CidrBlock: privateSubnetCIDRs[i].String(), 236 AvailabilityZone: zone, 237 IsPublic: false, 238 }) 239 } 240 241 return subnets, nil 242 } 243 244 func (s *Service) deleteSubnets() error { 245 if s.scope.VPC().IsUnmanaged(s.scope.Name()) { 246 s.scope.V(4).Info("Skipping subnets deletion in unmanaged mode") 247 return nil 248 } 249 250 // Describe subnets in the vpc. 251 existing, err := s.describeSubnets() 252 if err != nil { 253 return err 254 } 255 256 for _, sn := range existing.Subnets { 257 if err := s.deleteSubnet(aws.StringValue(sn.SubnetId)); err != nil { 258 return err 259 } 260 } 261 262 return nil 263 } 264 265 func (s *Service) describeVpcSubnets() (infrav1.Subnets, error) { 266 sns, err := s.describeSubnets() 267 if err != nil { 268 return nil, err 269 } 270 271 routeTables, err := s.describeVpcRouteTablesBySubnet() 272 if err != nil { 273 return nil, err 274 } 275 276 natGateways, err := s.describeNatGatewaysBySubnet() 277 if err != nil { 278 return nil, err 279 } 280 281 subnets := make([]infrav1.SubnetSpec, 0, len(sns.Subnets)) 282 // Besides what the AWS API tells us directly about the subnets, we also want to discover whether the subnet is "public" (i.e. directly connected to the internet) and if there are any associated NAT gateways. 283 // We also look for a tag indicating that a particular subnet should be public, to try and determine whether a managed VPC's subnet should have such a route, but does not. 284 for _, ec2sn := range sns.Subnets { 285 spec := infrav1.SubnetSpec{ 286 ID: *ec2sn.SubnetId, 287 CidrBlock: *ec2sn.CidrBlock, 288 AvailabilityZone: *ec2sn.AvailabilityZone, 289 Tags: converters.TagsToMap(ec2sn.Tags), 290 } 291 292 // A subnet is public if it's tagged as such... 293 if spec.Tags.GetRole() == infrav1.PublicRoleTagValue { 294 spec.IsPublic = true 295 } 296 297 // ... or if it has an internet route 298 rt := routeTables[*ec2sn.SubnetId] 299 if rt == nil { 300 // If there is no explicit association, subnet defaults to main route table as implicit association 301 rt = routeTables[mainRouteTableInVPCKey] 302 } 303 if rt != nil { 304 spec.RouteTableID = rt.RouteTableId 305 for _, route := range rt.Routes { 306 if route.GatewayId != nil && strings.HasPrefix(*route.GatewayId, "igw") { 307 spec.IsPublic = true 308 } 309 } 310 } 311 312 ngw := natGateways[*ec2sn.SubnetId] 313 if ngw != nil { 314 spec.NatGatewayID = ngw.NatGatewayId 315 } 316 subnets = append(subnets, spec) 317 } 318 319 return subnets, nil 320 } 321 322 func (s *Service) describeSubnets() (*ec2.DescribeSubnetsOutput, error) { 323 input := &ec2.DescribeSubnetsInput{ 324 Filters: []*ec2.Filter{ 325 filter.EC2.SubnetStates(ec2.SubnetStatePending, ec2.SubnetStateAvailable), 326 }, 327 } 328 329 if s.scope.VPC().ID == "" { 330 input.Filters = append(input.Filters, filter.EC2.Cluster(s.scope.Name())) 331 } else { 332 input.Filters = append(input.Filters, filter.EC2.VPC(s.scope.VPC().ID)) 333 } 334 335 out, err := s.EC2Client.DescribeSubnets(input) 336 if err != nil { 337 record.Eventf(s.scope.InfraCluster(), "FailedDescribeSubnet", "Failed to describe subnets in vpc %q: %v", s.scope.VPC().ID, err) 338 return nil, errors.Wrapf(err, "failed to describe subnets in vpc %q", s.scope.VPC().ID) 339 } 340 return out, nil 341 } 342 343 func (s *Service) createSubnet(sn *infrav1.SubnetSpec) (*infrav1.SubnetSpec, error) { 344 out, err := s.EC2Client.CreateSubnet(&ec2.CreateSubnetInput{ 345 VpcId: aws.String(s.scope.VPC().ID), 346 CidrBlock: aws.String(sn.CidrBlock), 347 AvailabilityZone: aws.String(sn.AvailabilityZone), 348 TagSpecifications: []*ec2.TagSpecification{ 349 tags.BuildParamsToTagSpecification( 350 ec2.ResourceTypeSubnet, 351 s.getSubnetTagParams(false, services.TemporaryResourceID, sn.IsPublic, sn.AvailabilityZone, sn.Tags), 352 ), 353 }, 354 }) 355 if err != nil { 356 record.Warnf(s.scope.InfraCluster(), "FailedCreateSubnet", "Failed creating new managed Subnet %v", err) 357 return nil, errors.Wrap(err, "failed to create subnet") 358 } 359 360 record.Eventf(s.scope.InfraCluster(), "SuccessfulCreateSubnet", "Created new managed Subnet %q", *out.Subnet.SubnetId) 361 s.scope.Info("Created subnet", "id", *out.Subnet.SubnetId, "public", sn.IsPublic, "az", sn.AvailabilityZone, "cidr", sn.CidrBlock) 362 363 wReq := &ec2.DescribeSubnetsInput{SubnetIds: []*string{out.Subnet.SubnetId}} 364 if err := s.EC2Client.WaitUntilSubnetAvailable(wReq); err != nil { 365 return nil, errors.Wrapf(err, "failed to wait for subnet %q", *out.Subnet.SubnetId) 366 } 367 368 if sn.IsPublic { 369 attReq := &ec2.ModifySubnetAttributeInput{ 370 MapPublicIpOnLaunch: &ec2.AttributeBooleanValue{ 371 Value: aws.Bool(true), 372 }, 373 SubnetId: out.Subnet.SubnetId, 374 } 375 376 if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) { 377 if _, err := s.EC2Client.ModifySubnetAttribute(attReq); err != nil { 378 return false, err 379 } 380 return true, nil 381 }, awserrors.SubnetNotFound); err != nil { 382 record.Warnf(s.scope.InfraCluster(), "FailedModifySubnetAttributes", "Failed modifying managed Subnet %q attributes: %v", *out.Subnet.SubnetId, err) 383 return nil, errors.Wrapf(err, "failed to set subnet %q attributes", *out.Subnet.SubnetId) 384 } 385 record.Eventf(s.scope.InfraCluster(), "SuccessfulModifySubnetAttributes", "Modified managed Subnet %q attributes", *out.Subnet.SubnetId) 386 } 387 388 s.scope.V(2).Info("Created new subnet in VPC with cidr and availability zone ", 389 "subnet-id", *out.Subnet.SubnetId, 390 "vpc-id", *out.Subnet.VpcId, 391 "cidr-block", *out.Subnet.CidrBlock, 392 "availability-zone", *out.Subnet.AvailabilityZone) 393 394 return &infrav1.SubnetSpec{ 395 ID: *out.Subnet.SubnetId, 396 AvailabilityZone: *out.Subnet.AvailabilityZone, 397 CidrBlock: *out.Subnet.CidrBlock, 398 IsPublic: sn.IsPublic, 399 }, nil 400 } 401 402 func (s *Service) deleteSubnet(id string) error { 403 _, err := s.EC2Client.DeleteSubnet(&ec2.DeleteSubnetInput{ 404 SubnetId: aws.String(id), 405 }) 406 if err != nil { 407 record.Warnf(s.scope.InfraCluster(), "FailedDeleteSubnet", "Failed to delete managed Subnet %q: %v", id, err) 408 return errors.Wrapf(err, "failed to delete subnet %q", id) 409 } 410 411 s.scope.Info("Deleted subnet", "subnet-id", id, "vpc-id", s.scope.VPC().ID) 412 record.Eventf(s.scope.InfraCluster(), "SuccessfulDeleteSubnet", "Deleted managed Subnet %q", id) 413 return nil 414 } 415 416 func (s *Service) getSubnetTagParams(unmanagedVPC bool, id string, public bool, zone string, manualTags infrav1.Tags) infrav1.BuildParams { 417 var role string 418 additionalTags := s.scope.AdditionalTags() 419 420 if public { 421 role = infrav1.PublicRoleTagValue 422 additionalTags[externalLoadBalancerTag] = "1" 423 } else { 424 role = infrav1.PrivateRoleTagValue 425 additionalTags[internalLoadBalancerTag] = "1" 426 } 427 428 // Add tag needed for Service type=LoadBalancer 429 additionalTags[infrav1.NameKubernetesAWSCloudProviderPrefix+s.scope.KubernetesClusterName()] = string(infrav1.ResourceLifecycleShared) 430 431 for k, v := range manualTags { 432 additionalTags[k] = v 433 } 434 435 if !unmanagedVPC { 436 var name strings.Builder 437 name.WriteString(s.scope.Name()) 438 name.WriteString("-subnet-") 439 name.WriteString(role) 440 name.WriteString("-") 441 name.WriteString(zone) 442 443 return infrav1.BuildParams{ 444 ClusterName: s.scope.Name(), 445 ResourceID: id, 446 Lifecycle: infrav1.ResourceLifecycleOwned, 447 Name: aws.String(name.String()), 448 Role: aws.String(role), 449 Additional: additionalTags, 450 } 451 } else { 452 return infrav1.BuildParams{ 453 ResourceID: id, 454 Additional: additionalTags, 455 } 456 } 457 }