github.com/cilium/statedb@v0.3.2/reconciler/types.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package reconciler
     5  
     6  import (
     7  	"cmp"
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"iter"
    12  	"log/slog"
    13  	"slices"
    14  	"strings"
    15  	"sync/atomic"
    16  	"time"
    17  
    18  	"github.com/cilium/hive/cell"
    19  	"github.com/cilium/hive/job"
    20  	"github.com/cilium/statedb"
    21  	"github.com/cilium/statedb/index"
    22  	"github.com/cilium/statedb/internal"
    23  	"gopkg.in/yaml.v3"
    24  )
    25  
    26  type Reconciler[Obj any] interface {
    27  	// Prune triggers an immediate pruning regardless of [PruneInterval].
    28  	// Implemented as a select+send to a channel of size 1, so N concurrent
    29  	// calls of this method may result in less than N full reconciliations.
    30  	// This still requires the table to be fully initialized to have an effect.
    31  	//
    32  	// Primarily useful in tests, but may be of use when there's knowledge
    33  	// that something has gone wrong in the reconciliation target and full
    34  	// reconciliation is needed to recover.
    35  	Prune()
    36  }
    37  
    38  // Params are the reconciler dependencies that are independent of the
    39  // use-case.
    40  type Params struct {
    41  	cell.In
    42  
    43  	Lifecycle      cell.Lifecycle
    44  	Log            *slog.Logger
    45  	DB             *statedb.DB
    46  	Jobs           job.Registry
    47  	ModuleID       cell.FullModuleID
    48  	Health         cell.Health
    49  	DefaultMetrics Metrics `optional:"true"`
    50  }
    51  
    52  // Operations defines how to reconcile an object.
    53  //
    54  // Each operation is given a context that limits the lifetime of the operation
    55  // and a ReadTxn to allow looking up referenced state.
    56  type Operations[Obj any] interface {
    57  	// Update the object in the target. If the operation is long-running it should
    58  	// abort if context is cancelled. Should return an error if the operation fails.
    59  	// The reconciler will retry the operation again at a later time, potentially
    60  	// with a new version of the object. The operation should thus be idempotent.
    61  	//
    62  	// Update is used both for incremental and full reconciliation. Incremental
    63  	// reconciliation is performed when the desired state is updated. A full
    64  	// reconciliation is done periodically by calling 'Update' on all objects.
    65  	//
    66  	// The object handed to Update is a clone produced by Config.CloneObject
    67  	// and thus Update can mutate the object. The mutations are only guaranteed
    68  	// to be retained if the object has a single reconciler (one Status).
    69  	Update(ctx context.Context, txn statedb.ReadTxn, obj Obj) error
    70  
    71  	// Delete the object in the target. Same semantics as with Update.
    72  	// Deleting a non-existing object is not an error and returns nil.
    73  	Delete(context.Context, statedb.ReadTxn, Obj) error
    74  
    75  	// Prune undesired state. It is given an iterator for the full set of
    76  	// desired objects. The implementation should diff the desired state against
    77  	// the realized state to find things to prune.
    78  	// Invoked during full reconciliation before the individual objects are Update()'d.
    79  	//
    80  	// Unlike failed Update()'s a failed Prune() operation is not retried until
    81  	// the next full reconciliation round.
    82  	Prune(ctx context.Context, txn statedb.ReadTxn, objects iter.Seq2[Obj, statedb.Revision]) error
    83  }
    84  
    85  type BatchEntry[Obj any] struct {
    86  	Object   Obj
    87  	Revision statedb.Revision
    88  	Result   error
    89  
    90  	original Obj
    91  }
    92  
    93  type BatchOperations[Obj any] interface {
    94  	UpdateBatch(ctx context.Context, txn statedb.ReadTxn, batch []BatchEntry[Obj])
    95  	DeleteBatch(context.Context, statedb.ReadTxn, []BatchEntry[Obj])
    96  }
    97  
    98  type StatusKind string
    99  
   100  const (
   101  	StatusKindPending    StatusKind = "Pending"
   102  	StatusKindRefreshing StatusKind = "Refreshing"
   103  	StatusKindDone       StatusKind = "Done"
   104  	StatusKindError      StatusKind = "Error"
   105  )
   106  
   107  var (
   108  	pendingKey    = index.Key("P")
   109  	refreshingKey = index.Key("R")
   110  	doneKey       = index.Key("D")
   111  	errorKey      = index.Key("E")
   112  )
   113  
   114  // Key implements an optimized construction of index.Key for StatusKind
   115  // to avoid copying and allocation.
   116  func (s StatusKind) Key() index.Key {
   117  	switch s {
   118  	case StatusKindPending:
   119  		return pendingKey
   120  	case StatusKindRefreshing:
   121  		return refreshingKey
   122  	case StatusKindDone:
   123  		return doneKey
   124  	case StatusKindError:
   125  		return errorKey
   126  	}
   127  	panic("BUG: unmatched StatusKind")
   128  }
   129  
   130  // Status is embedded into the reconcilable object. It allows
   131  // inspecting per-object reconciliation status and waiting for
   132  // the reconciler. Object may have multiple reconcilers and
   133  // multiple reconciliation statuses.
   134  type Status struct {
   135  	Kind      StatusKind
   136  	UpdatedAt time.Time
   137  	Error     string
   138  
   139  	// id is a unique identifier for a pending object.
   140  	// The reconciler uses this to compare whether the object
   141  	// has really changed when committing the resulting status.
   142  	// This allows multiple reconcilers to exist for a single
   143  	// object without repeating work when status is updated.
   144  	id uint64
   145  }
   146  
   147  // statusJSON defines the JSON/YAML format for [Status]. Separate to
   148  // [Status] to allow custom unmarshalling that fills in [id].
   149  type statusJSON struct {
   150  	Kind      string    `json:"kind" yaml:"kind"`
   151  	UpdatedAt time.Time `json:"updated-at" yaml:"updated-at"`
   152  	Error     string    `json:"error,omitempty" yaml:"error,omitempty"`
   153  }
   154  
   155  func (sj *statusJSON) fill(s *Status) {
   156  	s.Kind = StatusKind(sj.Kind)
   157  	s.UpdatedAt = sj.UpdatedAt
   158  	s.Error = sj.Error
   159  	s.id = nextID()
   160  }
   161  
   162  func (s *Status) UnmarshalYAML(value *yaml.Node) error {
   163  	var sj statusJSON
   164  	if err := value.Decode(&sj); err != nil {
   165  		return err
   166  	}
   167  	sj.fill(s)
   168  	return nil
   169  }
   170  
   171  func (s *Status) UnmarshalJSON(data []byte) error {
   172  	var sj statusJSON
   173  	if err := json.Unmarshal(data, &sj); err != nil {
   174  		return err
   175  	}
   176  	sj.fill(s)
   177  	return nil
   178  }
   179  
   180  func (s Status) IsPendingOrRefreshing() bool {
   181  	return s.Kind == StatusKindPending || s.Kind == StatusKindRefreshing
   182  }
   183  
   184  func (s Status) String() string {
   185  	if s.Kind == StatusKindError {
   186  		return fmt.Sprintf("Error: %s (%s ago)", s.Error, internal.PrettySince(s.UpdatedAt))
   187  	}
   188  	return fmt.Sprintf("%s (%s ago)", s.Kind, internal.PrettySince(s.UpdatedAt))
   189  }
   190  
   191  var idGen atomic.Uint64
   192  
   193  func nextID() uint64 {
   194  	return idGen.Add(1)
   195  }
   196  
   197  // StatusPending constructs the status for marking the object as
   198  // requiring reconciliation. The reconciler will perform the
   199  // Update operation and on success transition to Done status, or
   200  // on failure to Error status.
   201  func StatusPending() Status {
   202  	return Status{
   203  		Kind:      StatusKindPending,
   204  		UpdatedAt: time.Now(),
   205  		Error:     "",
   206  		id:        nextID(),
   207  	}
   208  }
   209  
   210  // StatusRefreshing constructs the status for marking the object as
   211  // requiring refreshing. The reconciler will perform the
   212  // Update operation and on success transition to Done status, or
   213  // on failure to Error status.
   214  //
   215  // This is distinct from the Pending status in order to give a hint
   216  // to the Update operation that this is a refresh of the object and
   217  // should be forced.
   218  func StatusRefreshing() Status {
   219  	return Status{
   220  		Kind:      StatusKindRefreshing,
   221  		UpdatedAt: time.Now(),
   222  		Error:     "",
   223  		id:        nextID(),
   224  	}
   225  }
   226  
   227  // StatusDone constructs the status that marks the object as
   228  // reconciled.
   229  func StatusDone() Status {
   230  	return Status{
   231  		Kind:      StatusKindDone,
   232  		UpdatedAt: time.Now(),
   233  		Error:     "",
   234  		id:        nextID(),
   235  	}
   236  }
   237  
   238  // statusError constructs the status that marks the object
   239  // as failed to be reconciled.
   240  func StatusError(err error) Status {
   241  	return Status{
   242  		Kind:      StatusKindError,
   243  		UpdatedAt: time.Now(),
   244  		Error:     err.Error(),
   245  		id:        nextID(),
   246  	}
   247  }
   248  
   249  // StatusSet is a set of named statuses. This allows for the use of
   250  // multiple reconcilers per object when the reconcilers are not known
   251  // up front.
   252  type StatusSet struct {
   253  	id        uint64
   254  	createdAt time.Time
   255  	statuses  []namedStatus
   256  }
   257  
   258  type namedStatus struct {
   259  	Status
   260  	name string
   261  }
   262  
   263  func NewStatusSet() StatusSet {
   264  	return StatusSet{
   265  		id:        nextID(),
   266  		createdAt: time.Now(),
   267  		statuses:  nil,
   268  	}
   269  }
   270  
   271  // Pending returns a new pending status set.
   272  // The names of reconcilers are reused to be able to show which
   273  // are still pending.
   274  func (s StatusSet) Pending() StatusSet {
   275  	// Generate a new id. This lets an individual reconciler
   276  	// differentiate between the status changing in an object
   277  	// versus the data itself, which is needed when the reconciler
   278  	// writes back the reconciliation status and the object has
   279  	// changed.
   280  	s.id = nextID()
   281  	s.createdAt = time.Now()
   282  
   283  	s.statuses = slices.Clone(s.statuses)
   284  	for i := range s.statuses {
   285  		s.statuses[i].Kind = StatusKindPending
   286  		s.statuses[i].id = s.id
   287  	}
   288  	return s
   289  }
   290  
   291  func (s StatusSet) String() string {
   292  	if len(s.statuses) == 0 {
   293  		return "Pending"
   294  	}
   295  
   296  	var updatedAt time.Time
   297  	done := []string{}
   298  	pending := []string{}
   299  	errored := []string{}
   300  
   301  	for _, status := range s.statuses {
   302  		if status.UpdatedAt.After(updatedAt) {
   303  			updatedAt = status.UpdatedAt
   304  		}
   305  		switch status.Kind {
   306  		case StatusKindDone:
   307  			done = append(done, status.name)
   308  		case StatusKindError:
   309  			errored = append(errored, status.name+" ("+status.Error+")")
   310  		default:
   311  			pending = append(pending, status.name)
   312  		}
   313  	}
   314  	var b strings.Builder
   315  	if len(errored) > 0 {
   316  		b.WriteString("Errored: ")
   317  		b.WriteString(strings.Join(errored, " "))
   318  	}
   319  	if len(pending) > 0 {
   320  		if b.Len() > 0 {
   321  			b.WriteString(", ")
   322  		}
   323  		b.WriteString("Pending: ")
   324  		b.WriteString(strings.Join(pending, " "))
   325  	}
   326  	if len(done) > 0 {
   327  		if b.Len() > 0 {
   328  			b.WriteString(", ")
   329  		}
   330  		b.WriteString("Done: ")
   331  		b.WriteString(strings.Join(done, " "))
   332  	}
   333  	b.WriteString(" (")
   334  	b.WriteString(internal.PrettySince(updatedAt))
   335  	b.WriteString(" ago)")
   336  	return b.String()
   337  }
   338  
   339  // Set the reconcilation status of the named reconciler.
   340  // Use this to implement 'SetObjectStatus' for your reconciler.
   341  func (s StatusSet) Set(name string, status Status) StatusSet {
   342  	idx := slices.IndexFunc(
   343  		s.statuses,
   344  		func(st namedStatus) bool { return st.name == name })
   345  
   346  	s.statuses = slices.Clone(s.statuses)
   347  	if idx >= 0 {
   348  		s.statuses[idx] = namedStatus{status, name}
   349  	} else {
   350  		s.statuses = append(s.statuses, namedStatus{status, name})
   351  		slices.SortFunc(s.statuses,
   352  			func(a, b namedStatus) int { return cmp.Compare(a.name, b.name) })
   353  	}
   354  	return s
   355  }
   356  
   357  // Get returns the status for the named reconciler.
   358  // Use this to implement 'GetObjectStatus' for your reconciler.
   359  // If this reconciler is new the status is pending.
   360  func (s StatusSet) Get(name string) Status {
   361  	idx := slices.IndexFunc(
   362  		s.statuses,
   363  		func(st namedStatus) bool { return st.name == name })
   364  	if idx < 0 {
   365  		return Status{
   366  			Kind:      StatusKindPending,
   367  			UpdatedAt: s.createdAt,
   368  			id:        s.id,
   369  		}
   370  	}
   371  	return s.statuses[idx].Status
   372  }
   373  
   374  func (s StatusSet) All() map[string]Status {
   375  	m := make(map[string]Status, len(s.statuses))
   376  	for _, ns := range s.statuses {
   377  		m[ns.name] = ns.Status
   378  	}
   379  	return m
   380  }
   381  
   382  func (s *StatusSet) UnmarshalJSON(data []byte) error {
   383  	m := map[string]Status{}
   384  	if err := json.Unmarshal(data, &m); err != nil {
   385  		return err
   386  	}
   387  	s.statuses = make([]namedStatus, 0, len(m))
   388  	for name, status := range m {
   389  		s.statuses = append(s.statuses, namedStatus{status, name})
   390  	}
   391  	slices.SortFunc(s.statuses,
   392  		func(a, b namedStatus) int { return cmp.Compare(a.name, b.name) })
   393  	return nil
   394  }
   395  
   396  // MarshalJSON marshals the StatusSet as a map[string]Status.
   397  // It carries enough information over to be able to implement String()
   398  // so this can be used to implement the TableRow() method.
   399  func (s StatusSet) MarshalJSON() ([]byte, error) {
   400  	return json.Marshal(s.All())
   401  }