internal/controllers/liveness/namespace.go (141 lines of code) (raw):

package liveness import ( "context" "fmt" "time" apiv1 "github.com/Azure/eno/api/v1" "github.com/Azure/eno/internal/manager" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) var orphanableKinds = []string{"Symphony", "Composition", "ResourceSlice"} // namespaceController is responsible for progressing resource deletion when the namespace is forcibly deleted. // This can happen if clients get tricky with the /finalize API. // Without this controller Eno resources will never be deleted since updates to remove the finalizers will fail. type namespaceController struct { client client.Client creationGracePeriod time.Duration orphanCheckIterations int } func NewNamespaceController(mgr ctrl.Manager, checks int, creationGracePeriod time.Duration) error { b := ctrl.NewControllerManagedBy(mgr).For(&corev1.Namespace{}) for _, kind := range orphanableKinds { b = b.Watches(&metav1.PartialObjectMetadata{ TypeMeta: metav1.TypeMeta{ Kind: kind, APIVersion: apiv1.SchemeGroupVersion.String(), }, }, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: o.GetNamespace()}}} })) } return b.WithLogConstructor(manager.NewLogConstructor(mgr, "namespaceLivenessController")). Complete(&namespaceController{ client: mgr.GetClient(), creationGracePeriod: creationGracePeriod, orphanCheckIterations: checks, }) } func (c *namespaceController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := logr.FromContextOrDiscard(ctx) ns := &corev1.Namespace{} ns.Name = req.Name err := c.client.Get(ctx, req.NamespacedName, ns) if client.IgnoreNotFound(err) != nil { logger.Error(err, "failed to get namespace") return ctrl.Result{}, err } const annoKey = "eno.azure.io/recreation-reason" const annoValue = "OrphanedResources" // Delete the recreated namespace immediately. // Its finalizers will keep it around until we've had time to remove our finalizers. logger = logger.WithValues("resourceNamespace", ns.Name) if ns.Annotations != nil && ns.Annotations[annoKey] == annoValue { if ns.DeletionTimestamp != nil { return ctrl.Result{}, c.cleanup(ctx, req.Name) } err := c.client.Delete(ctx, ns) if err != nil { logger.Error(err, "failed to delete namespace") return ctrl.Result{}, err } logger.V(0).Info("deleting recreated namespace") return ctrl.Result{}, nil } if err == nil { // Successful GETs mean the namespace still exists - nothing for us to do return ctrl.Result{}, nil } // Avoid recreating the namespace when it doesn't have any orphaned resources for i := 1; true; i++ { var foundOrphans bool for _, kind := range orphanableKinds { hasOrphans, res, err := c.findOrphans(ctx, ns.Name, kind) if err != nil { logger.Error(err, "failed to find orphaned resources", "resourceKind", kind) return ctrl.Result{}, err } if res != nil { return *res, nil } if hasOrphans { foundOrphans = true } } if !foundOrphans { return ctrl.Result{}, nil } if i >= c.orphanCheckIterations { break } // Sleep a bit before the next check to let informers catch up. time.Sleep(time.Second / 2) } // Recreate the namespace briefly so we can remove the finalizers. // Any updates (including finalizer updates) will fail if the namespace doesn't exist. ns.Annotations = map[string]string{annoKey: annoValue} err = c.client.Create(ctx, ns) if err != nil { logger.Error(err, "failed to create namespace") return ctrl.Result{}, err } logger.V(0).Info("recreated missing namespace to free orphaned resources") return ctrl.Result{}, nil } const removeFinalizersPatch = `[{ "op": "remove", "path": "/metadata/finalizers" }]` func (c *namespaceController) cleanup(ctx context.Context, ns string) error { logger := logr.FromContextOrDiscard(ctx).WithValues("resourceNamespace", ns) logger.V(1).Info("deleting any remaining resources in orphaned namespace") for _, kind := range orphanableKinds { err := c.client.DeleteAllOf(ctx, &metav1.PartialObjectMetadata{ TypeMeta: metav1.TypeMeta{Kind: kind, APIVersion: apiv1.SchemeGroupVersion.String()}, }, client.InNamespace(ns)) if err != nil { return fmt.Errorf("deleting resources of kind %q: %w", kind, err) } } return nil } func (c *namespaceController) findOrphans(ctx context.Context, ns, kind string) (bool, *ctrl.Result, error) { logger := logr.FromContextOrDiscard(ctx).WithValues("namespace", ns) list := &metav1.PartialObjectMetadataList{} list.Kind = kind list.APIVersion = "eno.azure.io/v1" err := c.client.List(ctx, list, client.InNamespace(ns)) if err != nil { return false, nil, err } if len(list.Items) == 0 { return false, nil, nil // no orphaned resources, nothing to do } if delta := time.Since(mostRecentCreation(list)); delta < c.creationGracePeriod { logger.V(1).Info("refusing to free orphaned resources because one or more are too new", "resourceKind", kind) return false, &ctrl.Result{RequeueAfter: delta}, nil // namespace probably just hasn't hit the cache yet } return true, nil, nil } func mostRecentCreation(list *metav1.PartialObjectMetadataList) time.Time { var max time.Time for _, item := range list.Items { if item.CreationTimestamp.After(max) { max = item.CreationTimestamp.Time } } return max }