github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/aws/subnet.go (about)

     1  package aws
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  
     9  	"github.com/aws/aws-sdk-go/aws"
    10  	"github.com/aws/aws-sdk-go/aws/session"
    11  	"github.com/aws/aws-sdk-go/service/ec2"
    12  	"github.com/sirupsen/logrus"
    13  
    14  	typesaws "github.com/openshift/installer/pkg/types/aws"
    15  )
    16  
    17  // Subnet holds metadata for a subnet.
    18  type Subnet struct {
    19  	// ID is the subnet's Identifier.
    20  	ID string
    21  
    22  	// ARN is the subnet's Amazon Resource Name.
    23  	ARN string
    24  
    25  	// Zone is the subnet's availability zone.
    26  	Zone *Zone
    27  
    28  	// CIDR is the subnet's CIDR block.
    29  	CIDR string
    30  
    31  	// Public is the flag to define the subnet public.
    32  	Public bool
    33  }
    34  
    35  // Subnets is the map for the Subnet metadata indexed by zone.
    36  type Subnets map[string]Subnet
    37  
    38  // SubnetGroups is the group of subnets used by installer.
    39  type SubnetGroups struct {
    40  	Public  Subnets
    41  	Private Subnets
    42  	Edge    Subnets
    43  	VPC     string
    44  }
    45  
    46  // subnets retrieves metadata for the given subnet(s).
    47  func subnets(ctx context.Context, session *session.Session, region string, ids []string) (subnetGroups SubnetGroups, err error) {
    48  	metas := make(Subnets, len(ids))
    49  	zoneNames := make([]*string, len(ids))
    50  	availabilityZones := make(map[string]*ec2.AvailabilityZone, len(ids))
    51  	subnetGroups = SubnetGroups{
    52  		Public:  make(Subnets, len(ids)),
    53  		Private: make(Subnets, len(ids)),
    54  		Edge:    make(Subnets, len(ids)),
    55  	}
    56  
    57  	var vpcFromSubnet string
    58  	client := ec2.New(session, aws.NewConfig().WithRegion(region))
    59  
    60  	idPointers := make([]*string, len(ids))
    61  	for _, id := range ids {
    62  		idPointers = append(idPointers, aws.String(id))
    63  	}
    64  
    65  	var lastError error
    66  	err = client.DescribeSubnetsPagesWithContext(
    67  		ctx,
    68  		&ec2.DescribeSubnetsInput{SubnetIds: idPointers},
    69  		func(results *ec2.DescribeSubnetsOutput, lastPage bool) bool {
    70  			for _, subnet := range results.Subnets {
    71  				if subnet.SubnetId == nil {
    72  					continue
    73  				}
    74  				if subnet.SubnetArn == nil {
    75  					lastError = fmt.Errorf("%s has no ARN", *subnet.SubnetId)
    76  					return false
    77  				}
    78  				if subnet.VpcId == nil {
    79  					lastError = fmt.Errorf("%s has no VPC", *subnet.SubnetId)
    80  					return false
    81  				}
    82  				if subnet.AvailabilityZone == nil {
    83  					lastError = fmt.Errorf("%s has not availability zone", *subnet.SubnetId)
    84  					return false
    85  				}
    86  
    87  				if subnetGroups.VPC == "" {
    88  					subnetGroups.VPC = *subnet.VpcId
    89  					vpcFromSubnet = *subnet.SubnetId
    90  				} else if *subnet.VpcId != subnetGroups.VPC {
    91  					lastError = fmt.Errorf("all subnets must belong to the same VPC: %s is from %s, but %s is from %s", *subnet.SubnetId, *subnet.VpcId, vpcFromSubnet, subnetGroups.VPC)
    92  					return false
    93  				}
    94  				metas[aws.StringValue(subnet.SubnetId)] = Subnet{
    95  					ID:     aws.StringValue(subnet.SubnetId),
    96  					ARN:    aws.StringValue(subnet.SubnetArn),
    97  					Zone:   &Zone{Name: aws.StringValue(subnet.AvailabilityZone)},
    98  					CIDR:   aws.StringValue(subnet.CidrBlock),
    99  					Public: false,
   100  				}
   101  				zoneNames = append(zoneNames, subnet.AvailabilityZone)
   102  			}
   103  			return !lastPage
   104  		},
   105  	)
   106  	if err == nil {
   107  		err = lastError
   108  	}
   109  	if err != nil {
   110  		return subnetGroups, fmt.Errorf("describing subnets: %w", err)
   111  	}
   112  
   113  	var routeTables []*ec2.RouteTable
   114  	err = client.DescribeRouteTablesPagesWithContext(
   115  		ctx,
   116  		&ec2.DescribeRouteTablesInput{
   117  			Filters: []*ec2.Filter{{
   118  				Name:   aws.String("vpc-id"),
   119  				Values: []*string{aws.String(subnetGroups.VPC)},
   120  			}},
   121  		},
   122  		func(results *ec2.DescribeRouteTablesOutput, lastPage bool) bool {
   123  			routeTables = append(routeTables, results.RouteTables...)
   124  			return !lastPage
   125  		},
   126  	)
   127  	if err != nil {
   128  		return subnetGroups, fmt.Errorf("describing route tables: %w", err)
   129  	}
   130  
   131  	azs, err := client.DescribeAvailabilityZonesWithContext(ctx, &ec2.DescribeAvailabilityZonesInput{ZoneNames: zoneNames})
   132  	if err != nil {
   133  		return subnetGroups, fmt.Errorf("describing availability zones: %w", err)
   134  	}
   135  	for _, az := range azs.AvailabilityZones {
   136  		availabilityZones[*az.ZoneName] = az
   137  	}
   138  
   139  	publicOnlySubnets := os.Getenv("OPENSHIFT_INSTALL_AWS_PUBLIC_ONLY") != ""
   140  
   141  	for _, id := range ids {
   142  		meta, ok := metas[id]
   143  		if !ok {
   144  			return subnetGroups, fmt.Errorf("failed to find %s", id)
   145  		}
   146  
   147  		isPublic, err := isSubnetPublic(routeTables, id)
   148  		if err != nil {
   149  			return subnetGroups, err
   150  		}
   151  		meta.Public = isPublic
   152  
   153  		zoneName := meta.Zone.Name
   154  		if _, ok := availabilityZones[zoneName]; !ok {
   155  			return subnetGroups, fmt.Errorf("unable to read properties of zone name %s from the list %v: %w", zoneName, zoneNames, err)
   156  		}
   157  		zone := availabilityZones[zoneName]
   158  		meta.Zone.Type = aws.StringValue(zone.ZoneType)
   159  		meta.Zone.GroupName = aws.StringValue(zone.GroupName)
   160  		if availabilityZones[zoneName].ParentZoneName != nil {
   161  			meta.Zone.ParentZoneName = aws.StringValue(zone.ParentZoneName)
   162  		}
   163  
   164  		// AWS Local Zones are grouped as Edge subnets
   165  		if meta.Zone.Type == typesaws.LocalZoneType ||
   166  			meta.Zone.Type == typesaws.WavelengthZoneType {
   167  			subnetGroups.Edge[id] = meta
   168  			continue
   169  		}
   170  		if meta.Public {
   171  			subnetGroups.Public[id] = meta
   172  
   173  			// Let public subnets work as if they were private. This allows us to
   174  			// have clusters with public-only subnets without having to introduce a
   175  			// lot of changes in the installer. Such clusters can be used in a
   176  			// NAT-less GW scenario, therefore decreasing costs in cases where node
   177  			// security is not a concern (e.g, ephemeral clusters in CI)
   178  			if publicOnlySubnets {
   179  				subnetGroups.Private[id] = meta
   180  			}
   181  			continue
   182  		}
   183  		// Subnet is grouped by default as private
   184  		subnetGroups.Private[id] = meta
   185  	}
   186  	return subnetGroups, nil
   187  }
   188  
   189  // https://github.com/kubernetes/kubernetes/blob/9f036cd43d35a9c41d7ac4ca82398a6d0bef957b/staging/src/k8s.io/legacy-cloud-providers/aws/aws.go#L3376-L3419
   190  func isSubnetPublic(rt []*ec2.RouteTable, subnetID string) (bool, error) {
   191  	var subnetTable *ec2.RouteTable
   192  	for _, table := range rt {
   193  		for _, assoc := range table.Associations {
   194  			if aws.StringValue(assoc.SubnetId) == subnetID {
   195  				subnetTable = table
   196  				break
   197  			}
   198  		}
   199  	}
   200  
   201  	if subnetTable == nil {
   202  		// If there is no explicit association, the subnet will be implicitly
   203  		// associated with the VPC's main routing table.
   204  		for _, table := range rt {
   205  			for _, assoc := range table.Associations {
   206  				if aws.BoolValue(assoc.Main) {
   207  					logrus.Debugf("Assuming implicit use of main routing table %s for %s",
   208  						aws.StringValue(table.RouteTableId), subnetID)
   209  					subnetTable = table
   210  					break
   211  				}
   212  			}
   213  		}
   214  	}
   215  
   216  	if subnetTable == nil {
   217  		return false, fmt.Errorf("could not locate routing table for %s", subnetID)
   218  	}
   219  
   220  	for _, route := range subnetTable.Routes {
   221  		// There is no direct way in the AWS API to determine if a subnet is public or private.
   222  		// A public subnet is one which has an internet gateway route
   223  		// we look for the gatewayId and make sure it has the prefix of igw to differentiate
   224  		// from the default in-subnet route which is called "local"
   225  		// or other virtual gateway (starting with vgv)
   226  		// or vpc peering connections (starting with pcx).
   227  		if strings.HasPrefix(aws.StringValue(route.GatewayId), "igw") {
   228  			return true, nil
   229  		}
   230  		if strings.HasPrefix(aws.StringValue(route.CarrierGatewayId), "cagw") {
   231  			return true, nil
   232  		}
   233  	}
   234  
   235  	return false, nil
   236  }