...

Source file src/edge-infra.dev/pkg/sds/k8s/controllers/terminalctl/terminal_controller.go

Documentation: edge-infra.dev/pkg/sds/k8s/controllers/terminalctl

     1  package terminalctl
     2  
     3  import (
     4  	"context"
     5  	"time"
     6  
     7  	"github.com/fluxcd/pkg/ssa"
     8  	"github.com/go-logr/logr"
     9  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
    10  	ctrl "sigs.k8s.io/controller-runtime"
    11  	"sigs.k8s.io/controller-runtime/pkg/client"
    12  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    13  	"sigs.k8s.io/controller-runtime/pkg/event"
    14  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    15  
    16  	"edge-infra.dev/pkg/k8s/meta/status"
    17  	"edge-infra.dev/pkg/k8s/runtime/conditions"
    18  	"edge-infra.dev/pkg/k8s/runtime/controller/metrics"
    19  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile"
    20  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr"
    21  	"edge-infra.dev/pkg/k8s/runtime/inventory"
    22  	"edge-infra.dev/pkg/k8s/runtime/patch"
    23  	v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1"
    24  	"edge-infra.dev/pkg/sds/k8s/controllers/terminalctl/pkg/plugins"
    25  )
    26  
    27  const (
    28  	ErrPluginFailed          = "plugin failed"
    29  	ErrPluginFinalizerFailed = "plugin finalizer failed"
    30  	ErrInvalidTerminal       = "invalid Terminal spec"
    31  	ErrAddingFinalizer       = "unable to register finalizer"
    32  	ErrTerminalStatusUpdate  = "unable to update Terminal status"
    33  )
    34  
    35  var terminalConditions = reconcile.Conditions{
    36  	Target: status.ReadyCondition,
    37  	Owned: []string{
    38  		string(v1ien.IENController),
    39  	},
    40  	Summarize: []string{
    41  		string(v1ien.IENController),
    42  	},
    43  	NegativePolarity: []string{},
    44  }
    45  
    46  // TerminalReconciler reconciles a Terminal object
    47  type TerminalReconciler struct {
    48  	client.Client
    49  	Log            logr.Logger
    50  	Metrics        metrics.Metrics
    51  	Config         *Config
    52  	DefaultRequeue time.Duration
    53  	Conditions     reconcile.Conditions
    54  	// ResourceManager is a server-side apply client that can poll for the resources
    55  	// we are applying to the server
    56  	ResourceManager *ssa.ResourceManager
    57  	Name            string
    58  }
    59  
    60  func terminalReconcilerPredicate() predicate.Predicate {
    61  	return predicate.Funcs{
    62  		UpdateFunc: func(e event.UpdateEvent) bool {
    63  			return !e.ObjectNew.GetDeletionTimestamp().IsZero() || (e.ObjectNew.GetGeneration() != e.ObjectOld.GetGeneration())
    64  		},
    65  		CreateFunc: func(_ event.CreateEvent) bool {
    66  			return true
    67  		},
    68  		DeleteFunc: func(_ event.DeleteEvent) bool {
    69  			return false
    70  		},
    71  	}
    72  }
    73  
    74  // SetupWithManager sets up the controller with the Manager.
    75  func (r *TerminalReconciler) SetupWithManager(mgr ctrl.Manager) error {
    76  	return ctrl.NewControllerManagedBy(mgr).
    77  		For(&v1ien.IENode{}).
    78  		WithEventFilter(terminalReconcilerPredicate()).
    79  		Complete(r)
    80  }
    81  
    82  func (r *TerminalReconciler) PatchOpts() []patch.Option {
    83  	return []patch.Option{
    84  		patch.WithOwnedConditions{Conditions: r.Conditions.Owned},
    85  		patch.WithFieldOwner(r.Name),
    86  	}
    87  }
    88  
    89  // +kubebuilder:rbac:groups=dsds.edge.ncr.com,resources=ienodes,verbs=create;get;list;update;patch;watch
    90  // +kubebuilder:rbac:groups=dsds.edge.ncr.com,resources=ienodes/status,verbs=get;update;patch
    91  // +kubebuilder:rbac:groups="core.cnrm.cloud.google.com",resources=configconnectors,verbs=create;update;patch;watch
    92  // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
    93  // +kubebuilder:rbac:groups="iam.cnrm.cloud.google.com",resources="iampartialpolicies",verbs=create;delete;get;list;update;patch
    94  // +kubebuilder:rbac:groups="pubsub.cnrm.cloud.google.com",resources="pubsubsubscriptions",verbs=create;delete;get;list;update;patch
    95  // +kubebuilder:rbac:groups=edge.ncr.com,resources=syncedobjects,verbs=create;get;watch;list;update;patch;delete
    96  // +kubebuilder:rbac:groups=edge.ncr.com,resources=clusters,verbs=get;watch;list
    97  
    98  // Reconcile is part of the main kubernetes reconciliation loop which aims to
    99  // move the current state of the cluster closer to the desired state.
   100  //
   101  //nolint:gocyclo
   102  func (r *TerminalReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, recErr error) {
   103  	reconcileStart := time.Now()
   104  	log := ctrl.LoggerFrom(ctx)
   105  	r.setResourceManager()
   106  
   107  	var result = reconcile.ResultEmpty
   108  
   109  	terminal := &v1ien.IENode{}
   110  	if err := r.Client.Get(ctx, req.NamespacedName, terminal); client.IgnoreNotFound(err) != nil {
   111  		log.Error(err, "failed to get Terminal")
   112  		return ctrl.Result{}, client.IgnoreNotFound(err)
   113  	}
   114  
   115  	patcher := patch.NewSerialPatcher(terminal, r.Client)
   116  
   117  	defer func() {
   118  		summarizer := reconcile.NewSummarizer(patcher)
   119  		res, recErr = summarizer.SummarizeAndPatch(ctx, terminal, []reconcile.SummarizeOption{
   120  			reconcile.WithConditions(r.Conditions),
   121  			reconcile.WithResult(result),
   122  			reconcile.WithError(recErr),
   123  			reconcile.WithIgnoreNotFound(),
   124  			reconcile.WithProcessors(
   125  				reconcile.RecordReconcileReq,
   126  				reconcile.RecordResult,
   127  			),
   128  			reconcile.WithFieldOwner(r.Name),
   129  		}...)
   130  
   131  		r.Metrics.RecordDuration(ctx, terminal, reconcileStart)
   132  		r.Metrics.RecordReconciling(ctx, terminal)
   133  		r.Metrics.RecordReadiness(ctx, terminal)
   134  	}()
   135  
   136  	var oldStatus v1ien.IENodeStatus
   137  	if terminal.Status != nil {
   138  		oldStatus = *terminal.Status.DeepCopy()
   139  	}
   140  	terminal = terminal.DeepCopy()
   141  
   142  	log = log.WithValues("name", terminal.ObjectMeta.Name, "spec", terminal.Spec)
   143  
   144  	ctx = logr.NewContext(ctx, log)
   145  	log.Info("reconciling started for terminal")
   146  
   147  	// Check if finalizer exists
   148  	if !controllerutil.ContainsFinalizer(terminal, v1ien.Finalizer) {
   149  		controllerutil.AddFinalizer(terminal, v1ien.Finalizer)
   150  
   151  		// Return immediately so that we requeue and reconcile object with finalizer
   152  		// added.
   153  		result = reconcile.ResultRequeue
   154  		return
   155  	}
   156  
   157  	// examine if the object is under deletion
   158  	if !terminal.ObjectMeta.DeletionTimestamp.IsZero() {
   159  		if err := r.deleteTerminal(ctx, terminal); err != nil {
   160  			recErr = err
   161  			return
   162  		}
   163  		return
   164  	}
   165  
   166  	if err := reconcile.Progressing(ctx, terminal, patcher, r.PatchOpts()...); err != nil {
   167  		recErr = err
   168  		return
   169  	}
   170  
   171  	// Reset Inventory to compare and prune old inventory after plugins recreate it
   172  	terminal.Status.Inventory = inventory.New()
   173  
   174  	err := plugins.Execute(ctx, r.ResourceManager, terminal)
   175  	if err != nil {
   176  		log.Error(err, "plugin error")
   177  		recErr = recerr.New(err, string(v1ien.PluginFailed))
   178  		return
   179  	}
   180  
   181  	if err := r.pruneInventory(ctx, oldStatus, terminal); err != nil {
   182  		log.Error(err, "error pruning inventory")
   183  		recErr = err
   184  		return
   185  	}
   186  
   187  	log.Info("reconciling finished")
   188  
   189  	conditions.MarkTrue(terminal, status.ReadyCondition, string(v1ien.Successful), "terminal reconciled successfully")
   190  	result = reconcile.ResultSuccess
   191  	return
   192  }
   193  
   194  func (r *TerminalReconciler) deleteTerminal(ctx context.Context, terminal *v1ien.IENode) error {
   195  	log := logr.FromContextOrDiscard(ctx)
   196  
   197  	err := plugins.ExecuteFinalizers(ctx, r.Client, terminal)
   198  	if err != nil {
   199  		log.Error(err, ErrPluginFinalizerFailed)
   200  		conditions.MarkFalse(terminal, status.ReadyCondition, string(v1ien.PluginFailed), ErrPluginFinalizerFailed)
   201  		return err
   202  	}
   203  
   204  	if terminal.Status != nil && terminal.Status.Inventory != nil {
   205  		inv, err := inventory.ListObjects(terminal.Status.Inventory)
   206  		if err != nil {
   207  			log.Error(err, "failed to get objects to cleanup from inventory", "terminal", terminal.Name)
   208  			return err
   209  		}
   210  		_, err = r.ResourceManager.DeleteAll(ctx, inv, ssa.DefaultDeleteOptions())
   211  		if err != nil {
   212  			log.Error(err, "failed to cleanup objects from inventory", "terminal", terminal.Name)
   213  			return err
   214  		}
   215  	}
   216  	controllerutil.RemoveFinalizer(terminal, v1ien.Finalizer)
   217  	log.Info("finalizer executed")
   218  	return nil
   219  }
   220  
   221  // Removes from the cluster all objects in the oldstatus inventory that are not
   222  // present in the new inventory in terminal
   223  func (r *TerminalReconciler) pruneInventory(ctx context.Context, oldStatus v1ien.IENodeStatus, terminal *v1ien.IENode) error {
   224  	log := logr.FromContextOrDiscard(ctx)
   225  
   226  	if oldStatus.Inventory == nil {
   227  		return nil
   228  	}
   229  
   230  	diff, err := inventory.Diff(oldStatus.Inventory, terminal.GetInventory())
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	if len(diff) == 0 {
   236  		return nil
   237  	}
   238  
   239  	changeSet, err := r.ResourceManager.DeleteAll(ctx, diff, ssa.DefaultDeleteOptions())
   240  	if err != nil {
   241  		return err
   242  	}
   243  	log.Info("pruned objects", "changeset", changeSet)
   244  	return nil
   245  }
   246  
   247  func (r *TerminalReconciler) setResourceManager() {
   248  	if r.ResourceManager == nil {
   249  		mgr := ssa.NewResourceManager(
   250  			r.Client,
   251  			// be sure to consistently communicate this controllers ownership of objects
   252  			// this should match the result of createOpts()
   253  			polling.NewStatusPoller(r.Client, r.Client.RESTMapper(), polling.Options{}), ssa.Owner{Field: r.Name},
   254  		)
   255  		r.ResourceManager = mgr
   256  	}
   257  }
   258  

View as plain text