convoy docs
Search

Pin and history

How convoy stores applied renders, keeps a history of them, and holds a Release on one render.

The render store is an object store that keeps the renders that convoy applied. A render is the set of objects that convoy made from the source, after the mutations. Its renderRef is a SHA-256 hash of these objects. With a render store, you can read the history of a Release, pin a past render, and roll back.

Configure the render store#

Set the flag --render-store-url on convoy-controller-manager and on convoy-observer. The value is a gocloud blob URL. The scheme selects the backend:

SchemeBackendExample
s3://Amazon S3, and stores that are compatible with S3s3://convoy-renders?endpoint=http://minio:9000&region=us-east-1&use_path_style=true
gs://Google Cloud Storagegs://convoy-renders
azblob://Azure Blob Storageazblob://convoy-renders
file://A directory, for example on a PersistentVolumeClaimfile:///var/lib/convoy/renders?create_dir=true&no_tmp_dir=true
mem://The memory of the process. Use it only for tests.mem://
  • Credentials follow the rules of each gocloud driver: the cloud credentials of the pod, or query parameters in the URL.
  • For file://, add no_tmp_dir=true when the directory is on a different file system than /tmp. Mount the same volume in the two pods.
  • An empty flag disables the render store.

What the store contains#

The store has two prefixes for each Release, under <namespace>/<name>/:

  • blobs/<renderRef>: the render, as a JSON array of objects. A blob is write-once. If a blob with the same renderRef exists, convoy does not write it again.
  • history/<time>-<renderRef>.json: one history entry for each applied render. The entry has renderRef, syncedAt and summary. The summary is the source URL or <kind>/<name>, then @ and the revision.

When you delete a Release, convoy deletes all keys of the Release before it clears the finalizer.

When convoy writes to the store#

During an apply, convoy does these steps:

  1. It runs the pre-apply dry run. A render that fails the dry run is not stored.
  2. It stores the blob before it applies an object. If this write fails, convoy applies nothing and sets Applied=False with reason DeployFailed. convoy retries.
  3. It applies the sync waves, prunes, and runs the hooks.
  4. After a full success, it adds a history entry, but only when the history has no entry for this renderRef. Then it runs the garbage collection.

As a result:

  • The history contains each render one time. syncedAt is the time of the first apply.
  • If the garbage collection deleted the entry of a render, a new apply of that render adds a new entry.
  • A render that waits for /sync or for a cluster wave is not stored.
  • A rollback by spec.failurePolicy adds no history entry and does not run the garbage collection.
  • If convoy cannot write a history entry, it logs the error. The apply stays successful.

Without a render store#

convoy renders and applies as usual. These functions do not work:

  • /history returns 503 Service Unavailable. status.resolvedRetentionPolicy.historyCount is 0.
  • /diff?render= returns an error.
  • A pin to a render that is not the current render fails with Applied=False, reason Pinned.
  • A rollback by spec.failurePolicy cannot apply the last healthy render.

A pin to the current render works without a store. But when the source changes, the pinned render is no longer the current render, so the pin fails with reason Pinned.

Retention#

spec.retentionPolicy.keepHistory sets the number of history entries that convoy keeps:

  • Not set: convoy keeps 10.
  • 0: convoy keeps all entries and all blobs. A negative value has the same effect. The API does not reject it.
  • A positive number N: convoy keeps the newest N entries.

status.resolvedRetentionPolicy shows the value that convoy uses (keepHistory) and the number of entries in the store now (historyCount). The controller-manager writes it at each reconcile.

After each successful apply, the garbage collection does these steps:

  1. It deletes the oldest history entries until N entries remain.
  2. It deletes each blob that no remaining entry refers to, unless the blob is protected.

These renders are protected:

  • the render that convoy just applied
  • status.lastSuccessful.renderRef and status.lastAttempted.renderRef
  • pin.renderRef
  • status.lastHealthy.renderRef

Thus a pin target and the rollback target stay in the store, also when their history entries are gone. Other old renders are deleted. You cannot pin them after that.

History and retention Pick a scenario and step through renders, pins and reconciles. Each event is one reconcile.
RenderHistory entryBlobWhy
R1deletedkeptstatus.lastHealthy refers to it
R2deleteddeletedNo entry and no protected field refers to it
R3keptkeptOne of the 2 newest entries
R4keptkeptThe newest entry, and the render that convoy just applied

Example with keepHistory: 2, after convoy applied R4.

Read the history#

sh
kubectl convoy release history podinfo
kubectl convoy release diff podinfo --render sha256:…
  • history shows the entries, newest first.
  • diff --render compares a stored render with the live objects. It shows what a rollback to that render changes.

See Release history and Release diff for the responses.

Pin a render#

A pin holds a Release on one render. You write it only through the /pin subresource:

sh
kubectl convoy release pin podinfo                      # pin the last applied render
kubectl convoy release pin podinfo --render-ref sha256:…
kubectl convoy release unpin podinfo

The apiserver rejects a pin before the first successful render. It does not check that the render exists in the store. It also sets the sync-requested-at annotation, so the controller reconciles the Release immediately.

At each reconcile, convoy renders the source and compares the result with pin.renderRef:

  • The same render: convoy applies it as usual.
  • A different render: convoy reads the pinned render from the store and applies it.
  • The pinned render is not in the store, or there is no store: convoy sets Applied=False with reason Pinned and applies nothing. It does not retry with backoff. It checks again at the next reconcile.

The pinned render goes through the usual apply: dry run, hooks, sync waves and prune. The observer also uses the pinned render as the desired state, if its store contains it.

In manual sync mode, a pin or an unpin also counts as a /sync request. convoy applies the pinned render, or after an unpin the current render, without a separate /sync.

Freeze or rollback#

status.pin.intent compares pin.renderRef with status.lastSuccessful.renderRef:

intentWhen
freezeThe pinned render is the last applied render.
rollbackThe pinned render is a different render.

When convoy applies the pinned render, status.lastSuccessful changes to that render. Then intent changes to freeze. Thus rollback shows only until the first apply of the pinned render, or while convoy cannot apply it.

Pin and rollout#

A pin overrides the progressive rollout. While a pin is set, convoy does not run the rollout state machine. See Progressive rollout.

In the code

convoy v0.9.0