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:
| Scheme | Backend | Example |
|---|---|---|
s3:// | Amazon S3, and stores that are compatible with S3 | s3://convoy-renders?endpoint=http://minio:9000®ion=us-east-1&use_path_style=true |
gs:// | Google Cloud Storage | gs://convoy-renders |
azblob:// | Azure Blob Storage | azblob://convoy-renders |
file:// | A directory, for example on a PersistentVolumeClaim | file:///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://, addno_tmp_dir=truewhen 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 samerenderRefexists, convoy does not write it again.history/<time>-<renderRef>.json: one history entry for each applied render. The entry hasrenderRef,syncedAtandsummary. 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:
- It runs the pre-apply dry run. A render that fails the dry run is not stored.
- It stores the blob before it applies an object. If this write fails, convoy applies nothing and sets
Applied=Falsewith reasonDeployFailed. convoy retries. - It applies the sync waves, prunes, and runs the hooks.
- 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.
syncedAtis 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
/syncor for a cluster wave is not stored. - A rollback by
spec.failurePolicyadds 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:
/historyreturns503 Service Unavailable.status.resolvedRetentionPolicy.historyCountis0./diff?render=returns an error.- A pin to a render that is not the current render fails with
Applied=False, reasonPinned. - A rollback by
spec.failurePolicycannot 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 newestNentries.
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:
- It deletes the oldest history entries until
Nentries remain. - 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.renderRefandstatus.lastAttempted.renderRefpin.renderRefstatus.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.
Read the history#
kubectl convoy release history podinfo
kubectl convoy release diff podinfo --render sha256:…historyshows the entries, newest first.diff --rendercompares 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:
kubectl convoy release pin podinfo # pin the last applied render
kubectl convoy release pin podinfo --render-ref sha256:…
kubectl convoy release unpin podinfoThe 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=Falsewith reasonPinnedand 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:
intent | When |
|---|---|
freeze | The pinned render is the last applied render. |
rollback | The 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
internal/blobstore.Openinternal/blobstore/gocloud.go:40internal/controller/release.(*Controller).persistRenderinternal/controller/release/controller.go:1775internal/controller/release.(*Controller).recordHistoryinternal/controller/release/controller.go:1800internal/controller/release.(*Controller).runGCinternal/controller/release/controller.go:1856internal/renderstore.(*RenderStore).GCinternal/renderstore/renderstore.go:213internal/controller/release.(*Controller).resolvePinnedRenderinternal/controller/release/controller.go:986internal/controller/release.pinStatusinternal/controller/release/controller.go:1713internal/observer.(*Controller).renderDesiredinternal/observer/observer.go:253