internal/controllers/composition/controller.go (190 lines of code) (raw):
package composition
import (
"context"
"fmt"
"time"
krmv1 "github.com/Azure/eno/pkg/krm/functions/api/v1"
"github.com/go-logr/logr"
"github.com/google/uuid"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
apiv1 "github.com/Azure/eno/api/v1"
"github.com/Azure/eno/internal/inputs"
"github.com/Azure/eno/internal/manager"
)
type compositionController struct {
client client.Client
}
func NewController(mgr ctrl.Manager) error {
c := &compositionController{
client: mgr.GetClient(),
}
return ctrl.NewControllerManagedBy(mgr).
For(&apiv1.Composition{}).
WithLogConstructor(manager.NewLogConstructor(mgr, "compositionController")).
Complete(c)
}
func (c *compositionController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := logr.FromContextOrDiscard(ctx)
comp := &apiv1.Composition{}
err := c.client.Get(ctx, req.NamespacedName, comp)
if err != nil {
logger.Error(err, "failed to get composition resource")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
logger = logger.WithValues("compositionName", comp.Name, "compositionNamespace", comp.Namespace, "compositionGeneration", comp.Generation, "synthesisUUID", comp.Status.GetCurrentSynthesisUUID())
if comp.DeletionTimestamp != nil {
return c.reconcileDeletedComposition(ctx, comp)
}
if controllerutil.AddFinalizer(comp, "eno.azure.io/cleanup") {
err = c.client.Update(ctx, comp)
if err != nil {
logger.Error(err, "failed to update composition")
return ctrl.Result{}, err
}
logger.V(1).Info("added cleanup finalizer to composition")
return ctrl.Result{}, nil
}
synth := &apiv1.Synthesizer{}
synth.Name = comp.Spec.Synthesizer.Name
err = c.client.Get(ctx, client.ObjectKeyFromObject(synth), synth)
if errors.IsNotFound(err) {
synth = nil
err = nil
}
if err != nil {
logger.Error(err, "failed to get synthesizer")
return ctrl.Result{}, err
}
if synth != nil {
logger = logger.WithValues("synthesizerName", synth.Name, "synthesizerGeneration", synth.Generation)
}
ctx = logr.NewContext(ctx, logger)
// Write the simplified status
modified, err := c.reconcileSimplifiedStatus(ctx, synth, comp)
if err != nil {
logger.Error(err, "failed to reconcile simplified status")
return ctrl.Result{}, err
}
if modified || synth == nil {
return ctrl.Result{}, nil
}
// Enforce the synthesis timeout period
if syn := comp.Status.InFlightSynthesis; syn != nil && syn.Canceled == nil && syn.Initialized != nil && synth.Spec.PodTimeout != nil {
delta := time.Until(syn.Initialized.Time.Add(synth.Spec.PodTimeout.Duration))
if delta > 0 {
return ctrl.Result{RequeueAfter: delta}, nil
}
syn.Canceled = ptr.To(metav1.Now())
if err := c.client.Status().Update(ctx, comp); err != nil {
logger.Error(err, "failed to update composition status to reflect synthesis timeout")
return ctrl.Result{}, err
}
logger.V(0).Info("synthesis timed out")
return ctrl.Result{}, nil
}
return ctrl.Result{}, nil
}
func (c *compositionController) reconcileDeletedComposition(ctx context.Context, comp *apiv1.Composition) (ctrl.Result, error) {
logger := logr.FromContextOrDiscard(ctx)
syn := comp.Status.CurrentSynthesis
if syn != nil {
// Deletion increments the composition's generation, but the reconstitution cache is only invalidated
// when the synthesized generation (from the status) changes, which will never happen because synthesis
// is righly disabled for deleted compositions. We break out of this deadlock condition by updating
// the status without actually synthesizing.
if syn.ObservedCompositionGeneration != comp.Generation {
comp.Status.CurrentSynthesis.ObservedCompositionGeneration = comp.Generation
comp.Status.CurrentSynthesis.UUID = uuid.NewString()
comp.Status.CurrentSynthesis.Synthesized = ptr.To(metav1.Now())
comp.Status.CurrentSynthesis.Reconciled = nil
comp.Status.CurrentSynthesis.Ready = nil
err := c.client.Status().Update(ctx, comp)
if err != nil {
logger.Error(err, "failed to update current composition generation")
return ctrl.Result{}, err
}
logger.V(0).Info("updated composition status to reflect deletion", "synthesisUUID", comp.Status.CurrentSynthesis.UUID)
return ctrl.Result{}, nil
}
if syn.Reconciled == nil {
logger.V(1).Info("refusing to remove composition finalizer because it is still being reconciled")
return ctrl.Result{}, nil
}
}
if controllerutil.RemoveFinalizer(comp, "eno.azure.io/cleanup") {
err := c.client.Update(ctx, comp)
if err != nil {
logger.Error(err, "failed to remove finalizer")
return ctrl.Result{}, err
}
logger.V(0).Info("removed finalizer from composition")
}
return ctrl.Result{}, nil
}
func (c *compositionController) reconcileSimplifiedStatus(ctx context.Context, synth *apiv1.Synthesizer, comp *apiv1.Composition) (bool, error) {
logger := logr.FromContextOrDiscard(ctx)
next := buildSimplifiedStatus(synth, comp)
if equality.Semantic.DeepEqual(next, comp.Status.Simplified) {
return false, nil
}
logger.V(0).Info("composition status changed", "status", next, "previousStatus", comp.Status.Simplified)
copy := comp.DeepCopy()
copy.Status.Simplified = next
if err := c.client.Status().Patch(ctx, copy, client.MergeFrom(comp)); err != nil {
return false, fmt.Errorf("patching simplified status: %w", err)
}
return true, nil
}
func buildSimplifiedStatus(synth *apiv1.Synthesizer, comp *apiv1.Composition) *apiv1.SimplifiedStatus {
status := &apiv1.SimplifiedStatus{}
if comp.DeletionTimestamp != nil {
status.Status = "Deleting"
return status
}
if synth == nil {
status.Status = "MissingSynthesizer"
return status
}
if syn := comp.Status.InFlightSynthesis; syn != nil {
for _, result := range syn.Results {
if result.Severity == krmv1.ResultSeverityError {
status.Error = result.Message
break
}
}
if syn.Canceled != nil {
if status.Error == "" {
status.Error = "Timeout"
}
status.Status = "SynthesisBackoff"
return status
}
status.Status = "Synthesizing"
return status
}
if !inputs.Exist(synth, comp) {
status.Status = "MissingInputs"
return status
}
if inputs.OutOfLockstep(synth, comp.Status.InputRevisions) {
status.Status = "MismatchedInputs"
}
if comp.Status.CurrentSynthesis == nil && comp.Status.InFlightSynthesis == nil {
status.Status = "PendingSynthesis"
return status
}
if syn := comp.Status.CurrentSynthesis; syn.Ready != nil {
status.Status = "Ready"
return status
}
if syn := comp.Status.CurrentSynthesis; syn.Reconciled != nil {
status.Status = "NotReady"
return status
}
if syn := comp.Status.CurrentSynthesis; syn != nil && syn.Reconciled == nil {
status.Status = "Reconciling"
return status
}
status.Status = "Unknown"
return status
}