Skip to content

Commit a188827

Browse files
committed
pkg ipamd: implement secondary ENI exclusion logic
1 parent 7946fd8 commit a188827

File tree

6 files changed

+245
-205
lines changed

6 files changed

+245
-205
lines changed

pkg/awsutils/awsutils.go

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,14 @@ type APIs interface {
223223

224224
IsPrefixDelegationSupported() bool
225225

226-
// IsPrimarySubnetExcluded returns if the primary subnet is excluded for pod IPs
227-
IsPrimarySubnetExcluded(ctx context.Context) (bool, error)
226+
// GetENISubnetID gets the subnet ID for an ENI from AWS
227+
GetENISubnetID(ctx context.Context, eniID string) (string, error)
228+
229+
// GetVpcSubnets returns all subnets in the VPC
230+
GetVpcSubnets(ctx context.Context) ([]ec2types.Subnet, error)
231+
232+
// IsSubnetExcluded returns if a subnet is excluded for pod IPs based on its tags
233+
IsSubnetExcluded(ctx context.Context, subnetID string) (bool, error)
228234
}
229235

230236
// EC2InstanceMetadataCache caches instance metadata
@@ -554,8 +560,8 @@ func (cache *EC2InstanceMetadataCache) discoverCustomSecurityGroups(ctx context.
554560
return sgIDs, nil
555561
}
556562

557-
// getENISubnetID gets the subnet ID for an ENI from AWS
558-
func (cache *EC2InstanceMetadataCache) getENISubnetID(ctx context.Context, eniID string) (string, error) {
563+
// GetENISubnetID gets the subnet ID for an ENI from AWS
564+
func (cache *EC2InstanceMetadataCache) GetENISubnetID(ctx context.Context, eniID string) (string, error) {
559565
describeInput := &ec2.DescribeNetworkInterfacesInput{
560566
NetworkInterfaceIds: []string{eniID},
561567
}
@@ -574,7 +580,7 @@ func (cache *EC2InstanceMetadataCache) getENISubnetID(ctx context.Context, eniID
574580

575581
// Helper function to check if an ENI is in a secondary subnet
576582
func (cache *EC2InstanceMetadataCache) isENIInSecondarySubnet(ctx context.Context, eniID string) bool {
577-
eniSubnetID, err := cache.getENISubnetID(ctx, eniID)
583+
eniSubnetID, err := cache.GetENISubnetID(ctx, eniID)
578584
return err == nil && eniSubnetID != cache.subnetID
579585
}
580586

@@ -1175,13 +1181,13 @@ func (cache *EC2InstanceMetadataCache) createENI(ctx context.Context, sg []*stri
11751181
}
11761182
} else {
11771183
if cache.useSubnetDiscovery {
1178-
subnetResult, vpcErr := cache.getVpcSubnets(ctx)
1184+
subnetResult, vpcErr := cache.GetVpcSubnets(ctx)
11791185
if vpcErr != nil {
11801186
log.Warnf("Failed to call ec2:DescribeSubnets: %v", vpcErr)
11811187
log.Info("Defaulting to same subnet as the primary interface for the new ENI")
11821188

11831189
// Even in fallback, check if primary subnet is excluded
1184-
excluded, checkErr := cache.IsPrimarySubnetExcluded(ctx)
1190+
excluded, checkErr := cache.IsSubnetExcluded(ctx, cache.subnetID)
11851191
if checkErr != nil {
11861192
log.Warnf("Failed to check if primary subnet is excluded: %v. Proceeding with ENI creation attempt.", checkErr)
11871193
} else if excluded {
@@ -1232,7 +1238,7 @@ func (cache *EC2InstanceMetadataCache) createENI(ctx context.Context, sg []*stri
12321238
} else {
12331239
log.Info("Using same security group config as the primary interface for the new ENI")
12341240
// When subnet discovery is disabled, check if primary subnet is excluded
1235-
excluded, checkErr := cache.IsPrimarySubnetExcluded(ctx)
1241+
excluded, checkErr := cache.IsSubnetExcluded(ctx, cache.subnetID)
12361242
if checkErr != nil {
12371243
// If we can't determine exclusion status, log warning and proceed
12381244
log.Warnf("Failed to check if primary subnet is excluded: %v. Proceeding with ENI creation attempt.", checkErr)
@@ -1250,7 +1256,7 @@ func (cache *EC2InstanceMetadataCache) createENI(ctx context.Context, sg []*stri
12501256
return "", errors.Wrap(err, "failed to create network interface")
12511257
}
12521258

1253-
func (cache *EC2InstanceMetadataCache) getVpcSubnets(ctx context.Context) ([]ec2types.Subnet, error) {
1259+
func (cache *EC2InstanceMetadataCache) GetVpcSubnets(ctx context.Context) ([]ec2types.Subnet, error) {
12541260
describeSubnetInput := &ec2.DescribeSubnetsInput{
12551261
Filters: []ec2types.Filter{
12561262
{
@@ -2576,31 +2582,34 @@ func checkAPIErrorAndBroadcastEvent(err error, api string) {
25762582
}
25772583
}
25782584

2579-
// isPrimarySubnetExcluded checks if the primary subnet has the kubernetes.io/role/cni=0 tag
2580-
func (cache *EC2InstanceMetadataCache) IsPrimarySubnetExcluded(ctx context.Context) (bool, error) {
2581-
// Get the primary subnet information
2582-
describeSubnetInput := &ec2.DescribeSubnetsInput{
2583-
SubnetIds: []string{cache.subnetID},
2584-
}
2585-
2586-
start := time.Now()
2587-
subnetResult, err := cache.ec2SVC.DescribeSubnets(ctx, describeSubnetInput)
2588-
prometheusmetrics.Ec2ApiReq.WithLabelValues("DescribeSubnets").Inc()
2589-
prometheusmetrics.AwsAPILatency.WithLabelValues("DescribeSubnets", fmt.Sprint(err != nil), awsReqStatus(err)).Observe(msSince(start))
2585+
// IsSubnetExcluded checks if a subnet is excluded by examining its kubernetes.io/role/cni tag
2586+
func (cache *EC2InstanceMetadataCache) IsSubnetExcluded(ctx context.Context, subnetID string) (bool, error) {
2587+
// Get all VPC subnets with their tags
2588+
subnets, err := cache.GetVpcSubnets(ctx)
25902589
if err != nil {
2591-
checkAPIErrorAndBroadcastEvent(err, "ec2:DescribeSubnets")
2592-
awsAPIErrInc("DescribeSubnets", err)
2593-
prometheusmetrics.Ec2ApiErr.WithLabelValues("DescribeSubnets").Inc()
2594-
log.Errorf("Failed to describe primary subnet %s: %v", cache.subnetID, err)
2595-
return false, fmt.Errorf("isPrimarySubnetExcluded: unable to describe primary subnet: %v", err)
2596-
}
2590+
return false, fmt.Errorf("failed to get VPC subnets: %v", err)
2591+
}
2592+
2593+
// Find the specific subnet and check its tags
2594+
for _, subnet := range subnets {
2595+
if *subnet.SubnetId == subnetID {
2596+
// Check if the subnet has the exclusion tag kubernetes.io/role/cni=0
2597+
for _, tag := range subnet.Tags {
2598+
if *tag.Key == "kubernetes.io/role/cni" {
2599+
tagValue := *tag.Value
2600+
excluded := tagValue == "0"
2601+
log.Debugf("IsSubnetExcluded: subnet %s has tag kubernetes.io/role/cni=%s, excluded=%t", subnetID, tagValue, excluded)
2602+
return excluded, nil
2603+
}
2604+
}
25972605

2598-
if len(subnetResult.Subnets) == 0 {
2599-
log.Errorf("Primary subnet %s not found in DescribeSubnets response", cache.subnetID)
2600-
return false, fmt.Errorf("isPrimarySubnetExcluded: primary subnet not found")
2606+
// If no kubernetes.io/role/cni tag found, subnet is not explicitly excluded
2607+
log.Debugf("IsSubnetExcluded: subnet %s has no kubernetes.io/role/cni tag, not excluded", subnetID)
2608+
return false, nil
2609+
}
26012610
}
26022611

2603-
subnet := subnetResult.Subnets[0]
2604-
// Check if the subnet has the exclusion tag
2605-
return !validTag(subnet, true), nil
2612+
// Subnet not found in VPC
2613+
log.Warnf("IsSubnetExcluded: subnet %s not found in VPC", subnetID)
2614+
return false, fmt.Errorf("subnet %s not found in VPC", subnetID)
26062615
}

pkg/awsutils/awsutils_test.go

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2514,7 +2514,7 @@ func TestValidTag(t *testing.T) {
25142514
}
25152515
}
25162516

2517-
func TestIsPrimarySubnetExcluded(t *testing.T) {
2517+
func TestIsSubnetExcluded(t *testing.T) {
25182518
ctrl, mockEC2 := setup(t)
25192519
defer ctrl.Finish()
25202520

@@ -2526,7 +2526,7 @@ func TestIsPrimarySubnetExcluded(t *testing.T) {
25262526
wantErr bool
25272527
}{
25282528
{
2529-
name: "primary subnet with tag value 0 - excluded",
2529+
name: "subnet with tag value 0 - excluded",
25302530
subnetTags: []ec2types.Tag{
25312531
{
25322532
Key: aws.String("kubernetes.io/role/cni"),
@@ -2537,7 +2537,7 @@ func TestIsPrimarySubnetExcluded(t *testing.T) {
25372537
wantErr: false,
25382538
},
25392539
{
2540-
name: "primary subnet with tag value 1 - not excluded",
2540+
name: "subnet with tag value 1 - not excluded",
25412541
subnetTags: []ec2types.Tag{
25422542
{
25432543
Key: aws.String("kubernetes.io/role/cni"),
@@ -2548,7 +2548,7 @@ func TestIsPrimarySubnetExcluded(t *testing.T) {
25482548
wantErr: false,
25492549
},
25502550
{
2551-
name: "primary subnet without tag - not excluded",
2551+
name: "subnet without tag - not excluded",
25522552
subnetTags: []ec2types.Tag{
25532553
{
25542554
Key: aws.String("other-tag"),
@@ -2559,7 +2559,7 @@ func TestIsPrimarySubnetExcluded(t *testing.T) {
25592559
wantErr: false,
25602560
},
25612561
{
2562-
name: "DescribeSubnets API error",
2562+
name: "GetVpcSubnets API error",
25632563
describeError: errors.New("API error"),
25642564
want: false,
25652565
wantErr: true,
@@ -2574,26 +2574,30 @@ func TestIsPrimarySubnetExcluded(t *testing.T) {
25742574

25752575
for _, tt := range tests {
25762576
t.Run(tt.name, func(t *testing.T) {
2577+
cache := &EC2InstanceMetadataCache{
2578+
ec2SVC: mockEC2,
2579+
vpcID: "vpc-12345",
2580+
availabilityZone: "us-west-2a",
2581+
}
2582+
25772583
if tt.describeError != nil {
2584+
// Mock DescribeSubnets to return error
25782585
mockEC2.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any()).Return(nil, tt.describeError)
25792586
} else {
2580-
result := &ec2.DescribeSubnetsOutput{
2587+
// Mock DescribeSubnets for GetVpcSubnets
2588+
subnetResult := &ec2.DescribeSubnetsOutput{
25812589
Subnets: []ec2types.Subnet{
25822590
{
25832591
SubnetId: aws.String(subnetID),
25842592
Tags: tt.subnetTags,
2593+
VpcId: aws.String("vpc-12345"),
25852594
},
25862595
},
25872596
}
2588-
mockEC2.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any()).Return(result, nil)
2589-
}
2590-
2591-
cache := &EC2InstanceMetadataCache{
2592-
ec2SVC: mockEC2,
2593-
subnetID: subnetID,
2597+
mockEC2.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any()).Return(subnetResult, nil)
25942598
}
25952599

2596-
got, err := cache.IsPrimarySubnetExcluded(context.Background())
2600+
got, err := cache.IsSubnetExcluded(context.Background(), subnetID)
25972601
if tt.wantErr {
25982602
assert.Error(t, err)
25992603
} else {
@@ -3071,7 +3075,7 @@ func TestDiscoverCustomSecurityGroups(t *testing.T) {
30713075
}
30723076
}
30733077

3074-
// TestGetENISubnetID tests the getENISubnetID helper method
3078+
// TestGetENISubnetID tests the GetENISubnetID helper method
30753079
func TestGetENISubnetID(t *testing.T) {
30763080
ctrl, mockEC2 := setup(t)
30773081
defer ctrl.Finish()
@@ -3135,7 +3139,7 @@ func TestGetENISubnetID(t *testing.T) {
31353139
gomock.Any(),
31363140
).Return(tt.describeOutput, tt.describeError)
31373141

3138-
subnetID, err := cache.getENISubnetID(context.Background(), tt.eniID)
3142+
subnetID, err := cache.GetENISubnetID(context.Background(), tt.eniID)
31393143

31403144
if tt.expectError {
31413145
assert.Error(t, err)

pkg/awsutils/mocks/awsutils_mocks.go

Lines changed: 36 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)