convoy docs
Search

Status ownership

Which component writes each status field and each ReleaseInventory field, and the Server-Side Apply field manager that it uses.

Three loops in two processes write the status of a Release. Each field has one writer. Each writer uses Server-Side Apply (SSA) with its own field manager and sends only the fields that it owns. So one writer never overwrites the fields of another writer. The same rule applies to the ReleaseInventory, which has four writers.

Who writes what Select a line of the Release status or of the ReleaseInventory. The widget shows the field manager, the process and the function that writes it.
ObjectField managerFields
Release statusconvoy-releaseApplied, RolloutSucceeded, LifecycleSucceeded and the other status fields
Release statusconvoy-observeInSync
Release statusconvoy-healthHealthy, healthSummary
ReleaseInventoryconvoy-release-inventorythe shard label, the applied objects
ReleaseInventoryconvoy-release-hooksthe shard label, the hook entries
ReleaseInventoryconvoy-health-inventorythe health of each object
ReleaseInventoryconvoy-hook-statusthe state of each hook

Release status#

All three writers send an apply patch to releases/<name>/status with force: true.

Field managerProcess and loopConditionsOther fields
convoy-releasecontroller-manager, Release controllerApplied, RolloutSucceeded, LifecycleSucceededobservedGeneration, lastAttempted, lastSuccessful, lastHealthy, rollout, pin, waves, lifecycle, resolvedRetentionPolicy, reconciledAt, lastHandledSyncAt, lastHandledForceSyncAt
convoy-observeobserver, drift loopInSyncnone
convoy-healthobserver, health loopHealthyhealthSummary

status.conditions is a list map with the key type. A writer that sends only its own condition types does not change the other conditions. See Status and conditions.

How the controller builds its patch#

The Release controller copies the full status from the informer cache, then changes it. ownedStatusApplyConfig builds the patch field by field from this copy:

  • It sends only the three conditions that it owns. The copy also contains InSync and Healthy, but the patch does not contain them.
  • It does not serialize the full ReleaseStatus struct. Some fields of that struct, for example healthSummary, have no omitempty. A full serialization would send zero values for fields of other writers.
  • It sends pin, lifecycle and LifecycleSucceeded only when they have a value. When the field manager omits a field that it sent before, SSA clears the field. This is how an unpin clears status.pin.
  • It always sends resolvedRetentionPolicy.

status.waves[].healthy comes from the observer, but the controller writes it. The controller reads the per-object health from the ReleaseInventory and calculates the health of each wave. So status.waves has one writer.

When a writer skips the write#

  • The controller compares the new status with the cached status (statusChanged). It writes only when they are different. It sets reconciledAt only when it writes.
  • writeInSync and writeHealth skip the write when status, reason, message and observed generation of the condition do not change. writeHealth also compares healthSummary.
  • writeInSync and writeHealth keep lastTransitionTime while the condition status stays the same.

No Ready condition#

A Release has no Ready condition. A Ready condition would combine the signals of different writers into one field. Then that field would have more than one writer. The four signals stay separate:

  • Applied tells what the controller did.
  • InSync tells whether the live objects match the render.
  • Healthy tells whether the live objects pass the health rules.
  • RolloutSucceeded tells the result of the rollout.

A Release can have Applied=True, InSync=True and Healthy=False at the same time. See Release conditions.

ReleaseInventory#

The ReleaseInventory has no status subresource. The writers send apply patches to the object itself, with force: true. The inventory list is a list map with the keys group, kind, namespace and name. The hooks list is a list map with the keys name and phase. So two field managers can own different fields of the same entry. Each writer of an entry must send the keys of the entry. Thus the keys have two owners.

Field managerProcessOwns
convoy-release-inventorycontroller-managerThe shard label. In each inventory entry: the keys, version, wave, uid, renderRef
convoy-release-hookscontroller-managerThe shard label. In each hooks entry: the keys, weight, group, version, kind, workloadName, workloadNamespace, renderRef, createdAt, timeout
convoy-health-inventoryobserver, health loopIn each inventory entry: the keys, state, message, firstUnhealthy, assessedRenderRef
convoy-hook-statusobserver, health loopIn each hooks entry: the keys, state, message, finishedAt

The controller uses two field managers on purpose. Put sends inventory without hooks, and PutHooks sends hooks without inventory. With one field manager, each call would clear the list that the other call wrote.

The health loop sends every hook entry that has a state, not only the entries that it polled in this pass. If it omitted an entry, SSA would clear finishedAt from that entry. Then the hook would look unfinished again.

Put writes the full membership. The health loop reads the inventory from its cache before it writes. See ReleaseInventory.

Target clusters#

Field managerProcessOperation
convoycontroller-managerThe apply of each render object, with force: true. The pre-apply dry run. The create of a lifecycle hook workload.
convoyobserver, drift loopA server-side apply dry run of each desired object, with force: true. A dry run does not store field ownership.

The drift loop uses the same field manager as the apply. So the dry run shows exactly what a real apply would change. See Apply and prune and Drift.

Writes without SSA#

Some writes do not use SSA:

  • The Cluster, GitRepository, HelmRegistry and OCIRegistry controllers write status with UpdateStatus. They set no field manager. Each kind has one writer, so a field manager is not necessary.
  • The Release and Cluster controllers add and clear their finalizers with Update on the full object.
  • The apiserver sets the convoy.kumodo.io/sync-requested-at annotation on a /sync, /pin, /mutations or /lifecycle request. The /sync handler reads the Release and updates it in etcd.
  • The Release controller deletes the ReleaseInventory when it finalizes a Release.

Status writes do not start a reconcile#

All convoy loops use the harness in internal/controller/base. On an informer update event, shouldEnqueueOnUpdate enqueues the object only in these cases:

  1. The resource version did not change. This is a periodic resync.
  2. metadata.generation changed. The apiserver increments the generation of a Release only when spec changes.
  3. The Release controller only: the annotation convoy.kumodo.io/sync-requested-at changed.

A status write changes the resource version but not the generation. So a status write of one loop does not wake the same loop or the other loops on the Release informer.

Two event handlers in the controller-manager react to status on purpose:

  • A change of the Applied or Healthy condition status enqueues the Releases of a higher cluster wave on the same cluster. It does not enqueue the Release itself.
  • A change of state or assessedRenderRef in a ReleaseInventory entry enqueues the Release. A change in the number of entries also enqueues it. The controller ignores its own fields renderRef and wave.

The observer has a similar handler on the ReleaseInventory. See Reconcile flow and Health loop.

In the code

convoy v0.9.0