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

fix: properly garbage collecting orphaned network interfaces #642

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5d7853b
feat: adding ListNics to instanceprovider interface alongside a refac…
Bryce-Soghigian Jan 8, 2025
4c3e8df
feat: adding garbage collection logic for network interfaces and refa…
Bryce-Soghigian Jan 8, 2025
3a77d94
feat: working poc of ARG Queries and nic garbage collection, need to …
Bryce-Soghigian Jan 8, 2025
d595df5
fix: tests failing due to ListVM
Bryce-Soghigian Jan 8, 2025
4ad1738
feat: split nic and vm into their own gc controllers, added shared st…
Bryce-Soghigian Jan 10, 2025
d066fc5
feat: Add DeleteNic option to instance provider
Bryce-Soghigian Jan 10, 2025
c71f4dc
docs(code-comment): adding clarification to unremovableNics
Bryce-Soghigian Jan 10, 2025
900c48d
test: instanceprovider.ListNics barebones test and arg fake list nic …
Bryce-Soghigian Jan 10, 2025
cce9f3c
fix: bootstrap.sh
Bryce-Soghigian Jan 10, 2025
d7eb78c
feat: adding in VM List into the networkinterface.garbagecollection c…
Bryce-Soghigian Jan 12, 2025
97db74f
refactor: unremovableNics out of the vm gc controller in favor for a …
Bryce-Soghigian Jan 12, 2025
8867815
fix:updating references to cache
Bryce-Soghigian Jan 12, 2025
871aebd
Update pkg/controllers/nodeclaim/garbagecollection/nic_garbagecollect…
Bryce-Soghigian Jan 12, 2025
a97ce3a
test: adding composable network interface options to our test utils b…
Bryce-Soghigian Jan 13, 2025
4f299d8
test: adding test we don't include unmanaged nics
Bryce-Soghigian Jan 13, 2025
9d39576
test: adding network garbage collection suite and happy path
Bryce-Soghigian Jan 13, 2025
85d84f5
test: adding tests for unremovable nics
Bryce-Soghigian Jan 15, 2025
c495b97
test: adding coverage that vm controller cleans up nics
Bryce-Soghigian Jan 15, 2025
176c7f8
refactor: renaming controller
Bryce-Soghigian Jan 16, 2025
d8fa55b
fix: refactor name
Bryce-Soghigian Jan 16, 2025
a02263c
refactor: using import directly
Bryce-Soghigian Jan 17, 2025
41319bf
ci: checking error for controller
Bryce-Soghigian Jan 17, 2025
e300439
fix: ci
Bryce-Soghigian Jan 19, 2025
8418208
fix: addressing comments
Bryce-Soghigian Jan 22, 2025
ad84b51
Update pkg/controllers/nodeclaim/garbagecollection/instance_garbageco…
Bryce-Soghigian Jan 25, 2025
aaccc95
refactor: removing name constant
Bryce-Soghigian Jan 25, 2025
fa14e9a
refactor: moving to test utils
Bryce-Soghigian Jan 25, 2025
fdf3147
fix: removing GetZoneID
Bryce-Soghigian Jan 25, 2025
730b754
style: improving the readability of the network interface garbage col…
Bryce-Soghigian Jan 28, 2025
fe0c4a0
revert: removing lo.FromPtr checks for nodeclaim creation to avoid cr…
Bryce-Soghigian Jan 28, 2025
3d0b0b9
refactor: VirtualmachineProperties needs a default for time created
Bryce-Soghigian Jan 28, 2025
c0e4043
fix: modifying the tests to be aware of time created to properly simu…
Bryce-Soghigian Jan 28, 2025
50d9455
refactor: added nodepoolName to test.VirtualMachine and test.Interface
Bryce-Soghigian Jan 28, 2025
f4f2e59
refactor: moving vm gc tests to use test.VirtualMachine
Bryce-Soghigian Jan 28, 2025
cfa1776
refactor: renaming arg files to azureresourcegraph
Bryce-Soghigian Jan 28, 2025
0ea44f4
refactor: using deleteIfNicExists directly in cleanupAzureResources
Bryce-Soghigian Jan 28, 2025
d9d858a
test: createNicFromQueryResponseData missing id, missing name and hap…
Bryce-Soghigian Jan 28, 2025
d631e19
refactor: using fake.Region for the default region for test.VirtualMa…
Bryce-Soghigian Jan 28, 2025
9d49d25
fix: using input.InterfaceName rather than the outer scope interface …
Bryce-Soghigian Jan 28, 2025
38666ed
test: modifying fake to only initialize the query once
Bryce-Soghigian Jan 28, 2025
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
2 changes: 1 addition & 1 deletion pkg/cloudprovider/cloudprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ func (c *CloudProvider) List(ctx context.Context) ([]*karpv1.NodeClaim, error) {
if err != nil {
return nil, fmt.Errorf("listing instances, %w", err)
}

var nodeClaims []*karpv1.NodeClaim
for _, instance := range instances {
instanceType, err := c.resolveInstanceTypeFromInstance(ctx, instance)
Expand Down Expand Up @@ -337,7 +338,6 @@ func (c *CloudProvider) instanceToNodeClaim(ctx context.Context, vm *armcompute.

labels[karpv1.CapacityTypeLabelKey] = instance.GetCapacityType(vm)

// TODO: v1beta1 new kes/labels
if tag, ok := vm.Tags[instance.NodePoolTagKey]; ok {
labels[karpv1.NodePoolLabelKey] = *tag
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cloudprovider/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ var _ = Describe("CloudProvider", func() {
nodeClaims, _ := cloudProvider.List(ctx)
Expect(azureEnv.AzureResourceGraphAPI.AzureResourceGraphResourcesBehavior.CalledWithInput.Len()).To(Equal(1))
queryRequest := azureEnv.AzureResourceGraphAPI.AzureResourceGraphResourcesBehavior.CalledWithInput.Pop().Query
Expect(*queryRequest.Query).To(Equal(instance.GetListQueryBuilder(azureEnv.AzureResourceGraphAPI.ResourceGroup).String()))
Expect(*queryRequest.Query).To(Equal(instance.GetVMListQueryBuilder(azureEnv.AzureResourceGraphAPI.ResourceGroup).String()))
Expect(nodeClaims).To(HaveLen(1))
Expect(nodeClaims[0]).ToNot(BeNil())
resp, _ := azureEnv.VirtualMachinesAPI.Get(ctx, azureEnv.AzureResourceGraphAPI.ResourceGroup, nodeClaims[0].Name, nil)
Expand Down
5 changes: 4 additions & 1 deletion pkg/controllers/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ func NewControllers(ctx context.Context, mgr manager.Manager, kubeClient client.
nodeclasshash.NewController(kubeClient),
nodeclassstatus.NewController(kubeClient),
nodeclasstermination.NewController(kubeClient, recorder),
nodeclaimgarbagecollection.NewController(kubeClient, cloudProvider),

nodeclaimgarbagecollection.NewVirtualMachine(kubeClient, cloudProvider),
nodeclaimgarbagecollection.NewNetworkInterface(kubeClient, instanceProvider),

// TODO: nodeclaim tagging
inplaceupdate.NewController(kubeClient, instanceProvider),
status.NewController[*v1alpha2.AKSNodeClass](kubeClient, mgr.GetEventRecorderFor("karpenter")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (

"github.com/awslabs/operatorpkg/singleton"

// "github.com/Azure/karpenter-provider-azure/pkg/cloudprovider"
"github.com/samber/lo"
"go.uber.org/multierr"
v1 "k8s.io/api/core/v1"
Expand All @@ -41,21 +40,21 @@ import (
corecloudprovider "sigs.k8s.io/karpenter/pkg/cloudprovider"
)

type Controller struct {
type VirtualMachine struct {
kubeClient client.Client
cloudProvider corecloudprovider.CloudProvider
successfulCount uint64 // keeps track of successful reconciles for more aggressive requeueing near the start of the controller
successfulCount uint64 // keeps track of successful reconciles for more aggressive requeuing near the start of the controller
Bryce-Soghigian marked this conversation as resolved.
Show resolved Hide resolved
}

func NewController(kubeClient client.Client, cloudProvider corecloudprovider.CloudProvider) *Controller {
return &Controller{
func NewVirtualMachine(kubeClient client.Client, cloudProvider corecloudprovider.CloudProvider) *VirtualMachine {
return &VirtualMachine{
kubeClient: kubeClient,
cloudProvider: cloudProvider,
successfulCount: 0,
}
}

func (c *Controller) Reconcile(ctx context.Context) (reconcile.Result, error) {
func (c *VirtualMachine) Reconcile(ctx context.Context) (reconcile.Result, error) {
ctx = injection.WithControllerName(ctx, "instance.garbagecollection")

// We LIST VMs on the CloudProvider BEFORE we grab NodeClaims/Nodes on the cluster so that we make sure that, if
Expand All @@ -65,6 +64,7 @@ func (c *Controller) Reconcile(ctx context.Context) (reconcile.Result, error) {
if err != nil {
return reconcile.Result{}, fmt.Errorf("listing cloudprovider VMs, %w", err)
}

managedRetrieved := lo.Filter(retrieved, func(nc *karpv1.NodeClaim, _ int) bool {
return nc.DeletionTimestamp.IsZero()
})
Expand Down Expand Up @@ -93,7 +93,7 @@ func (c *Controller) Reconcile(ctx context.Context) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: lo.Ternary(c.successfulCount <= 20, time.Second*10, time.Minute*2)}, nil
}

func (c *Controller) garbageCollect(ctx context.Context, nodeClaim *karpv1.NodeClaim, nodeList *v1.NodeList) error {
func (c *VirtualMachine) garbageCollect(ctx context.Context, nodeClaim *karpv1.NodeClaim, nodeList *v1.NodeList) error {
ctx = logging.WithLogger(ctx, logging.FromContext(ctx).With("provider-id", nodeClaim.Status.ProviderID))
if err := c.cloudProvider.Delete(ctx, nodeClaim); err != nil {
return corecloudprovider.IgnoreNodeClaimNotFoundError(err)
Expand All @@ -112,7 +112,7 @@ func (c *Controller) garbageCollect(ctx context.Context, nodeClaim *karpv1.NodeC
return nil
}

func (c *Controller) Register(_ context.Context, m manager.Manager) error {
func (c *VirtualMachine) Register(_ context.Context, m manager.Manager) error {
return controllerruntime.NewControllerManagedBy(m).
Named("instance.garbagecollection").
WatchesRawSource(singleton.Source()).
Expand Down
112 changes: 112 additions & 0 deletions pkg/controllers/nodeclaim/garbagecollection/nic_garbagecollection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
Portions Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package garbagecollection

import (
"context"
"fmt"
"time"

"github.com/samber/lo"
"knative.dev/pkg/logging"

"github.com/awslabs/operatorpkg/singleton"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/util/workqueue"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
karpv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
"sigs.k8s.io/karpenter/pkg/operator/injection"

"github.com/Azure/karpenter-provider-azure/pkg/providers/instance"
)

const (
NicReservationDuration = time.Second * 180
// We set this interval at 5 minutes, as thats how often our NRP limits are reset.
// See: https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/request-limits-and-throttling#network-throttling
NicGarbageCollectionInterval = time.Minute * 5
Bryce-Soghigian marked this conversation as resolved.
Show resolved Hide resolved
)

type NetworkInterface struct {
kubeClient client.Client
instanceProvider instance.Provider
}

func NewNetworkInterface(kubeClient client.Client, instanceProvider instance.Provider) *NetworkInterface {
return &NetworkInterface{
kubeClient: kubeClient,
instanceProvider: instanceProvider,
}
}

func (c *NetworkInterface) populateUnremovableInterfaces(ctx context.Context) (sets.Set[string], error) {
unremovableInterfaces := sets.New[string]()
vms, err := c.instanceProvider.List(ctx)
if err != nil {
return unremovableInterfaces, fmt.Errorf("listing VMs: %w", err)
}
for _, vm := range vms {
unremovableInterfaces.Insert(lo.FromPtr(vm.Name))
}
nodeClaimList := &karpv1.NodeClaimList{}
if err := c.kubeClient.List(ctx, nodeClaimList); err != nil {
return unremovableInterfaces, fmt.Errorf("listing NodeClaims for NIC GC: %w", err)
}

for _, nodeClaim := range nodeClaimList.Items {
unremovableInterfaces.Insert(instance.GenerateResourceName(nodeClaim.Name))
}
return unremovableInterfaces, nil
}

func (c *NetworkInterface) Reconcile(ctx context.Context) (reconcile.Result, error) {
ctx = injection.WithControllerName(ctx, "networkinterface.garbagecollection")
nics, err := c.instanceProvider.ListNics(ctx)
if err != nil {
return reconcile.Result{}, fmt.Errorf("listing NICs: %w", err)
}

unremovableInterfaces, err := c.populateUnremovableInterfaces(ctx)
if err != nil {
return reconcile.Result{}, fmt.Errorf("error listing resources needed to populate unremovable nics %w", err)
}
workqueue.ParallelizeUntil(ctx, 100, len(nics), func(i int) {
nicName := lo.FromPtr(nics[i].Name)
if !unremovableInterfaces.Has(nicName) {
err := c.instanceProvider.DeleteNic(ctx, nicName)
if err != nil {
logging.FromContext(ctx).Error(err)
return
}
Comment on lines +94 to +97
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instance GC controller combines the errors and returns them, here they are only logged an never returned. Why the difference in behaviour?

Copy link
Collaborator Author

@Bryce-Soghigian Bryce-Soghigian Jan 28, 2025

Choose a reason for hiding this comment

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

I tried looking into the original pr to see if we got any rationality behind that: aws/karpenter-provider-aws#3405

Didn't see much.

We shouldn't be requeuing more frequently in the presence of errors. We should probably change that, if VM Deletion fails due to a TooManyRequests error, we can't retry until the quota window resets(After 5 minutes?)

2m might make some sense for vms as the volume of vms that need garbage collection is in most cases significantly lower than the volume of interfaces that need garbage collection.

We could try requeuing depending on the kind of error at different intervals.

If we see TooManyRequests Error wait 5 minutes to retry, if we see IsNicReservedForAnotherVM retry 3 minutes later?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Overall it seems simpler to just not do all of that requeuing and come and optimize it later if we are optimizing for performance. For now just requeuing every 5 minutes seems to be fine.


logging.FromContext(ctx).With("nic", nicName).Infof("garbage collected NIC")
}
})
return reconcile.Result{
RequeueAfter: NicGarbageCollectionInterval,
}, nil
}

func (c *NetworkInterface) Register(_ context.Context, m manager.Manager) error {
return controllerruntime.NewControllerManagedBy(m).
Named("networkinterface.garbagecollection").
WatchesRawSource(singleton.Source()).
Complete(singleton.AsReconciler(c))
}
Loading