pkg/controllers/member/endpointsliceexport/controller.go (81 lines of code) (raw):
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/
// Package endpointsliceexport features the EndpointSliceExport controller for cleaning up left over
// EndpointSlices on the hub cluster.
package endpointsliceexport
import (
"context"
"time"
discoveryv1 "k8s.io/api/discovery/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
fleetnetv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1"
"go.goms.io/fleet-networking/pkg/common/objectmeta"
)
const (
endpointSliceExportRetryInterval = time.Minute * 5
)
type Reconciler struct {
MemberClient client.Client
HubClient client.Client
}
//+kubebuilder:rbac:groups=networking.fleet.azure.com,resources=endpointsliceexports,verbs=get;list;watch;delete
//+kubebuilder:rbac:groups=discovery.k8s.io,resources=endpointslices,verbs=get;list;watch
// Reconcile verifies if an EndpointSliceExport in the hub cluster matches with a exported EndpointSlice from
// the current member cluster, and will clean up EndpointSliceExports that fail to match.
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
endpointSliceExportRef := klog.KRef(req.Namespace, req.Name)
startTime := time.Now()
klog.V(2).InfoS("Reconciliation starts", "endpointSliceExport", endpointSliceExportRef)
defer func() {
latency := time.Since(startTime).Seconds()
klog.V(2).InfoS("Reconciliation ends", "endpointSliceExport", endpointSliceExportRef, "latency", latency)
}()
// Retrieve the EndpointSliceExport object.
endpointSliceExport := &fleetnetv1alpha1.EndpointSliceExport{}
if err := r.HubClient.Get(ctx, req.NamespacedName, endpointSliceExport); err != nil {
// Skip the reconciliation if the EndpointSliceExport does not exist; this should only happen when an
// EndpointSliceExport is deleted before the controller gets a chance to reconcile it;
// this requires no action to take on this controller's end.
if errors.IsNotFound(err) {
klog.V(4).InfoS("Ignoring NotFound endpointSliceExport", "endpointSliceExport", endpointSliceExportRef)
return ctrl.Result{}, nil
}
klog.ErrorS(err, "Failed to get endpointSliceExport", "endpointSliceExport", endpointSliceExportRef)
return ctrl.Result{}, err
}
// Check if the EndpointSliceExport refers to an existing EndpointSlice.
endpointSlice := &discoveryv1.EndpointSlice{}
endpointSliceKey := types.NamespacedName{
Namespace: endpointSliceExport.Spec.EndpointSliceReference.Namespace,
Name: endpointSliceExport.Spec.EndpointSliceReference.Name,
}
endpointSliceRef := klog.KRef(endpointSliceKey.Namespace, endpointSliceKey.Name)
err := r.MemberClient.Get(ctx, endpointSliceKey, endpointSlice)
switch {
case errors.IsNotFound(err):
// The matching EndpointSlice is not found; the EndpointSliceExport should be deleted.
klog.V(2).InfoS("Referred endpointSlice is not found; delete the endpointSliceExport",
"endpointSliceExport", endpointSliceExportRef,
"endpointSlice", endpointSliceRef,
)
return r.deleteEndpointSliceExport(ctx, endpointSliceExport)
case err != nil:
// An unexpected error has occurred.
klog.ErrorS(err, "Failed to get endpointSlice",
"endpointSliceExport", endpointSliceExportRef,
"endpointSlice", endpointSliceRef)
return ctrl.Result{}, err
}
// Check if the EndpointSliceExport is linked the referred EndpointSlice by the assigned unique name for export.
// This helps guard against some corner cases, e.g.
// * A user tampers with the unique name for export assigned to an EndpointSlice,
// which leads to the same EndpointSlice being exported for multiple times with different names.
// * An EndpointSlice is deleted and immediately re-created with the same name, and the EndpointSlice
// controller fails to unexport the EndpointSlice in time when it is deleted.
if !isEndpointSliceExportLinkedWithEndpointSlice(endpointSliceExport, endpointSlice) {
return r.deleteEndpointSliceExport(ctx, endpointSliceExport)
}
// Periodically re-scan EndpointSliceExports; this help addresses corner cases where an EndpointSlice
// is deleted without the EndpointSlice controller getting a chance to withdraw it from the hub cluster.
return ctrl.Result{RequeueAfter: endpointSliceExportRetryInterval}, nil
}
// SetupWithManager builds a controller with Reconciler and sets it up with a controller manager.
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
// The EndpointSliceExport controller watches over EndpointSliceExport objects.
// TO-DO (chenyu1): use predicates to filter out some events.
For(&fleetnetv1alpha1.EndpointSliceExport{}).
Complete(r)
}
// deleteEndpointSliceExport deletes an EndpointSliceExport from the hub cluster.
func (r *Reconciler) deleteEndpointSliceExport(ctx context.Context, endpointSliceExport *fleetnetv1alpha1.EndpointSliceExport) (ctrl.Result, error) {
if err := r.HubClient.Delete(ctx, endpointSliceExport); err != nil && !errors.IsNotFound(err) {
klog.ErrorS(err, "Failed to delete endpoint slice export", "endpointSliceExport", klog.KObj(endpointSliceExport))
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
// isEndpointSliceExportLinkedWithEndpointSlice returns if an EndpointSliceExport's name matches with the
// unique name for export assigned to an exported EndpointSlice.
func isEndpointSliceExportLinkedWithEndpointSlice(endpointSliceExport *fleetnetv1alpha1.EndpointSliceExport, endpointSlice *discoveryv1.EndpointSlice) bool {
uniqueName, ok := endpointSlice.Annotations[objectmeta.ExportedObjectAnnotationUniqueName]
if !ok || uniqueName != endpointSliceExport.Name {
return false
}
return true
}