internal/controllers/resourceslice/slice.go (162 lines of code) (raw):

package resourceslice import ( "context" "fmt" "time" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" apiv1 "github.com/Azure/eno/api/v1" "github.com/Azure/eno/internal/manager" "github.com/go-logr/logr" ) // sliceController manages the lifecycle of resource slices in the context of their owning composition. // This consists of aggregating their status into the composition, and replacing missing slices. // Deletion of slices is handled by a separate controller to handle cases where the related composition no longer exists. type sliceController struct { client client.Client } func NewController(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&apiv1.Composition{}). Owns(&apiv1.ResourceSlice{}). WithLogConstructor(manager.NewLogConstructor(mgr, "sliceController")). Complete(&sliceController{client: mgr.GetClient()}) } func (s *sliceController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := logr.FromContextOrDiscard(ctx) comp := &apiv1.Composition{} err := s.client.Get(ctx, req.NamespacedName, comp) if err != nil { logger.Error(err, "failed to get composition") return ctrl.Result{}, client.IgnoreNotFound(err) } logger = logger.WithValues("compositionGeneration", comp.Generation, "compositionName", comp.Name, "compositionNamespace", comp.Namespace, "synthesisUUID", comp.Status.GetCurrentSynthesisUUID()) ctx = logr.NewContext(ctx, logger) if comp.Status.CurrentSynthesis == nil { return ctrl.Result{}, nil } snapshot := statusSnapshot{Reconciled: true, Ready: true} for _, ref := range comp.Status.CurrentSynthesis.ResourceSlices { slice := &apiv1.ResourceSlice{} slice.Name = ref.Name slice.Namespace = comp.Namespace err := s.client.Get(ctx, client.ObjectKeyFromObject(slice), slice) if errors.IsNotFound(err) { if comp.DeletionTimestamp != nil { logger.V(1).Info("resource slice is missing, ignoring because composition is being deleted", "resourceSliceName", ref.Name) continue } return s.handleMissingSlice(ctx, comp, ref.Name) } if err != nil { logger.Error(err, "failed to get resource slice") return ctrl.Result{}, err } // Handle a case where the reconciliation controller hasn't updated the slice's status yet if len(slice.Status.Resources) == 0 && len(slice.Spec.Resources) > 0 { snapshot.Ready = false snapshot.Reconciled = false break // no need to check the other slices } // Collect the state of every resource for _, state := range slice.Status.Resources { state := state if resourceNotReconciled(comp, &state) { snapshot.Reconciled = false } if state.Ready == nil { snapshot.Ready = false } if state.Ready != nil && (snapshot.ReadyTime == nil || state.Ready.After(snapshot.ReadyTime.Time)) { snapshot.ReadyTime = state.Ready } } } // Aggregate the status of all slices into the composition if !processCompositionTransition(ctx, comp, snapshot) { return ctrl.Result{}, nil } err = s.client.Status().Update(ctx, comp) if err != nil { logger.Error(err, "failed to update composition status") return ctrl.Result{}, err } logger.V(1).Info("aggregated resource status into composition", "reconciled", snapshot.Reconciled, "ready", snapshot.Ready) return ctrl.Result{}, nil } func (s *sliceController) handleMissingSlice(ctx context.Context, comp *apiv1.Composition, sliceName string) (ctrl.Result, error) { logger := logr.FromContextOrDiscard(ctx) // We can't do anything about missing resource slices if synthesis is already in-flight or it isn't safe to resynthesize if comp.ShouldIgnoreSideEffects() || comp.Status.InFlightSynthesis != nil || comp.ShouldForceResynthesis() { return ctrl.Result{}, nil } // It's possible that newly created slices haven't hit the informer cache yet if synthd := comp.Status.CurrentSynthesis.Synthesized; synthd != nil { delta := time.Since(synthd.Time) if delta < time.Second*5 { return ctrl.Result{RequeueAfter: delta}, nil } } // Be absolutely sure the slice is missing meta := &metav1.PartialObjectMetadata{} meta.Kind = "ResourceSlice" meta.APIVersion = apiv1.SchemeGroupVersion.String() meta.Name = sliceName meta.Namespace = comp.Namespace err := s.client.Get(ctx, client.ObjectKeyFromObject(meta), meta) if err == nil { logger.V(1).Info("resource slice is not missing!", "resourceSliceName", sliceName) return ctrl.Result{}, nil } if !errors.IsNotFound(err) { return ctrl.Result{}, fmt.Errorf("getting resource slice metadata: %w", err) } // Resynthesis is required logger.Info("resource slice is missing - resynthesizing", "resourceSliceName", sliceName) comp.ForceResynthesis() err = s.client.Update(ctx, comp) if err != nil { return ctrl.Result{}, fmt.Errorf("updating composition pending resynthesis: %w", err) } return ctrl.Result{}, nil } func processCompositionTransition(ctx context.Context, comp *apiv1.Composition, snapshot statusSnapshot) (modified bool) { logger := logr.FromContextOrDiscard(ctx) if comp.Status.CurrentSynthesis == nil || ((comp.Status.CurrentSynthesis.Reconciled != nil) == snapshot.Reconciled && (comp.Status.CurrentSynthesis.Ready != nil) == snapshot.Ready) { return false // either no change or no synthesis yet } // Empty compositions should logically become ready immediately after reconciliation if len(comp.Status.CurrentSynthesis.ResourceSlices) == 0 { snapshot.ReadyTime = comp.Status.CurrentSynthesis.Reconciled } now := metav1.Now() comp.Status.CurrentSynthesis.Reconciled = snapshot.GetReconciled(comp, &now, logger) comp.Status.CurrentSynthesis.Ready = snapshot.GetReady(comp, logger) return true } // resourceNotReconciled returns true when the resource has not been reconciled. // - When its status has Reconciled == true // - When it has been deleted and the composition has also been deleted // - When it has been deleted and the composition is configured to orphan resources func resourceNotReconciled(comp *apiv1.Composition, state *apiv1.ResourceState) bool { shouldOrphan := comp.Annotations != nil && comp.Annotations["eno.azure.io/deletion-strategy"] == "orphan" return !state.Reconciled || (!state.Deleted && !shouldOrphan && comp.DeletionTimestamp != nil) } type statusSnapshot struct { Reconciled bool Ready bool ReadyTime *metav1.Time } func (s *statusSnapshot) GetReconciled(comp *apiv1.Composition, now *metav1.Time, logger logr.Logger) *metav1.Time { if !s.Reconciled { return nil } if synthed := comp.Status.CurrentSynthesis.Synthesized; synthed != nil { latency := now.Sub(synthed.Time) if latency > 0 { logger = logger.WithValues("latency", latency.Milliseconds()) } } logger.V(0).Info("composition was reconciled") return now } func (s *statusSnapshot) GetReady(comp *apiv1.Composition, logger logr.Logger) *metav1.Time { if !s.Ready || s.ReadyTime == nil { return nil } if synthed := comp.Status.CurrentSynthesis.Synthesized; synthed != nil { latency := s.ReadyTime.Sub(synthed.Time) if latency > 0 { logger = logger.WithValues("latency", latency.Milliseconds()) } } logger.V(0).Info("composition became ready") return s.ReadyTime }