Skip to content

Commit 5edfbdb

Browse files
committed
feat: add --default-subnets option
1 parent de50bdd commit 5edfbdb

15 files changed

+518
-84
lines changed

controllers/ingress/group_controller.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func NewGroupReconciler(cloud services.Cloud, k8sClient client.Client, eventReco
6363
cloud.EC2(), cloud.ELBV2(), cloud.ACM(),
6464
annotationParser, subnetsResolver,
6565
authConfigBuilder, enhancedBackendBuilder, trackingProvider, elbv2TaggingManager, controllerConfig.FeatureGates,
66-
cloud.VpcID(), controllerConfig.ClusterName, controllerConfig.DefaultTags, controllerConfig.ExternalManagedTags,
66+
cloud.VpcID(), controllerConfig.ClusterName, controllerConfig.DefaultSubnets, controllerConfig.DefaultTags, controllerConfig.ExternalManagedTags,
6767
controllerConfig.DefaultSSLPolicy, controllerConfig.DefaultTargetType, controllerConfig.DefaultLoadBalancerScheme, backendSGProvider, sgResolver,
6868
controllerConfig.EnableBackendSecurityGroup, controllerConfig.DisableRestrictedSGRules, controllerConfig.IngressConfig.AllowedCertificateAuthorityARNs, controllerConfig.FeatureGates.Enabled(config.EnableIPTargetType), logger, metricsCollector)
6969
stackMarshaller := deploy.NewDefaultStackMarshaller()

controllers/service/service_controller.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func NewServiceReconciler(cloud services.Cloud, k8sClient client.Client, eventRe
4949
trackingProvider := tracking.NewDefaultProvider(serviceTagPrefix, controllerConfig.ClusterName)
5050
serviceUtils := service.NewServiceUtils(annotationParser, serviceFinalizer, controllerConfig.ServiceConfig.LoadBalancerClass, controllerConfig.FeatureGates)
5151
modelBuilder := service.NewDefaultModelBuilder(annotationParser, subnetsResolver, vpcInfoProvider, cloud.VpcID(), trackingProvider,
52-
elbv2TaggingManager, cloud.EC2(), controllerConfig.FeatureGates, controllerConfig.ClusterName, controllerConfig.DefaultTags, controllerConfig.ExternalManagedTags,
52+
elbv2TaggingManager, cloud.EC2(), controllerConfig.FeatureGates, controllerConfig.ClusterName, controllerConfig.DefaultSubnets, controllerConfig.DefaultTags, controllerConfig.ExternalManagedTags,
5353
controllerConfig.DefaultSSLPolicy, controllerConfig.DefaultTargetType, controllerConfig.DefaultLoadBalancerScheme, controllerConfig.FeatureGates.Enabled(config.EnableIPTargetType), serviceUtils,
5454
backendSGProvider, sgResolver, controllerConfig.EnableBackendSecurityGroup, controllerConfig.DisableRestrictedSGRules, logger, metricsCollector)
5555
stackMarshaller := deploy.NewDefaultStackMarshaller()

docs/deploy/configurations.md

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Currently, you can set only 1 namespace to watch in this flag. See [this Kuberne
7777
| backend-security-group | string | | Backend security group id to use for the ingress rules on the worker node SG |
7878
| cluster-name | string | | Kubernetes cluster name |
7979
| default-ssl-policy | string | ELBSecurityPolicy-2016-08 | Default SSL Policy that will be applied to all Ingresses or Services that do not have the SSL Policy annotation |
80+
| default-subnets | stringList | [] | Default subnets to be selected when not explicitly specified through annotations or other methods |
8081
| default-tags | stringMap | | AWS Tags that will be applied to all AWS resources managed by this controller. Specified Tags takes highest priority |
8182
| default-target-type | string | instance | Default target type for Ingresses and Services - ip, instance |
8283
| default-load-balancer-scheme | string | internal | Default scheme for ELBs - internal, internet-facing |

docs/deploy/subnet_discovery.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ A subnet is classified as public if its route table contains a route to an Inter
6161
The controller selects one subnet per availability zone. When multiple subnets exist per Availability Zone, the following priority order applies:
6262

6363
1. Subnets with cluster tag for the current cluster (`kubernetes.io/cluster/<clusterName>`) are prioritized
64-
2. Subnets with lower lexicographical order of subnet ID are prioritized
64+
2. Subnets with the `--default-subnets` flag (prioritized in the order specified)
65+
3. Subnets with lower lexicographical order of subnet ID are prioritized
6566

6667
## Minimum Subnet Requirements
6768

pkg/config/controller_config.go

+30-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
const (
1616
flagLogLevel = "log-level"
1717
flagK8sClusterName = "cluster-name"
18+
flagDefaultSubnets = "default-subnets"
1819
flagDefaultTags = "default-tags"
1920
flagDefaultTargetType = "default-target-type"
2021
flagDefaultLoadBalancerScheme = "default-load-balancer-scheme"
@@ -69,6 +70,9 @@ type ControllerConfig struct {
6970
// Configurations for the Service controller
7071
ServiceConfig ServiceConfig
7172

73+
// Default subnets that will be used for all AWS resources managed by the networking controller.
74+
DefaultSubnets []string
75+
7276
// Default AWS Tags that will be applied to all AWS resources managed by this controller.
7377
DefaultTags map[string]string
7478

@@ -119,6 +123,8 @@ func (cfg *ControllerConfig) BindFlags(fs *pflag.FlagSet) {
119123
fs.StringVar(&cfg.LogLevel, flagLogLevel, defaultLogLevel,
120124
"Set the controller log level - info(default), debug")
121125
fs.StringVar(&cfg.ClusterName, flagK8sClusterName, "", "Kubernetes cluster name")
126+
fs.StringSliceVar(&cfg.DefaultSubnets, flagDefaultSubnets, nil,
127+
"Default subnets that will be used for all AWS resources managed by the networking controller")
122128
fs.StringToStringVar(&cfg.DefaultTags, flagDefaultTags, nil,
123129
"Default AWS Tags that will be applied to all AWS resources managed by this controller")
124130
fs.StringVar(&cfg.DefaultTargetType, flagDefaultTargetType, string(elbv2.TargetTypeInstance),
@@ -162,7 +168,9 @@ func (cfg *ControllerConfig) Validate() error {
162168
if len(cfg.ClusterName) == 0 {
163169
return errors.New("kubernetes cluster name must be specified")
164170
}
165-
171+
if err := cfg.validateDefaultSubnets(); err != nil {
172+
return err
173+
}
166174
if err := cfg.validateDefaultTagsCollisionWithTrackingTags(); err != nil {
167175
return err
168176
}
@@ -184,6 +192,27 @@ func (cfg *ControllerConfig) Validate() error {
184192
return nil
185193
}
186194

195+
func (cfg *ControllerConfig) validateDefaultSubnets() error {
196+
if len(cfg.DefaultSubnets) == 0 {
197+
return nil
198+
}
199+
for _, subnetID := range cfg.DefaultSubnets {
200+
if !strings.HasPrefix(subnetID, "subnet-") {
201+
return errors.Errorf("invalid value %v for default subnet id", subnetID)
202+
}
203+
}
204+
205+
//validate duplicate subnet ids
206+
seen := make(map[string]bool)
207+
for _, str := range cfg.DefaultSubnets {
208+
if seen[str] {
209+
return errors.Errorf("duplicate subnet id %v is specified in the --default-subnets flag", str)
210+
}
211+
seen[str] = true
212+
}
213+
return nil
214+
}
215+
187216
func (cfg *ControllerConfig) validateDefaultTagsCollisionWithTrackingTags() error {
188217
for tagKey := range cfg.DefaultTags {
189218
if trackingTagKeys.Has(tagKey) {

pkg/config/controller_config_test.go

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package config
22

33
import (
4+
"testing"
5+
46
"github.com/pkg/errors"
57
"github.com/stretchr/testify/assert"
6-
"testing"
78
)
89

910
func TestControllerConfig_validateDefaultTagsCollisionWithTrackingTags(t *testing.T) {
@@ -164,3 +165,49 @@ func TestControllerConfig_validateExternalManagedTagsCollisionWithDefaultTags(t
164165
})
165166
}
166167
}
168+
169+
func TestControllerConfig_validateDefaultSubnets(t *testing.T) {
170+
type fields struct {
171+
DefaultSubnets []string
172+
}
173+
tests := []struct {
174+
name string
175+
fields fields
176+
wantErr error
177+
}{
178+
{
179+
name: "default subnets is empty",
180+
fields: fields{
181+
DefaultSubnets: nil,
182+
},
183+
wantErr: nil,
184+
},
185+
{
186+
name: "default subnets is not empty",
187+
fields: fields{
188+
DefaultSubnets: []string{"subnet-1", "subnet-2"},
189+
},
190+
wantErr: nil,
191+
},
192+
{
193+
name: "default subnets is not empty and duplicate subnets are specified",
194+
fields: fields{
195+
DefaultSubnets: []string{"subnet-1", "subnet-2", "subnet-1"},
196+
},
197+
wantErr: errors.New("duplicate subnet id subnet-1 is specified in the --default-subnets flag"),
198+
},
199+
}
200+
for _, tt := range tests {
201+
t.Run(tt.name, func(t *testing.T) {
202+
cfg := &ControllerConfig{
203+
DefaultSubnets: tt.fields.DefaultSubnets,
204+
}
205+
err := cfg.validateDefaultSubnets()
206+
if tt.wantErr != nil {
207+
assert.EqualError(t, err, tt.wantErr.Error())
208+
} else {
209+
assert.NoError(t, err)
210+
}
211+
})
212+
}
213+
}

pkg/ingress/model_build_load_balancer.go

+1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ func (t *defaultModelBuildTask) buildLoadBalancerSubnetMappings(ctx context.Cont
260260
chosenSubnets, err := t.subnetsResolver.ResolveViaDiscovery(ctx,
261261
networking.WithSubnetsResolveLBType(elbv2model.LoadBalancerTypeApplication),
262262
networking.WithSubnetsResolveLBScheme(scheme),
263+
networking.WithDefaultSubnets(t.defaultSubnets),
263264
)
264265
if err != nil {
265266
return nil, errors.Wrap(err, "couldn't auto-discover subnets")

pkg/ingress/model_builder.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func NewDefaultModelBuilder(k8sClient client.Client, eventRecorder record.EventR
4646
annotationParser annotations.Parser, subnetsResolver networkingpkg.SubnetsResolver,
4747
authConfigBuilder AuthConfigBuilder, enhancedBackendBuilder EnhancedBackendBuilder,
4848
trackingProvider tracking.Provider, elbv2TaggingManager elbv2deploy.TaggingManager, featureGates config.FeatureGates,
49-
vpcID string, clusterName string, defaultTags map[string]string, externalManagedTags []string, defaultSSLPolicy string, defaultTargetType string, defaultLoadBalancerScheme string,
49+
vpcID string, clusterName string, defaultSubnets []string, defaultTags map[string]string, externalManagedTags []string, defaultSSLPolicy string, defaultTargetType string, defaultLoadBalancerScheme string,
5050
backendSGProvider networkingpkg.BackendSGProvider, sgResolver networkingpkg.SecurityGroupResolver,
5151
enableBackendSG bool, disableRestrictedSGRules bool, allowedCAARNs []string, enableIPTargetType bool, logger logr.Logger, metricsCollector lbcmetrics.MetricCollector) *defaultModelBuilder {
5252
certDiscovery := NewACMCertDiscovery(acmClient, allowedCAARNs, logger)
@@ -69,6 +69,7 @@ func NewDefaultModelBuilder(k8sClient client.Client, eventRecorder record.EventR
6969
trackingProvider: trackingProvider,
7070
elbv2TaggingManager: elbv2TaggingManager,
7171
featureGates: featureGates,
72+
defaultSubnets: defaultSubnets,
7273
defaultTags: defaultTags,
7374
externalManagedTags: sets.NewString(externalManagedTags...),
7475
defaultSSLPolicy: defaultSSLPolicy,
@@ -105,6 +106,7 @@ type defaultModelBuilder struct {
105106
trackingProvider tracking.Provider
106107
elbv2TaggingManager elbv2deploy.TaggingManager
107108
featureGates config.FeatureGates
109+
defaultSubnets []string
108110
defaultTags map[string]string
109111
externalManagedTags sets.String
110112
defaultSSLPolicy string
@@ -148,6 +150,7 @@ func (b *defaultModelBuilder) Build(ctx context.Context, ingGroup Group, metrics
148150
ingGroup: ingGroup,
149151
stack: stack,
150152

153+
defaultSubnets: b.defaultSubnets,
151154
defaultTags: b.defaultTags,
152155
externalManagedTags: b.externalManagedTags,
153156
defaultIPAddressType: elbv2model.IPAddressTypeIPV4,
@@ -205,6 +208,7 @@ type defaultModelBuildTask struct {
205208
disableRestrictedSGRules bool
206209
enableIPTargetType bool
207210

211+
defaultSubnets []string
208212
defaultTags map[string]string
209213
externalManagedTags sets.String
210214
defaultIPAddressType elbv2model.IPAddressType

pkg/networking/subnet_resolver.go

+42-10
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ type SubnetsResolveOptions struct {
5353
// The Load Balancer Scheme.
5454
// By default, it's internet-facing.
5555
LBScheme elbv2model.LoadBalancerScheme
56+
// Subnets specified with --default-subnets
57+
DefaultSubnets []string
5658
}
5759

5860
// ApplyOptions applies slice of SubnetsResolveOption.
@@ -86,6 +88,13 @@ func WithSubnetsResolveLBScheme(lbScheme elbv2model.LoadBalancerScheme) SubnetsR
8688
}
8789
}
8890

91+
// WithDefaultSubnets generates an option that configures DefaultSubnets.
92+
func WithDefaultSubnets(defaultSubnets []string) SubnetsResolveOption {
93+
return func(opts *SubnetsResolveOptions) {
94+
opts.DefaultSubnets = defaultSubnets
95+
}
96+
}
97+
8998
// SubnetsResolver is responsible for resolve EC2 Subnets for Load Balancers.
9099
type SubnetsResolver interface {
91100
// ResolveViaDiscovery resolve subnets by auto discover matching subnets.
@@ -412,7 +421,7 @@ func (r *defaultSubnetsResolver) validateSpecifiedSubnets(ctx context.Context, s
412421
// chooseAndValidateSubnetsPerAZ will choose one subnet per AZ from eligible subnets and then validate against chosen subnets.
413422
func (r *defaultSubnetsResolver) chooseAndValidateSubnetsPerAZ(ctx context.Context, subnets []ec2types.Subnet, resolveOpts SubnetsResolveOptions) ([]ec2types.Subnet, error) {
414423
categorizedSubnets := r.categorizeSubnetsByEligibility(subnets)
415-
chosenSubnets := r.chooseSubnetsPerAZ(categorizedSubnets.eligible)
424+
chosenSubnets := r.chooseSubnetsPerAZ(categorizedSubnets.eligible, resolveOpts.DefaultSubnets)
416425
if len(chosenSubnets) == 0 {
417426
return nil, fmt.Errorf("unable to resolve at least one subnet. Evaluated %d subnets: %d are tagged for other clusters, and %d have insufficient available IP addresses",
418427
len(subnets), len(categorizedSubnets.ineligibleClusterTag), len(categorizedSubnets.insufficientIPs))
@@ -452,26 +461,49 @@ func (r *defaultSubnetsResolver) categorizeSubnetsByEligibility(subnets []ec2typ
452461

453462
// chooseSubnetsPerAZ will choose one subnet per AZ.
454463
// * subnets with current cluster tag will be prioritized.
455-
func (r *defaultSubnetsResolver) chooseSubnetsPerAZ(subnets []ec2types.Subnet) []ec2types.Subnet {
464+
func (r *defaultSubnetsResolver) chooseSubnetsPerAZ(subnets []ec2types.Subnet, defaultSubnets []string) []ec2types.Subnet {
465+
466+
prioritySubnetMap := make(map[string]int)
467+
468+
if len(defaultSubnets) > 0 {
469+
for i, subnetID := range defaultSubnets {
470+
prioritySubnetMap[subnetID] = i
471+
}
472+
}
456473
subnetsByAZ := mapSDKSubnetsByAZ(subnets)
457474
chosenSubnets := make([]ec2types.Subnet, 0, len(subnetsByAZ))
458475
for az, azSubnets := range subnetsByAZ {
459476
if len(azSubnets) == 1 {
460477
chosenSubnets = append(chosenSubnets, azSubnets[0])
461-
} else if len(subnets) > 1 {
462-
sort.Slice(subnets, func(i, j int) bool {
463-
subnetIHasCurrentClusterTag := r.isSubnetContainsCurrentClusterTag(subnets[i])
464-
subnetJHasCurrentClusterTag := r.isSubnetContainsCurrentClusterTag(subnets[j])
478+
} else if len(azSubnets) > 1 {
479+
sort.Slice(azSubnets, func(i, j int) bool {
480+
subnetIHasCurrentClusterTag := r.isSubnetContainsCurrentClusterTag(azSubnets[i])
481+
subnetJHasCurrentClusterTag := r.isSubnetContainsCurrentClusterTag(azSubnets[j])
465482
if subnetIHasCurrentClusterTag && (!subnetJHasCurrentClusterTag) {
466483
return true
467484
} else if (!subnetIHasCurrentClusterTag) && subnetJHasCurrentClusterTag {
468485
return false
469486
}
470-
return awssdk.ToString(subnets[i].SubnetId) < awssdk.ToString(subnets[j].SubnetId)
487+
488+
// When azSubnets are specified in --default-azSubnets, the azSubnets list will be sorted according to this order.
489+
// Any azSubnets not specified in --default-azSubnets will be sorted in lexicographical order and placed after the prioritized azSubnets.
490+
iVal, iExists := prioritySubnetMap[awssdk.ToString(azSubnets[i].SubnetId)]
491+
jVal, jExists := prioritySubnetMap[awssdk.ToString(azSubnets[j].SubnetId)]
492+
493+
if iExists && jExists {
494+
return iVal < jVal
495+
}
496+
if iExists {
497+
return true
498+
}
499+
if jExists {
500+
return false
501+
}
502+
return awssdk.ToString(azSubnets[i].SubnetId) < awssdk.ToString(azSubnets[j].SubnetId)
471503
})
472-
r.logger.V(1).Info("multiple subnets in the same AvailabilityZone", "AvailabilityZone", az,
473-
"chosen", subnets[0].SubnetId, "ignored", extractSubnetIDs(subnets[1:]))
474-
chosenSubnets = append(chosenSubnets, subnets[0])
504+
r.logger.V(1).Info("multiple azSubnets in the same AvailabilityZone", "AvailabilityZone", az,
505+
"chosen", azSubnets[0].SubnetId, "ignored", extractSubnetIDs(azSubnets[1:]))
506+
chosenSubnets = append(chosenSubnets, azSubnets[0])
475507
}
476508
}
477509
sortSubnetsByID(chosenSubnets)

0 commit comments

Comments
 (0)