Skip to content

Commit 59cc24d

Browse files
Merge pull request openshift#8173 from mtulio/CORS-2899-edge-zones
CORS-2899: AWS/CAPI Local Zones network provisioning
2 parents 37d5379 + 7cebd6a commit 59cc24d

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)