Skip to content

Commit 7cebd6a

Browse files
committed
CORS-2899: AWS/edge-zones/CAPA - Local Zones provisioner
The subnet structure created in managed VPC by CAPA is created with SubnetSpec, without providing the valid ID, so CAPA will understand that the subnet does not exists and will created it following the zone specified/discovered in the install config.
1 parent e45745b commit 7cebd6a

File tree

2 files changed

+848
-327
lines changed

2 files changed

+848
-327
lines changed

pkg/asset/manifests/aws/zones.go

Lines changed: 156 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ import (
1515
"github.com/openshift/installer/pkg/types"
1616
)
1717

18+
// subnetsInput handles subnets information gathered from metadata.
1819
type subnetsInput struct {
1920
vpc string
2021
privateSubnets aws.Subnets
2122
publicSubnets aws.Subnets
23+
edgeSubnets aws.Subnets
2224
}
2325

26+
// zonesInput handles input parameters required to create managed and unmanaged
27+
// Subnets to CAPI.
2428
type zonesInput struct {
2529
InstallConfig *installconfig.InstallConfig
2630
Cluster *capa.AWSCluster
@@ -49,56 +53,69 @@ func (zin *zonesInput) GatherSubnetsFromMetadata(ctx context.Context) (err error
4953
if zin.Subnets.publicSubnets, err = zin.InstallConfig.AWS.PublicSubnets(ctx); err != nil {
5054
return fmt.Errorf("failed to get public subnets: %w", err)
5155
}
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+
}
5259
if zin.Subnets.vpc, err = zin.InstallConfig.AWS.VPC(ctx); err != nil {
5360
return fmt.Errorf("failed to get VPC: %w", err)
5461
}
5562
return nil
5663
}
5764

58-
type zonesCAPI struct {
59-
controlPlaneZones sets.Set[string]
60-
computeZones sets.Set[string]
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))
6178
}
6279

63-
// AvailabilityZones returns a sorted union of Availability Zones defined
64-
// in the zone attribute in the pools for control plane and compute zones.
65-
func (zo *zonesCAPI) AvailabilityZones() []string {
66-
return sets.List(zo.controlPlaneZones.Union(zo.computeZones))
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)
6784
}
6885

6986
// SetAvailabilityZones insert the zone to the given compute pool, and to
7087
// the regular zone (zone type availability-zone) list.
71-
func (zo *zonesCAPI) SetAvailabilityZones(pool string, zones []string) {
88+
func (zo *ZonesCAPI) SetAvailabilityZones(pool string, zones []string) {
7289
switch pool {
7390
case types.MachinePoolControlPlaneRoleName:
74-
zo.controlPlaneZones.Insert(zones...)
91+
zo.ControlPlaneZones.Insert(zones...)
7592

7693
case types.MachinePoolComputeRoleName:
77-
zo.computeZones.Insert(zones...)
94+
zo.ComputeZones.Insert(zones...)
7895
}
7996
}
8097

8198
// SetDefaultConfigZones evaluates if machine pools (control plane and workers) have been
8299
// set the zones from install-config.yaml, if not sets the default from platform, when exists,
83100
// otherwise set the default from the region discovered from AWS API.
84-
func (zo *zonesCAPI) SetDefaultConfigZones(pool string, defConfig []string, defRegion []string) {
101+
func (zo *ZonesCAPI) SetDefaultConfigZones(pool string, defConfig []string, defRegion []string) {
85102
zones := []string{}
86103
switch pool {
87104
case types.MachinePoolControlPlaneRoleName:
88-
if len(zo.controlPlaneZones) == 0 && len(defConfig) > 0 {
105+
if len(zo.ControlPlaneZones) == 0 && len(defConfig) > 0 {
89106
zones = defConfig
90-
} else if len(zo.controlPlaneZones) == 0 {
107+
} else if len(zo.ControlPlaneZones) == 0 {
91108
zones = defRegion
92109
}
93-
zo.controlPlaneZones.Insert(zones...)
110+
zo.ControlPlaneZones.Insert(zones...)
94111

95112
case types.MachinePoolComputeRoleName:
96-
if len(zo.computeZones) == 0 && len(defConfig) > 0 {
113+
if len(zo.ComputeZones) == 0 && len(defConfig) > 0 {
97114
zones = defConfig
98-
} else if len(zo.computeZones) == 0 {
115+
} else if len(zo.ComputeZones) == 0 {
99116
zones = defRegion
100117
}
101-
zo.computeZones.Insert(zones...)
118+
zo.ComputeZones.Insert(zones...)
102119
}
103120
}
104121

@@ -118,13 +135,16 @@ func setSubnets(ctx context.Context, in *zonesInput) error {
118135
if in.Cluster == nil {
119136
return fmt.Errorf("failed to get AWSCluster config")
120137
}
138+
139+
// BYO VPC ("unmanaged") deployments
121140
if len(in.InstallConfig.Config.AWS.Subnets) > 0 {
122141
if err := in.GatherSubnetsFromMetadata(ctx); err != nil {
123142
return fmt.Errorf("failed to get subnets from metadata: %w", err)
124143
}
125144
return setSubnetsBYOVPC(in)
126145
}
127146

147+
// Managed VPC (fully automated) deployments
128148
if err := in.GatherZonesFromMetadata(ctx); err != nil {
129149
return fmt.Errorf("failed to get availability zones from metadata: %w", err)
130150
}
@@ -133,10 +153,10 @@ func setSubnets(ctx context.Context, in *zonesInput) error {
133153

134154
// setSubnetsBYOVPC creates the CAPI NetworkSpec.Subnets setting the
135155
// desired subnets from install-config.yaml in the BYO VPC deployment.
136-
// This function does not have support for unit test to mock for AWS API,
156+
// This function does not provide support for unit test to mock for AWS API,
137157
// so all API calls must be done prior this execution.
138-
// TODO: create support to mock AWS API calls in the unit tests, so we can merge
139-
// the methods GatherSubnetsFromMetadata() into this.
158+
// TODO: create support to mock AWS API calls in the unit tests, then the method
159+
// GatherSubnetsFromMetadata() can be added in setSubnetsBYOVPC.
140160
func setSubnetsBYOVPC(in *zonesInput) error {
141161
in.Cluster.Spec.NetworkSpec.VPC = capa.VPCSpec{
142162
ID: in.Subnets.vpc,
@@ -159,64 +179,90 @@ func setSubnetsBYOVPC(in *zonesInput) error {
159179
})
160180
}
161181

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+
162193
return nil
163194
}
164195

165196
// setSubnetsManagedVPC creates the CAPI NetworkSpec.VPC and the NetworkSpec.Subnets,
166197
// setting the desired zones from install-config.yaml in the managed
167198
// VPC deployment, when specified, otherwise default zones are set from
168-
// the previously discovered from AWS API.
169-
// This function does not have mock for AWS API, so all API calls must be done prior
170-
// this execution.
171-
// TODO: create support to mock AWS API calls in the unit tests, so we can merge
172-
// the methods GatherZonesFromMetadata() into this.
199+
// the AWS API, previously discovered.
173200
// The CIDR blocks are calculated leaving free blocks to allow future expansions,
174201
// 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.
175206
func setSubnetsManagedVPC(in *zonesInput) error {
176207
out, err := extractZonesFromInstallConfig(in)
177208
if err != nil {
178209
return fmt.Errorf("failed to get availability zones: %w", err)
179210
}
180211

181-
allZones := out.AvailabilityZones()
182212
isPublishingExternal := in.InstallConfig.Config.Publish == types.ExternalPublishingStrategy
213+
allAvailabilityZones := out.GetAvailabilityZones()
214+
allEdgeZones := out.GetEdgeZones()
215+
183216
mainCIDR := capiutils.CIDRFromInstallConfig(in.InstallConfig)
184217
in.Cluster.Spec.NetworkSpec.VPC = capa.VPCSpec{
185218
CidrBlock: mainCIDR.String(),
186219
}
187220

188-
// Base subnets considering only private zones, leaving one free block to allow
221+
// Base subnets count considering only private zones, leaving one free block to allow
189222
// future subnet expansions in Day-2.
190-
numSubnets := len(allZones) + 1
223+
numSubnets := len(allAvailabilityZones) + 1
191224

192-
// Public subnets consumes one range from base blocks.
225+
// Public subnets consumes one range from private CIDR block.
193226
if isPublishingExternal {
194227
numSubnets++
195228
}
196229

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+
197236
privateCIDRs, err := utilscidr.SplitIntoSubnetsIPv4(mainCIDR.String(), numSubnets)
198237
if err != nil {
199-
return fmt.Errorf("unable to retrieve CIDR blocks for all private subnets: %w", err)
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()
200246
}
201247

202248
var publicCIDRs []*net.IPNet
203249
if isPublishingExternal {
204250
// The last num(zones) blocks are dedicated to the public subnets.
205-
publicCIDRs, err = utilscidr.SplitIntoSubnetsIPv4(privateCIDRs[len(allZones)].String(), len(allZones))
251+
publicCIDRs, err = utilscidr.SplitIntoSubnetsIPv4(publicCIDR, len(allAvailabilityZones))
206252
if err != nil {
207-
return fmt.Errorf("unable to retrieve CIDR blocks for all public subnets: %w", err)
253+
return fmt.Errorf("unable to generate CIDR blocks for all public subnets: %w", err)
208254
}
209255
}
210256

211-
// Create subnets from zone pool with type availability-zone
212-
if len(privateCIDRs) < len(allZones) {
257+
// Create subnets from zone pools (control plane and compute) with type availability-zone.
258+
if len(privateCIDRs) < len(allAvailabilityZones) {
213259
return fmt.Errorf("unable to define CIDR blocks to all zones for private subnets")
214260
}
215-
if isPublishingExternal && len(publicCIDRs) < len(allZones) {
261+
if isPublishingExternal && len(publicCIDRs) < len(allAvailabilityZones) {
216262
return fmt.Errorf("unable to define CIDR blocks to all zones for public subnets")
217263
}
218264

219-
for idxCIDR, zone := range allZones {
265+
for idxCIDR, zone := range allAvailabilityZones {
220266
in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
221267
AvailabilityZone: zone,
222268
CidrBlock: privateCIDRs[idxCIDR].String(),
@@ -232,14 +278,63 @@ func setSubnetsManagedVPC(in *zonesInput) error {
232278
})
233279
}
234280
}
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+
235329
return nil
236330
}
237331

238332
// extractZonesFromInstallConfig extracts zones defined in the install-config.
239-
func extractZonesFromInstallConfig(in *zonesInput) (*zonesCAPI, error) {
240-
out := zonesCAPI{
241-
controlPlaneZones: sets.New[string](),
242-
computeZones: sets.New[string](),
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](),
243338
}
244339

245340
cfg := in.InstallConfig.Config
@@ -253,19 +348,35 @@ func extractZonesFromInstallConfig(in *zonesInput) (*zonesCAPI, error) {
253348
}
254349
out.SetDefaultConfigZones(types.MachinePoolControlPlaneRoleName, defaultZones, in.ZonesInRegion)
255350

351+
// set the zones in the compute/worker pool, when defined, otherwise use defaults.
256352
for _, pool := range cfg.Compute {
257353
if pool.Platform.AWS == nil {
258354
continue
259355
}
260-
if len(pool.Platform.AWS.Zones) > 0 {
261-
out.SetAvailabilityZones(pool.Name, pool.Platform.AWS.Zones)
262-
}
263-
// Ignoring as edge pool is not yet supported by CAPA.
264-
// See https://github.com/openshift/installer/pull/8173
356+
// edge compute pools should have zones defined.
265357
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...)
266362
continue
267363
}
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 {
268373
out.SetDefaultConfigZones(types.MachinePoolComputeRoleName, defaultZones, in.ZonesInRegion)
269374
}
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+
270381
return &out, nil
271382
}

0 commit comments

Comments
 (0)