github.com/openshift/installer@v1.4.17/pkg/asset/manifests/aws/zones.go (about) 1 package aws 2 3 import ( 4 "context" 5 "fmt" 6 "net" 7 8 "k8s.io/apimachinery/pkg/util/sets" 9 capa "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" 10 11 "github.com/openshift/installer/pkg/asset/installconfig" 12 "github.com/openshift/installer/pkg/asset/installconfig/aws" 13 "github.com/openshift/installer/pkg/asset/manifests/capiutils" 14 utilscidr "github.com/openshift/installer/pkg/asset/manifests/capiutils/cidr" 15 "github.com/openshift/installer/pkg/types" 16 ) 17 18 // subnetsInput handles subnets information gathered from metadata. 19 type subnetsInput struct { 20 vpc string 21 privateSubnets aws.Subnets 22 publicSubnets aws.Subnets 23 edgeSubnets aws.Subnets 24 } 25 26 // zonesInput handles input parameters required to create managed and unmanaged 27 // Subnets to CAPI. 28 type zonesInput struct { 29 InstallConfig *installconfig.InstallConfig 30 Cluster *capa.AWSCluster 31 ClusterID *installconfig.ClusterID 32 ZonesInRegion []string 33 Subnets *subnetsInput 34 } 35 36 // GatherZonesFromMetadata retrieves zones from AWS API to be used 37 // when building the subnets to CAPA. 38 func (zin *zonesInput) GatherZonesFromMetadata(ctx context.Context) (err error) { 39 zin.ZonesInRegion, err = zin.InstallConfig.AWS.AvailabilityZones(ctx) 40 if err != nil { 41 return fmt.Errorf("failed to get availability zones: %w", err) 42 } 43 return nil 44 } 45 46 // GatherSubnetsFromMetadata retrieves subnets from AWS API to be used 47 // when building the subnets to CAPA. 48 func (zin *zonesInput) GatherSubnetsFromMetadata(ctx context.Context) (err error) { 49 zin.Subnets = &subnetsInput{} 50 if zin.Subnets.privateSubnets, err = zin.InstallConfig.AWS.PrivateSubnets(ctx); err != nil { 51 return fmt.Errorf("failed to get private subnets: %w", err) 52 } 53 if zin.Subnets.publicSubnets, err = zin.InstallConfig.AWS.PublicSubnets(ctx); err != nil { 54 return fmt.Errorf("failed to get public subnets: %w", err) 55 } 56 if zin.Subnets.edgeSubnets, err = zin.InstallConfig.AWS.EdgeSubnets(ctx); err != nil { 57 return fmt.Errorf("failed to get edge subnets: %w", err) 58 } 59 if zin.Subnets.vpc, err = zin.InstallConfig.AWS.VPC(ctx); err != nil { 60 return fmt.Errorf("failed to get VPC: %w", err) 61 } 62 return nil 63 } 64 65 // ZonesCAPI handles the discovered zones used to create subnets to CAPA. 66 // ZonesCAPI is scoped in this package, but exported to use complex scenarios 67 // with go-cmp on unit tests. 68 type ZonesCAPI struct { 69 ControlPlaneZones sets.Set[string] 70 ComputeZones sets.Set[string] 71 EdgeZones sets.Set[string] 72 } 73 74 // GetAvailabilityZones returns a sorted union of Availability Zones defined 75 // in the zone attribute in the pools for control plane and compute. 76 func (zo *ZonesCAPI) GetAvailabilityZones() []string { 77 return sets.List(zo.ControlPlaneZones.Union(zo.ComputeZones)) 78 } 79 80 // GetEdgeZones returns a sorted union of Local Zones or Wavelength Zones 81 // defined in the zone attribute in the edge compute pool. 82 func (zo *ZonesCAPI) GetEdgeZones() []string { 83 return sets.List(zo.EdgeZones) 84 } 85 86 // SetAvailabilityZones insert the zone to the given compute pool, and to 87 // the regular zone (zone type availability-zone) list. 88 func (zo *ZonesCAPI) SetAvailabilityZones(pool string, zones []string) { 89 switch pool { 90 case types.MachinePoolControlPlaneRoleName: 91 zo.ControlPlaneZones.Insert(zones...) 92 93 case types.MachinePoolComputeRoleName: 94 zo.ComputeZones.Insert(zones...) 95 } 96 } 97 98 // SetDefaultConfigZones evaluates if machine pools (control plane and workers) have been 99 // set the zones from install-config.yaml, if not sets the default from platform, when exists, 100 // otherwise set the default from the region discovered from AWS API. 101 func (zo *ZonesCAPI) SetDefaultConfigZones(pool string, defConfig []string, defRegion []string) { 102 zones := []string{} 103 switch pool { 104 case types.MachinePoolControlPlaneRoleName: 105 if len(zo.ControlPlaneZones) == 0 && len(defConfig) > 0 { 106 zones = defConfig 107 } else if len(zo.ControlPlaneZones) == 0 { 108 zones = defRegion 109 } 110 zo.ControlPlaneZones.Insert(zones...) 111 112 case types.MachinePoolComputeRoleName: 113 if len(zo.ComputeZones) == 0 && len(defConfig) > 0 { 114 zones = defConfig 115 } else if len(zo.ComputeZones) == 0 { 116 zones = defRegion 117 } 118 zo.ComputeZones.Insert(zones...) 119 } 120 } 121 122 // setSubnets is the entrypoint to create the CAPI NetworkSpec structures 123 // for managed or BYO VPC deployments from install-config.yaml. 124 // The NetworkSpec.Subnets will be populated with the desired zones. 125 func setSubnets(ctx context.Context, in *zonesInput) error { 126 if in.InstallConfig == nil { 127 return fmt.Errorf("failed to get installConfig") 128 } 129 if in.InstallConfig.AWS == nil { 130 return fmt.Errorf("failed to get AWS metadata") 131 } 132 if in.InstallConfig.Config == nil { 133 return fmt.Errorf("unable to get Config") 134 } 135 if in.Cluster == nil { 136 return fmt.Errorf("failed to get AWSCluster config") 137 } 138 139 // BYO VPC ("unmanaged") deployments 140 if len(in.InstallConfig.Config.AWS.Subnets) > 0 { 141 if err := in.GatherSubnetsFromMetadata(ctx); err != nil { 142 return fmt.Errorf("failed to get subnets from metadata: %w", err) 143 } 144 return setSubnetsBYOVPC(in) 145 } 146 147 // Managed VPC (fully automated) deployments 148 if err := in.GatherZonesFromMetadata(ctx); err != nil { 149 return fmt.Errorf("failed to get availability zones from metadata: %w", err) 150 } 151 return setSubnetsManagedVPC(in) 152 } 153 154 // setSubnetsBYOVPC creates the CAPI NetworkSpec.Subnets setting the 155 // desired subnets from install-config.yaml in the BYO VPC deployment. 156 // This function does not provide support for unit test to mock for AWS API, 157 // so all API calls must be done prior this execution. 158 // TODO: create support to mock AWS API calls in the unit tests, then the method 159 // GatherSubnetsFromMetadata() can be added in setSubnetsBYOVPC. 160 func setSubnetsBYOVPC(in *zonesInput) error { 161 in.Cluster.Spec.NetworkSpec.VPC = capa.VPCSpec{ 162 ID: in.Subnets.vpc, 163 } 164 for _, subnet := range in.Subnets.privateSubnets { 165 in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{ 166 ID: subnet.ID, 167 CidrBlock: subnet.CIDR, 168 AvailabilityZone: subnet.Zone.Name, 169 IsPublic: subnet.Public, 170 }) 171 } 172 173 for _, subnet := range in.Subnets.publicSubnets { 174 in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{ 175 ID: subnet.ID, 176 CidrBlock: subnet.CIDR, 177 AvailabilityZone: subnet.Zone.Name, 178 IsPublic: subnet.Public, 179 }) 180 } 181 182 // edgeSubnets are subnet created on AWS Local Zones or Wavelength Zone, 183 // discovered by ID and zone-type attribute. 184 for _, subnet := range in.Subnets.edgeSubnets { 185 in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{ 186 ID: subnet.ID, 187 CidrBlock: subnet.CIDR, 188 AvailabilityZone: subnet.Zone.Name, 189 IsPublic: subnet.Public, 190 }) 191 } 192 193 return nil 194 } 195 196 // setSubnetsManagedVPC creates the CAPI NetworkSpec.VPC and the NetworkSpec.Subnets, 197 // setting the desired zones from install-config.yaml in the managed 198 // VPC deployment, when specified, otherwise default zones are set from 199 // the AWS API, previously discovered. 200 // The CIDR blocks are calculated leaving free blocks to allow future expansions, 201 // in Day-2, when desired. 202 // This function does not have mock for AWS API, so all API calls must be added prior 203 // this execution. 204 // TODO: create support to mock AWS API calls in the unit tests, then the method 205 // GatherZonesFromMetadata() can be added in setSubnetsManagedVPC. 206 func setSubnetsManagedVPC(in *zonesInput) error { 207 out, err := extractZonesFromInstallConfig(in) 208 if err != nil { 209 return fmt.Errorf("failed to get availability zones: %w", err) 210 } 211 212 isPublishingExternal := in.InstallConfig.Config.Publish == types.ExternalPublishingStrategy 213 allAvailabilityZones := out.GetAvailabilityZones() 214 allEdgeZones := out.GetEdgeZones() 215 216 mainCIDR := capiutils.CIDRFromInstallConfig(in.InstallConfig) 217 in.Cluster.Spec.NetworkSpec.VPC = capa.VPCSpec{ 218 CidrBlock: mainCIDR.String(), 219 } 220 221 // Base subnets count considering only private zones, leaving one free block to allow 222 // future subnet expansions in Day-2. 223 numSubnets := len(allAvailabilityZones) + 1 224 225 // Public subnets consumes one range from private CIDR block. 226 if isPublishingExternal { 227 numSubnets++ 228 } 229 230 // Edge subnets consumes one CIDR block from private CIDR, slicing it 231 // into smaller depending on the amount edge zones added to install config. 232 if len(allEdgeZones) > 0 { 233 numSubnets++ 234 } 235 236 privateCIDRs, err := utilscidr.SplitIntoSubnetsIPv4(mainCIDR.String(), numSubnets) 237 if err != nil { 238 return fmt.Errorf("unable to generate CIDR blocks for all private subnets: %w", err) 239 } 240 241 publicCIDR := privateCIDRs[len(allAvailabilityZones)].String() 242 243 var edgeCIDR string 244 if len(allEdgeZones) > 0 { 245 edgeCIDR = privateCIDRs[len(allAvailabilityZones)+1].String() 246 } 247 248 var publicCIDRs []*net.IPNet 249 if isPublishingExternal { 250 // The last num(zones) blocks are dedicated to the public subnets. 251 publicCIDRs, err = utilscidr.SplitIntoSubnetsIPv4(publicCIDR, len(allAvailabilityZones)) 252 if err != nil { 253 return fmt.Errorf("unable to generate CIDR blocks for all public subnets: %w", err) 254 } 255 } 256 257 // Create subnets from zone pools (control plane and compute) with type availability-zone. 258 if len(privateCIDRs) < len(allAvailabilityZones) { 259 return fmt.Errorf("unable to define CIDR blocks to all zones for private subnets") 260 } 261 if isPublishingExternal && len(publicCIDRs) < len(allAvailabilityZones) { 262 return fmt.Errorf("unable to define CIDR blocks to all zones for public subnets") 263 } 264 265 for idxCIDR, zone := range allAvailabilityZones { 266 in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{ 267 AvailabilityZone: zone, 268 CidrBlock: privateCIDRs[idxCIDR].String(), 269 ID: fmt.Sprintf("%s-subnet-private-%s", in.ClusterID.InfraID, zone), 270 IsPublic: false, 271 }) 272 if isPublishingExternal { 273 in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{ 274 AvailabilityZone: zone, 275 CidrBlock: publicCIDRs[idxCIDR].String(), 276 ID: fmt.Sprintf("%s-subnet-public-%s", in.ClusterID.InfraID, zone), 277 IsPublic: true, 278 }) 279 } 280 } 281 282 // no edge zones, nothing else to do 283 if len(allEdgeZones) == 0 { 284 return nil 285 } 286 287 // Create subnets from edge zone pool with type local-zone. 288 289 // Slice the main CIDR (edgeCIDR) into N*zones for privates subnets, 290 // and, when publish external, duplicate to create public subnets. 291 numEdgeSubnets := len(allEdgeZones) 292 if isPublishingExternal { 293 numEdgeSubnets *= 2 294 } 295 296 // Allow one CIDR block for future expansion. 297 numEdgeSubnets++ 298 299 // Slice the edgeCIDR into the amount of desired subnets. 300 edgeCIDRs, err := utilscidr.SplitIntoSubnetsIPv4(edgeCIDR, numEdgeSubnets) 301 if err != nil { 302 return fmt.Errorf("unable to generate CIDR blocks for all edge subnets: %w", err) 303 } 304 if len(edgeCIDRs) < len(allEdgeZones) { 305 return fmt.Errorf("unable to define CIDR blocks to all edge zones for private subnets") 306 } 307 if isPublishingExternal && (len(edgeCIDRs) < (len(allEdgeZones) * 2)) { 308 return fmt.Errorf("unable to define CIDR blocks to all edge zones for public subnets") 309 } 310 311 // Create subnets from zone pool with type local-zone or wavelength-zone (edge zones) 312 for idxCIDR, zone := range allEdgeZones { 313 in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{ 314 AvailabilityZone: zone, 315 CidrBlock: edgeCIDRs[idxCIDR].String(), 316 ID: fmt.Sprintf("%s-subnet-private-%s", in.ClusterID.InfraID, zone), 317 IsPublic: false, 318 }) 319 if isPublishingExternal { 320 in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{ 321 AvailabilityZone: zone, 322 CidrBlock: edgeCIDRs[len(allEdgeZones)+idxCIDR].String(), 323 ID: fmt.Sprintf("%s-subnet-public-%s", in.ClusterID.InfraID, zone), 324 IsPublic: true, 325 }) 326 } 327 } 328 329 return nil 330 } 331 332 // extractZonesFromInstallConfig extracts zones defined in the install-config. 333 func extractZonesFromInstallConfig(in *zonesInput) (*ZonesCAPI, error) { 334 out := ZonesCAPI{ 335 ControlPlaneZones: sets.New[string](), 336 ComputeZones: sets.New[string](), 337 EdgeZones: sets.New[string](), 338 } 339 340 cfg := in.InstallConfig.Config 341 defaultZones := []string{} 342 if cfg.AWS != nil && cfg.AWS.DefaultMachinePlatform != nil && len(cfg.AWS.DefaultMachinePlatform.Zones) > 0 { 343 defaultZones = cfg.AWS.DefaultMachinePlatform.Zones 344 } 345 346 if cfg.ControlPlane != nil && cfg.ControlPlane.Platform.AWS != nil { 347 out.SetAvailabilityZones(types.MachinePoolControlPlaneRoleName, cfg.ControlPlane.Platform.AWS.Zones) 348 } 349 out.SetDefaultConfigZones(types.MachinePoolControlPlaneRoleName, defaultZones, in.ZonesInRegion) 350 351 // set the zones in the compute/worker pool, when defined, otherwise use defaults. 352 for _, pool := range cfg.Compute { 353 if pool.Platform.AWS == nil { 354 continue 355 } 356 // edge compute pools should have zones defined. 357 if pool.Name == types.MachinePoolEdgeRoleName { 358 if len(pool.Platform.AWS.Zones) == 0 { 359 return nil, fmt.Errorf("expect one or more zones in the edge compute pool, got: %q", pool.Platform.AWS.Zones) 360 } 361 out.EdgeZones.Insert(pool.Platform.AWS.Zones...) 362 continue 363 } 364 365 if len(pool.Platform.AWS.Zones) > 0 { 366 out.SetAvailabilityZones(pool.Name, pool.Platform.AWS.Zones) 367 } 368 out.SetDefaultConfigZones(types.MachinePoolComputeRoleName, defaultZones, in.ZonesInRegion) 369 } 370 371 // set defaults for worker pool when not defined in config. 372 if len(out.ComputeZones) == 0 { 373 out.SetDefaultConfigZones(types.MachinePoolComputeRoleName, defaultZones, in.ZonesInRegion) 374 } 375 376 // should raise an error if no zones is available in the pools, default platform config, or metadata. 377 if azs := out.GetAvailabilityZones(); len(azs) == 0 { 378 return nil, fmt.Errorf("failed to set zones from config, got: %q", azs) 379 } 380 381 return &out, nil 382 }