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 }