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 }