Skip to content

Commit 9e2595d

Browse files
Merge pull request openshift#8156 from mtulio/CORS-2895-cluster-zones
CORS-2895: aws/capi: setting zones to when creating cluster
2 parents bbc22b2 + a9d50f4 commit 9e2595d

File tree

3 files changed

+1119
-52
lines changed

3 files changed

+1119
-52
lines changed

pkg/asset/manifests/aws/cluster.go

Lines changed: 14 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"time"
77

8-
"github.com/pkg/errors"
98
corev1 "k8s.io/api/core/v1"
109
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1110
"k8s.io/utils/ptr"
@@ -19,16 +18,10 @@ import (
1918
)
2019

2120
// GenerateClusterAssets generates the manifests for the cluster-api.
22-
func GenerateClusterAssets(installConfig *installconfig.InstallConfig, clusterID *installconfig.ClusterID) (*capiutils.GenerateClusterAssetsOutput, error) {
21+
func GenerateClusterAssets(ic *installconfig.InstallConfig, clusterID *installconfig.ClusterID) (*capiutils.GenerateClusterAssetsOutput, error) {
2322
manifests := []*asset.RuntimeFile{}
24-
mainCIDR := capiutils.CIDRFromInstallConfig(installConfig)
2523

26-
zones, err := installConfig.AWS.AvailabilityZones(context.TODO())
27-
if err != nil {
28-
return nil, errors.Wrap(err, "failed to get availability zones")
29-
}
30-
31-
tags, err := aws.CapaTagsFromUserTags(clusterID.InfraID, installConfig.Config.AWS.UserTags)
24+
tags, err := aws.CapaTagsFromUserTags(clusterID.InfraID, ic.Config.AWS.UserTags)
3225
if err != nil {
3326
return nil, fmt.Errorf("failed to get user tags: %w", err)
3427
}
@@ -39,13 +32,8 @@ func GenerateClusterAssets(installConfig *installconfig.InstallConfig, clusterID
3932
Namespace: capiutils.Namespace,
4033
},
4134
Spec: capa.AWSClusterSpec{
42-
Region: installConfig.Config.AWS.Region,
35+
Region: ic.Config.AWS.Region,
4336
NetworkSpec: capa.NetworkSpec{
44-
VPC: capa.VPCSpec{
45-
CidrBlock: mainCIDR.String(),
46-
AvailabilityZoneUsageLimit: ptr.To(len(zones)),
47-
AvailabilityZoneSelection: &capa.AZSelectionSchemeOrdered,
48-
},
4937
CNI: &capa.CNISpec{
5038
CNIIngressRules: capa.CNIIngressRules{
5139
{
@@ -174,7 +162,7 @@ func GenerateClusterAssets(installConfig *installconfig.InstallConfig, clusterID
174162
Protocol: capa.SecurityGroupProtocolTCP,
175163
FromPort: 22623,
176164
ToPort: 22623,
177-
CidrBlocks: []string{mainCIDR.String()},
165+
CidrBlocks: []string{capiutils.CIDRFromInstallConfig(ic).String()},
178166
},
179167
},
180168
},
@@ -183,7 +171,7 @@ func GenerateClusterAssets(installConfig *installconfig.InstallConfig, clusterID
183171
}
184172
awsCluster.SetGroupVersionKind(capa.GroupVersion.WithKind("AWSCluster"))
185173

186-
if installConfig.Config.Publish == types.ExternalPublishingStrategy {
174+
if ic.Config.Publish == types.ExternalPublishingStrategy {
187175
// FIXME: CAPA bug. Remove when fixed upstream
188176
// The primary and secondary load balancers in CAPA share the same
189177
// security group. However, specifying an ingress rule only in the
@@ -218,41 +206,15 @@ func GenerateClusterAssets(installConfig *installconfig.InstallConfig, clusterID
218206
}
219207
}
220208

221-
// If the install config has subnets, use them.
222-
if len(installConfig.AWS.Subnets) > 0 {
223-
privateSubnets, err := installConfig.AWS.PrivateSubnets(context.TODO())
224-
if err != nil {
225-
return nil, errors.Wrap(err, "failed to get private subnets")
226-
}
227-
for _, subnet := range privateSubnets {
228-
awsCluster.Spec.NetworkSpec.Subnets = append(awsCluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
229-
ID: subnet.ID,
230-
CidrBlock: subnet.CIDR,
231-
AvailabilityZone: subnet.Zone.Name,
232-
IsPublic: subnet.Public,
233-
})
234-
}
235-
publicSubnets, err := installConfig.AWS.PublicSubnets(context.TODO())
236-
if err != nil {
237-
return nil, errors.Wrap(err, "failed to get public subnets")
238-
}
239-
240-
for _, subnet := range publicSubnets {
241-
awsCluster.Spec.NetworkSpec.Subnets = append(awsCluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
242-
ID: subnet.ID,
243-
CidrBlock: subnet.CIDR,
244-
AvailabilityZone: subnet.Zone.Name,
245-
IsPublic: subnet.Public,
246-
})
247-
}
248-
249-
vpc, err := installConfig.AWS.VPC(context.TODO())
250-
if err != nil {
251-
return nil, errors.Wrap(err, "failed to get VPC")
252-
}
253-
awsCluster.Spec.NetworkSpec.VPC = capa.VPCSpec{
254-
ID: vpc,
255-
}
209+
// Set the NetworkSpec.Subnets from VPC and zones (managed)
210+
// or subnets (BYO VPC) based in the install-config.yaml.
211+
err = setSubnets(context.TODO(), &zonesInput{
212+
InstallConfig: ic,
213+
ClusterID: clusterID,
214+
Cluster: awsCluster,
215+
})
216+
if err != nil {
217+
return nil, fmt.Errorf("failed to set cluster zones or subnets: %w", err)
256218
}
257219

258220
manifests = append(manifests, &asset.RuntimeFile{

pkg/asset/manifests/aws/zones.go

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
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+
type subnetsInput struct {
19+
vpc string
20+
privateSubnets aws.Subnets
21+
publicSubnets aws.Subnets
22+
}
23+
24+
type zonesInput struct {
25+
InstallConfig *installconfig.InstallConfig
26+
Cluster *capa.AWSCluster
27+
ClusterID *installconfig.ClusterID
28+
ZonesInRegion []string
29+
Subnets *subnetsInput
30+
}
31+
32+
// GatherZonesFromMetadata retrieves zones from AWS API to be used
33+
// when building the subnets to CAPA.
34+
func (zin *zonesInput) GatherZonesFromMetadata(ctx context.Context) (err error) {
35+
zin.ZonesInRegion, err = zin.InstallConfig.AWS.AvailabilityZones(ctx)
36+
if err != nil {
37+
return fmt.Errorf("failed to get availability zones: %w", err)
38+
}
39+
return nil
40+
}
41+
42+
// GatherSubnetsFromMetadata retrieves subnets from AWS API to be used
43+
// when building the subnets to CAPA.
44+
func (zin *zonesInput) GatherSubnetsFromMetadata(ctx context.Context) (err error) {
45+
zin.Subnets = &subnetsInput{}
46+
if zin.Subnets.privateSubnets, err = zin.InstallConfig.AWS.PrivateSubnets(ctx); err != nil {
47+
return fmt.Errorf("failed to get private subnets: %w", err)
48+
}
49+
if zin.Subnets.publicSubnets, err = zin.InstallConfig.AWS.PublicSubnets(ctx); err != nil {
50+
return fmt.Errorf("failed to get public subnets: %w", err)
51+
}
52+
if zin.Subnets.vpc, err = zin.InstallConfig.AWS.VPC(ctx); err != nil {
53+
return fmt.Errorf("failed to get VPC: %w", err)
54+
}
55+
return nil
56+
}
57+
58+
type zonesCAPI struct {
59+
controlPlaneZones sets.Set[string]
60+
computeZones sets.Set[string]
61+
}
62+
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))
67+
}
68+
69+
// SetAvailabilityZones insert the zone to the given compute pool, and to
70+
// the regular zone (zone type availability-zone) list.
71+
func (zo *zonesCAPI) SetAvailabilityZones(pool string, zones []string) {
72+
switch pool {
73+
case types.MachinePoolControlPlaneRoleName:
74+
zo.controlPlaneZones.Insert(zones...)
75+
76+
case types.MachinePoolComputeRoleName:
77+
zo.computeZones.Insert(zones...)
78+
}
79+
}
80+
81+
// SetDefaultConfigZones evaluates if machine pools (control plane and workers) have been
82+
// set the zones from install-config.yaml, if not sets the default from platform, when exists,
83+
// otherwise set the default from the region discovered from AWS API.
84+
func (zo *zonesCAPI) SetDefaultConfigZones(pool string, defConfig []string, defRegion []string) {
85+
zones := []string{}
86+
switch pool {
87+
case types.MachinePoolControlPlaneRoleName:
88+
if len(zo.controlPlaneZones) == 0 && len(defConfig) > 0 {
89+
zones = defConfig
90+
} else if len(zo.controlPlaneZones) == 0 {
91+
zones = defRegion
92+
}
93+
zo.controlPlaneZones.Insert(zones...)
94+
95+
case types.MachinePoolComputeRoleName:
96+
if len(zo.computeZones) == 0 && len(defConfig) > 0 {
97+
zones = defConfig
98+
} else if len(zo.computeZones) == 0 {
99+
zones = defRegion
100+
}
101+
zo.computeZones.Insert(zones...)
102+
}
103+
}
104+
105+
// setSubnets is the entrypoint to create the CAPI NetworkSpec structures
106+
// for managed or BYO VPC deployments from install-config.yaml.
107+
// The NetworkSpec.Subnets will be populated with the desired zones.
108+
func setSubnets(ctx context.Context, in *zonesInput) error {
109+
if in.InstallConfig == nil {
110+
return fmt.Errorf("failed to get installConfig")
111+
}
112+
if in.InstallConfig.AWS == nil {
113+
return fmt.Errorf("failed to get AWS metadata")
114+
}
115+
if in.InstallConfig.Config == nil {
116+
return fmt.Errorf("unable to get Config")
117+
}
118+
if in.Cluster == nil {
119+
return fmt.Errorf("failed to get AWSCluster config")
120+
}
121+
if len(in.InstallConfig.Config.AWS.Subnets) > 0 {
122+
if err := in.GatherSubnetsFromMetadata(ctx); err != nil {
123+
return fmt.Errorf("failed to get subnets from metadata: %w", err)
124+
}
125+
return setSubnetsBYOVPC(in)
126+
}
127+
128+
if err := in.GatherZonesFromMetadata(ctx); err != nil {
129+
return fmt.Errorf("failed to get availability zones from metadata: %w", err)
130+
}
131+
return setSubnetsManagedVPC(in)
132+
}
133+
134+
// setSubnetsBYOVPC creates the CAPI NetworkSpec.Subnets setting the
135+
// 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,
137+
// 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.
140+
func setSubnetsBYOVPC(in *zonesInput) error {
141+
in.Cluster.Spec.NetworkSpec.VPC = capa.VPCSpec{
142+
ID: in.Subnets.vpc,
143+
}
144+
for _, subnet := range in.Subnets.privateSubnets {
145+
in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
146+
ID: subnet.ID,
147+
CidrBlock: subnet.CIDR,
148+
AvailabilityZone: subnet.Zone.Name,
149+
IsPublic: subnet.Public,
150+
})
151+
}
152+
153+
for _, subnet := range in.Subnets.publicSubnets {
154+
in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
155+
ID: subnet.ID,
156+
CidrBlock: subnet.CIDR,
157+
AvailabilityZone: subnet.Zone.Name,
158+
IsPublic: subnet.Public,
159+
})
160+
}
161+
162+
return nil
163+
}
164+
165+
// setSubnetsManagedVPC creates the CAPI NetworkSpec.VPC and the NetworkSpec.Subnets,
166+
// setting the desired zones from install-config.yaml in the managed
167+
// 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.
173+
// The CIDR blocks are calculated leaving free blocks to allow future expansions,
174+
// in Day-2, when desired.
175+
func setSubnetsManagedVPC(in *zonesInput) error {
176+
out, err := extractZonesFromInstallConfig(in)
177+
if err != nil {
178+
return fmt.Errorf("failed to get availability zones: %w", err)
179+
}
180+
181+
allZones := out.AvailabilityZones()
182+
isPublishingExternal := in.InstallConfig.Config.Publish == types.ExternalPublishingStrategy
183+
mainCIDR := capiutils.CIDRFromInstallConfig(in.InstallConfig)
184+
in.Cluster.Spec.NetworkSpec.VPC = capa.VPCSpec{
185+
CidrBlock: mainCIDR.String(),
186+
}
187+
188+
// Base subnets considering only private zones, leaving one free block to allow
189+
// future subnet expansions in Day-2.
190+
numSubnets := len(allZones) + 1
191+
192+
// Public subnets consumes one range from base blocks.
193+
if isPublishingExternal {
194+
numSubnets++
195+
}
196+
197+
privateCIDRs, err := utilscidr.SplitIntoSubnetsIPv4(mainCIDR.String(), numSubnets)
198+
if err != nil {
199+
return fmt.Errorf("unable to retrieve CIDR blocks for all private subnets: %w", err)
200+
}
201+
202+
var publicCIDRs []*net.IPNet
203+
if isPublishingExternal {
204+
// The last num(zones) blocks are dedicated to the public subnets.
205+
publicCIDRs, err = utilscidr.SplitIntoSubnetsIPv4(privateCIDRs[len(allZones)].String(), len(allZones))
206+
if err != nil {
207+
return fmt.Errorf("unable to retrieve CIDR blocks for all public subnets: %w", err)
208+
}
209+
}
210+
211+
// Create subnets from zone pool with type availability-zone
212+
if len(privateCIDRs) < len(allZones) {
213+
return fmt.Errorf("unable to define CIDR blocks to all zones for private subnets")
214+
}
215+
if isPublishingExternal && len(publicCIDRs) < len(allZones) {
216+
return fmt.Errorf("unable to define CIDR blocks to all zones for public subnets")
217+
}
218+
219+
for idxCIDR, zone := range allZones {
220+
in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
221+
AvailabilityZone: zone,
222+
CidrBlock: privateCIDRs[idxCIDR].String(),
223+
ID: fmt.Sprintf("%s-subnet-private-%s", in.ClusterID.InfraID, zone),
224+
IsPublic: false,
225+
})
226+
if isPublishingExternal {
227+
in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
228+
AvailabilityZone: zone,
229+
CidrBlock: publicCIDRs[idxCIDR].String(),
230+
ID: fmt.Sprintf("%s-subnet-public-%s", in.ClusterID.InfraID, zone),
231+
IsPublic: true,
232+
})
233+
}
234+
}
235+
return nil
236+
}
237+
238+
// 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](),
243+
}
244+
245+
cfg := in.InstallConfig.Config
246+
defaultZones := []string{}
247+
if cfg.AWS != nil && cfg.AWS.DefaultMachinePlatform != nil && len(cfg.AWS.DefaultMachinePlatform.Zones) > 0 {
248+
defaultZones = cfg.AWS.DefaultMachinePlatform.Zones
249+
}
250+
251+
if cfg.ControlPlane != nil && cfg.ControlPlane.Platform.AWS != nil {
252+
out.SetAvailabilityZones(types.MachinePoolControlPlaneRoleName, cfg.ControlPlane.Platform.AWS.Zones)
253+
}
254+
out.SetDefaultConfigZones(types.MachinePoolControlPlaneRoleName, defaultZones, in.ZonesInRegion)
255+
256+
for _, pool := range cfg.Compute {
257+
if pool.Platform.AWS == nil {
258+
continue
259+
}
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
265+
if pool.Name == types.MachinePoolEdgeRoleName {
266+
continue
267+
}
268+
out.SetDefaultConfigZones(types.MachinePoolComputeRoleName, defaultZones, in.ZonesInRegion)
269+
}
270+
return &out, nil
271+
}

0 commit comments

Comments
 (0)