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  }