Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: create vpc objects in explicitly provided availability zones #4543

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/v1beta1/awscluster_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error {
}

dst.Spec.NetworkSpec.AdditionalControlPlaneIngressRules = restored.Spec.NetworkSpec.AdditionalControlPlaneIngressRules
dst.Spec.NetworkSpec.VPC.AvailabilityZones = restored.Spec.NetworkSpec.VPC.AvailabilityZones

if restored.Spec.NetworkSpec.VPC.IPAMPool != nil {
if dst.Spec.NetworkSpec.VPC.IPAMPool == nil {
Expand Down
1 change: 1 addition & 0 deletions api/v1beta1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions api/v1beta2/network_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ type VPCSpec struct {
// +kubebuilder:validation:Enum=Ordered;Random
AvailabilityZoneSelection *AZSelectionScheme `json:"availabilityZoneSelection,omitempty"`

// AvailabilityZones defines a list of Availability Zones in which to create network resources in.
// If defined, both AvailabilityZoneUsageLimit and AvailabilityZoneSelection are ignored.
// +optional
AvailabilityZones []string `json:"availabilityZones,omitempty"`

// EmptyRoutesDefaultVPCSecurityGroup specifies whether the default VPC security group ingress
// and egress rules should be removed.
//
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,14 @@ spec:
to 3
minimum: 1
type: integer
availabilityZones:
description: AvailabilityZones defines a list of Availability
Zones in which to create network resources in. If defined,
both AvailabilityZoneUsageLimit and AvailabilityZoneSelection
are ignored.
items:
type: string
type: array
cidrBlock:
description: CidrBlock is the CIDR block to be used when the
provider creates a managed VPC. Defaults to 10.0.0.0/16.
Expand Down Expand Up @@ -2404,6 +2412,14 @@ spec:
to 3
minimum: 1
type: integer
availabilityZones:
description: AvailabilityZones defines a list of Availability
Zones in which to create network resources in. If defined,
both AvailabilityZoneUsageLimit and AvailabilityZoneSelection
are ignored.
items:
type: string
type: array
cidrBlock:
description: CidrBlock is the CIDR block to be used when the
provider creates a managed VPC. Defaults to 10.0.0.0/16.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,14 @@ spec:
to 3
minimum: 1
type: integer
availabilityZones:
description: AvailabilityZones defines a list of Availability
Zones in which to create network resources in. If defined,
both AvailabilityZoneUsageLimit and AvailabilityZoneSelection
are ignored.
items:
type: string
type: array
cidrBlock:
description: CidrBlock is the CIDR block to be used when the
provider creates a managed VPC. Defaults to 10.0.0.0/16.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,14 @@ spec:
when creating default subnets. Defaults to 3
minimum: 1
type: integer
availabilityZones:
description: AvailabilityZones defines a list of Availability
Zones in which to create network resources in. If
defined, both AvailabilityZoneUsageLimit and AvailabilityZoneSelection
are ignored.
items:
type: string
type: array
cidrBlock:
description: CidrBlock is the CIDR block to be used
when the provider creates a managed VPC. Defaults
Expand Down
66 changes: 42 additions & 24 deletions pkg/cloud/services/network/subnets.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,32 +212,18 @@ func (s *Service) reconcileSubnets() error {
}

func (s *Service) getDefaultSubnets() (infrav1.Subnets, error) {
zones, err := s.getAvailableZones()
if err != nil {
return nil, err
}

maxZones := defaultMaxNumAZs
if s.scope.VPC().AvailabilityZoneUsageLimit != nil {
maxZones = *s.scope.VPC().AvailabilityZoneUsageLimit
}
selectionScheme := infrav1.AZSelectionSchemeOrdered
if s.scope.VPC().AvailabilityZoneSelection != nil {
selectionScheme = *s.scope.VPC().AvailabilityZoneSelection
}
var (
// by default, we will take the set availability zones, if they are defined.
// if not, we fall back to the two other settings.
zones = s.scope.VPC().AvailabilityZones
err error
)

if len(zones) > maxZones {
s.scope.Debug("region has more than AvailabilityZoneUsageLimit availability zones, picking zones to use", "region", s.scope.Region(), "AvailabilityZoneUsageLimit", maxZones)
if selectionScheme == infrav1.AZSelectionSchemeRandom {
rand.Shuffle(len(zones), func(i, j int) {
zones[i], zones[j] = zones[j], zones[i]
})
}
if selectionScheme == infrav1.AZSelectionSchemeOrdered {
sort.Strings(zones)
if len(zones) == 0 {
zones, err = s.selectZones()
if err != nil {
return nil, errors.Wrap(err, "failed to select availability zones")
}
zones = zones[:maxZones]
s.scope.Debug("zones selected", "region", s.scope.Region(), "zones", zones)
}

// 1 private subnet for each AZ plus 1 other subnet that will be further sub-divided for the public subnets
Expand Down Expand Up @@ -304,6 +290,38 @@ func (s *Service) getDefaultSubnets() (infrav1.Subnets, error) {
return subnets, nil
}

func (s *Service) selectZones() ([]string, error) {
zones, err := s.getAvailableZones()
if err != nil {
return nil, err
}

maxZones := defaultMaxNumAZs
if s.scope.VPC().AvailabilityZoneUsageLimit != nil {
maxZones = *s.scope.VPC().AvailabilityZoneUsageLimit
}
selectionScheme := infrav1.AZSelectionSchemeOrdered
if s.scope.VPC().AvailabilityZoneSelection != nil {
selectionScheme = *s.scope.VPC().AvailabilityZoneSelection
}

if len(zones) > maxZones {
s.scope.Debug("region has more than AvailabilityZoneUsageLimit availability zones, picking zones to use", "region", s.scope.Region(), "AvailabilityZoneUsageLimit", maxZones)
if selectionScheme == infrav1.AZSelectionSchemeRandom {
rand.Shuffle(len(zones), func(i, j int) {
zones[i], zones[j] = zones[j], zones[i]
})
}
if selectionScheme == infrav1.AZSelectionSchemeOrdered {
sort.Strings(zones)
}
zones = zones[:maxZones]
s.scope.Debug("zones selected", "region", s.scope.Region(), "zones", zones)
}

return zones, nil
}

func (s *Service) deleteSubnets() error {
if s.scope.VPC().IsUnmanaged(s.scope.Name()) {
s.scope.Trace("Skipping subnets deletion in unmanaged mode")
Expand Down
140 changes: 140 additions & 0 deletions pkg/cloud/services/network/subnets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2316,6 +2316,146 @@ func TestReconcileSubnets(t *testing.T) {
After(zone2PrivateSubnet)
},
},
{
name: "Managed VPC, no existing subnets exist, one az is explicitly defined, expect one private and one public from default",
input: NewClusterScope().WithNetwork(&infrav1.NetworkSpec{
VPC: infrav1.VPCSpec{
ID: subnetsVPCID,
Tags: infrav1.Tags{
infrav1.ClusterTagKey("test-cluster"): "owned",
},
CidrBlock: defaultVPCCidr,
AvailabilityZones: []string{"us-east-1b"},
},
Subnets: []infrav1.SubnetSpec{},
}),
expect: func(m *mocks.MockEC2APIMockRecorder) {
describeCall := m.DescribeSubnetsWithContext(context.TODO(), gomock.Eq(&ec2.DescribeSubnetsInput{
Filters: []*ec2.Filter{
{
Name: aws.String("state"),
Values: []*string{aws.String("pending"), aws.String("available")},
},
{
Name: aws.String("vpc-id"),
Values: []*string{aws.String(subnetsVPCID)},
},
},
})).Return(&ec2.DescribeSubnetsOutput{}, nil)

m.DescribeRouteTablesWithContext(context.TODO(), gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})).
Return(&ec2.DescribeRouteTablesOutput{}, nil)

m.DescribeNatGatewaysPagesWithContext(context.TODO(),
gomock.Eq(&ec2.DescribeNatGatewaysInput{
Filter: []*ec2.Filter{
{
Name: aws.String("vpc-id"),
Values: []*string{aws.String(subnetsVPCID)},
},
{
Name: aws.String("state"),
Values: []*string{aws.String("pending"), aws.String("available")},
},
},
}),
gomock.Any()).Return(nil)

zone1PublicSubnet := m.CreateSubnetWithContext(context.TODO(), gomock.Eq(&ec2.CreateSubnetInput{
VpcId: aws.String(subnetsVPCID),
CidrBlock: aws.String("10.0.0.0/17"),
AvailabilityZone: aws.String("us-east-1b"),
TagSpecifications: []*ec2.TagSpecification{
{
ResourceType: aws.String("subnet"),
Tags: []*ec2.Tag{
{
Key: aws.String("Name"),
Value: aws.String("test-cluster-subnet-public-us-east-1b"),
},
{
Key: aws.String("kubernetes.io/cluster/test-cluster"),
Value: aws.String("shared"),
},
{
Key: aws.String("kubernetes.io/role/elb"),
Value: aws.String("1"),
},
{
Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
Value: aws.String("owned"),
},
{
Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
Value: aws.String("public"),
},
},
},
},
})).Return(&ec2.CreateSubnetOutput{
Subnet: &ec2.Subnet{
VpcId: aws.String(subnetsVPCID),
SubnetId: aws.String("subnet-1"),
CidrBlock: aws.String("10.0.0.0/17"),
AvailabilityZone: aws.String("us-east-1b"),
MapPublicIpOnLaunch: aws.Bool(false),
},
}, nil).After(describeCall)

m.WaitUntilSubnetAvailableWithContext(context.TODO(), gomock.Any()).After(zone1PublicSubnet)

m.ModifySubnetAttributeWithContext(context.TODO(), &ec2.ModifySubnetAttributeInput{
MapPublicIpOnLaunch: &ec2.AttributeBooleanValue{
Value: aws.Bool(true),
},
SubnetId: aws.String("subnet-1"),
}).Return(&ec2.ModifySubnetAttributeOutput{}, nil).After(zone1PublicSubnet)

zone1PrivateSubnet := m.CreateSubnetWithContext(context.TODO(), gomock.Eq(&ec2.CreateSubnetInput{
VpcId: aws.String(subnetsVPCID),
CidrBlock: aws.String("10.0.128.0/17"),
AvailabilityZone: aws.String("us-east-1b"),
TagSpecifications: []*ec2.TagSpecification{
{
ResourceType: aws.String("subnet"),
Tags: []*ec2.Tag{
{
Key: aws.String("Name"),
Value: aws.String("test-cluster-subnet-private-us-east-1b"),
},
{
Key: aws.String("kubernetes.io/cluster/test-cluster"),
Value: aws.String("shared"),
},
{
Key: aws.String("kubernetes.io/role/internal-elb"),
Value: aws.String("1"),
},
{
Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
Value: aws.String("owned"),
},
{
Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
Value: aws.String("private"),
},
},
},
},
})).Return(&ec2.CreateSubnetOutput{
Subnet: &ec2.Subnet{
VpcId: aws.String(subnetsVPCID),
SubnetId: aws.String("subnet-2"),
CidrBlock: aws.String("10.0.128.0/17"),
AvailabilityZone: aws.String("us-east-1b"),
MapPublicIpOnLaunch: aws.Bool(false),
},
}, nil).After(zone1PublicSubnet)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor]

Isn't it an implementation detail in which order private and public subnets are created i.e. we shouldn't check for private.After(public) here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters that much here? I copied this entire section from previous tests. :) So whatever they do this one does as well. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not a blocker at all


m.WaitUntilSubnetAvailableWithContext(context.TODO(), gomock.Any()).
After(zone1PrivateSubnet)
},
},
}

for _, tc := range testCases {
Expand Down
Loading