Rolling back a release
A rollback is an upgrade run from the older release’s own checkout. There is no rollback flag:
upgrade.sh refuses to run unless the sources it runs from are the exact commit --image-tag
names, so a move to N-1 from a checkout or a release bundle runs N-1’s copy of the script,
N-1’s chart and N-1’s CRDs against your install. Two of its three modes do that by Helm alone
and never run Terraform; the third re-applies N-1’s Terraform composition, Helm release
included. The supported rollback is the Helm-only pair below. The full mode is the GCP-level
revert, and it needs a plan read first.
Because the script that runs is N-1’s, its timeouts and Helm flags are N-1’s too, and this
page says where the published releases differ. A copy of the script that carries no baked version
— one taken from main rather than from release N-1 — fetches N-1’s tree and runs its own
steps over it, and it cannot complete a rollback to any release through 0.7.0: it calls
installer functions the libraries of 0.4.0 through 0.7.0 do not define, and 0.3.0 and
earlier have no installer library to load, so its operator step, --plan and full mode each stop
before they apply anything of N-1. Roll back from the N-1 checkout, which is what this page
describes.
The forward move, the three upgrade modes, and how a run resolves the version it targets are in Upgrade; this page covers going backwards only.
Before you start
Section titled “Before you start”Record what the install runs now, so you can tell afterwards what moved:
kubectl get deployment platform-agent-gateway -n kubeagents-system \ -o jsonpath='{.spec.template.spec.containers[?(@.name=="platform-agent")].image}{"\n"}'kubectl get deployment kube-agents-controller-manager -n kubeagents-system \ -o jsonpath='{.spec.template.spec.containers[*].image}{"\n"}'kubectl get deployment platform-agent-gateway -n kubeagents-system \ -o jsonpath='{range .spec.template.spec.initContainers[*]}{.image}{"\n"}{end}{range .spec.template.spec.volumes[*]}{.image.reference}{"\n"}{end}'helm history kube-agents -n kubeagents-systemThe third command lists the gateway’s init container and image volume images; the plugin ones are
the lines naming pubsub-platform or gke-stockout-investigator (a stage-<plugin> init
container or a plugin-<name> image volume, depending on the cluster). None means no plugin is
enabled.
Get N-1’s sources. Either a clean checkout of the tag or the release bundle passes the
source check:
git clone --branch <N-1> --depth 1 https://github.com/gke-labs/kube-agents.git kube-agents-<N-1>cd kube-agents-<N-1>or unpack kube-agents-<N-1>.tar.gz from
the release, whose scripts carry N-1 as
their baked version. A checkout at any other commit, or one with uncommitted changes, is refused
before anything is touched.
The script also refuses to run without the install’s configuration, because it re-renders the
PlatformAgent resource from it. Copy the install’s install.env into the new checkout, or point
KUBE_AGENTS_INSTALL_ENV at where it lives:
export KUBE_AGENTS_INSTALL_ENV=/path/to/your/install/install.envRead N’s release notes before you go. Everything they list is something N-1 does not know
about, and the next two sections say what happens to each kind of thing.
On an install running spec.mode: next, switch it to today first (Upgrade → Switching back to
today) and check that Slack answers. No
chart up to 0.9.0 declares platformAgent.mode, so a rollback to one cannot keep the mode: the
recorded key is a value N-1 does not declare, refused, dropped or ignored as The
rollback and When a rollback is refused say, and
without it the install is back on today with nothing in the run to say so. Switching first moves
Slack back to the credential broker, and the Slack app back to its today settings, with the checks
that section gives, instead of leaving you to find the app without its slash commands after the
rollback.
The rollback
Section titled “The rollback”From the N-1 checkout, a dry run, then operator and harness back to back:
./upgrade.sh --dry-run --upgrade-mode=operator --image-tag <N-1>./upgrade.sh --upgrade-mode=operator --image-tag <N-1>./upgrade.sh --upgrade-mode=harness --image-tag <N-1>Where N-1 is a release after 0.7.0, add --drop-undeclared-values to the operator and harness
commands. Without it, their script stops before any of N-1 is applied when the release records a
value N-1’s chart does not declare, and names each one; with it, the script drops and names them.
The scripts of 0.7.0 and earlier do not take the flag.
Where N-1’s script takes --allow-breaking-downgrade, it reads the commits between the tag the
install runs and N-1, and stops when one of them is marked as a breaking change: a type!:
subject or a BREAKING CHANGE: footer. Each mode reads the tag of what it moves: the operator mode
the operator’s, the harness mode the agent’s, and the full mode both. It names each such commit. Read what each one changed, then
add the flag to the operator and harness commands, or to a full-mode run, to roll back anyway. The
check is in the script that runs, which in a rollback is N-1’s, so a release from before the flag
rolls back without it.
--dry-run prints the target and the image references the step would apply, from the
configuration alone, and contacts nothing. It refuses a checkout at the wrong commit, as the real
run does, and only warns about uncommitted changes, which the real run refuses.
The operator step applies N-1’s CRDs to the cluster with a server-side apply, then runs one
helm upgrade on the existing release with N-1’s chart, re-tagging the operator image. N-1’s
operator then reconciles the PlatformAgent from N-1’s schema, and its first pass re-renders
the agent Deployment with N-1’s spec, rolling the pod wherever that spec differs, still on N’s
image, because the agent tag in the release’s values has not moved yet. The harness step is a
second helm upgrade with the same chart re-tagging the agent image (on releases that ship the
shell sandbox, the sandbox image with it, and from the first release after 0.6.0 the plugin
images the release records), followed by a wait for the rollout. Operator first
because the CRD schema and the controller have to agree before the agent the controller renders
is replaced. Between the two commands N’s agent image runs under N-1’s Deployment spec,
without whatever N’s operator had added to it, which is why the pair is run back to back rather
than a step at a time.
Each step’s helm upgrade waits up to ten minutes for the objects the chart renders, the
controller Deployment among them. A timeout there leaves the release failed, which the next run
does not un-stick on its own: the current script un-sticks a pending-* release alone, and
0.4.0’s un-sticks nothing. After Helm returns, the script first reads the gateway Deployment’s
release images back against the tag and fails the step on any still on N (from 0.5.0; the
current script reads plugin image volumes too), then waits with kubectl rollout status
for the rollouts the chart does not cover, the agent Deployment first, and those waits are
N-1’s: the current script gives the agent twenty-five minutes in the namespace install.env names,
while 0.4.0’s gives it two minutes in kubeagents-system whatever NAMESPACE says. So on a
slow image pull, or on an install in another namespace, 0.4.0’s harness step can report a
failure after both Helm moves have succeeded. A timeout is not a signal to run the step again.
Read the rollout yourself first:
kubectl get pods -n kubeagents-systemkubectl rollout status deployment/platform-agent-gateway -n kubeagents-systemkubectl describe pod -n kubeagents-system -l app=platform-agent-gatewayWhat the two steps change
Section titled “What the two steps change”-
The Helm release:
N-1’s chart version, two new revisions inhelm history. -
The operator and agent images, at tag
N-1; the sandbox image too whenN-1’s chart has it, and the plugin images whenN-1’s script is from after0.6.0.0.4.0,0.5.0and0.6.0all leave the plugin images onN’s tag. With a plugin enabled,0.5.0’s and0.6.0’s image check then refuses where the plugin is staged by an init container (GKE Autopilot, and Standard below 1.35), with both Helm moves already made; where it is mounted as an image volume the check does not read it and passes, and0.4.0has no check. In every case, finish that rollback from theN-1checkout by re-tagging the two plugin keys by hand (--reuse-valuesfor0.4.0, to match its script):Terminal window helm upgrade kube-agents charts/kube-agents -n kubeagents-system --reset-then-reuse-values \--set plugins.pubsubPlatform.image.tag=<N-1> \--set plugins.stockoutInvestigator.image.tag=<N-1>kubectl rollout status deployment/platform-agent-gateway -n kubeagents-system -
The CRD schema, now
N-1’s. -
Every object the chart renders, including the
PlatformAgentresource, re-rendered fromN-1’s templates. ObjectsN’s chart rendered andN-1’s does not are deleted by the upgrade; that is Helm’s ordinary behaviour. -
Which values those templates are rendered with depends on the script. From
0.5.0through0.7.0the re-tag ishelm upgrade --reset-then-reuse-values:N-1’s chart defaults, with the values the install set on top. From the first release after0.7.0it is the same, less any value the install set thatN-1’s values schema refuses as undeclared, which the script drops and names before any ofN-1is applied when it is given--drop-undeclared-values, and names and stops on when it is not.0.4.0and earlier use--reuse-values, which keeps every valueN’s release computed, defaults included, so a chart defaultNchanged stays atN’s value after a rollback to0.4.0even though the chart version reads0.4.0.
What they leave as it is
Section titled “What they leave as it is”Terraform state and every GCP resource. The Helm-only modes write nothing to state, so it keeps
recording N as the installed tag. What ./upgrade.sh --plan then reports depends on the copy it
runs from: the N-1 checkout’s script carries N-1 as its baked version, so it plans at N-1 and
the report holds the re-tag along with every composition difference between N-1 and N, which is
the next section’s subject. A copy carrying no baked version plans at the tag state records
instead, and the re-tag is not in that report. The state and the cluster disagree on the tag until
the next --upgrade-mode=full, which re-applies whatever tag it is given. The terraform.tfvars
in the N-1 checkout is regenerated on every run and is not a record of anything.
Secrets. The script never rewrites a Secret value that exists. A key that N-1’s script knows and
finds missing is generated and added, which is what a forward upgrade does too.
Objects N’s operator created that N-1’s operator does not know. The operator only reconciles
what its own release renders, so an object introduced by a later release stays in place, on N’s
image, unmanaged, until the next forward upgrade re-adopts it. The shell sandbox is the current
example: rolling back to a release whose chart has no agentSandbox values (0.4.0 and earlier)
leaves the platform-agent-shell StatefulSet behind, and that release’s harness step re-tags the
agent alone. It keeps running until its pod next restarts: the same Helm move deleted the
platform-agent-shell-authorized-keys Secret it mounts, because 0.4.0’s chart does not render
it, so the replacement pod stays in ContainerCreating. That is harmless to the 0.4.0 agent,
which has no sandbox, and the next forward upgrade renders the Secret again.
Fields N added to the PlatformAgent schema. Once N-1’s CRD is applied, the API server prunes
them from the stored object, and N-1’s chart does not render them. The full mode discards the
Helm values that produced them. A re-tag by the script of 0.7.0 or earlier keeps those values in
the release’s recorded values: a later forward re-tag renders them again, and the schema refusal
below turns on them. A re-tag by a script from after 0.7.0, run with --drop-undeclared-values,
drops each one N-1’s values schema refuses as undeclared, so after such a rollback the release no
longer records it, and the next forward re-tag renders it from N’s chart default until a
full-mode apply sets it again.
The agent’s persistent volume, apart from what the entrypoint re-syncs from the image. The
harness step rolls the pod, and the volume follows it; what the next start does to it is N-1’s
entrypoint’s doing, and every published release’s entrypoint does the same. Skills are not kept:
on every start the entrypoint replaces the platform profile’s skills/, and every cluster
profile’s, whole from the image, so a skill N added is gone at the first start after the
harness step rather than staying and failing on use
(Skills says the same of an injected
skill). The platform profile’s persona files, governance and hindsight configuration are
overwritten with N-1’s copies too, though a governance file only N shipped stays, because
that overlay prunes nothing; the default profile’s persona files and the shared scripts are
overwritten the same way. Cron definitions are merged per job id: N-1’s image wins every key
it ships, the scheduler’s own state stays, and a job only N shipped keeps its entry and keeps
firing, now against N-1’s tools. The rest of what the image seeds is copied with cp -ru,
which skips any file the volume holds that is newer, so those files stay at N’s copy, and the
state the agent itself wrote is kept.
A cluster profile’s remote MCP entries. Since the gke and developer_knowledge MCP servers went
through the credential proxy, the entrypoint rewrites those two entries in every cluster profile’s
config.yaml (and the platform profile’s, when it is the front door) to the proxy’s relay. An
earlier release’s entrypoint repairs only the User-Agent header in them, so after a rollback across
that change each Cluster Agent’s two servers name a relay address the older operator never sets,
and every call to them fails. Put the two entries back from the rolled-back image’s own templates,
keeping the rest of each file, from the gateway’s platform-agent container. The command covers
every cluster profile and, when the platform profile is the front door, that one too:
python3 - <<'PY'import glob, os, yamlhome = os.environ.get("PLATFORM_AGENT_HOME", "/opt/data")targets = [(path, "/opt/cluster-template/config.yaml") for path in glob.glob(f"{home}/profiles/cluster-*/config.yaml")]if os.environ.get("HERMES_GATEWAY_PROFILE") == "platform": targets.append((f"{home}/profiles/platform/config.yaml", "/opt/platform-template/config.yaml"))for path, template_path in targets: template = yaml.safe_load(open(template_path)) live = yaml.safe_load(open(path)) for name in ("gke", "developer_knowledge"): if name in template.get("mcp_servers", {}): live["mcp_servers"][name] = template["mcp_servers"][name] yaml.safe_dump(live, open(path + ".tmp", "w"), sort_keys=False) os.replace(path + ".tmp", path) print("restored", path)PYIt prints each file it restored; an install with Cluster Agents that prints none is looking in the wrong place.
Reverting GCP resources too
Section titled “Reverting GCP resources too”./upgrade.sh --plan --image-tag <N-1>./upgrade.sh --upgrade-mode=full --image-tag <N-1>From the N-1 checkout, the full mode applies N-1’s CRDs, then runs terraform apply on
N-1’s composition on top of the state N wrote. Its Helm half is not the re-tag: the
composition’s own helm_release upgrades the release with N-1’s chart and the values N-1’s
composition computes from install.env, and it sets neither reuse_values nor reset_values,
so the values the release recorded are discarded rather than reused. Anything in them that the
composition does not render goes with them: a value set by hand with helm upgrade --set, a key
N’s composition added, and on a 0.4.0 rollback the N defaults the pair’s --reuse-values
kept. The plan lists that as the helm_release change, next to the GCP resources. This is the
GCP-level revert: a resource N’s composition added is planned for destruction because N-1’s
composition does not declare it, and a setting N changed goes back. Read the whole plan first.
A plan that destroys a bucket, a KMS key or the cluster is a decision to take with what those
resources hold in front of you, not a rollback step; the composition on N may have added a
resource that now carries data. The plan’s exit code follows Terraform’s: 0 for no changes,
2 for changes, 1 for an error.
The composition refuses some of those destructions itself: its lifecycle.sh exits before
terraform apply when the regenerated configuration disagrees with state on the cluster, the
agent’s service account, the CMEK key, the release namespace, the Pub/Sub subscription or the
minter key. That refusal comes after the full mode’s CRD apply, so it leaves N-1’s CRDs in
place, as the schema refusal below does when N-1 is 0.7.0 or earlier. How to read a destroy line in the plan, as missing
configuration first and real drift second, is in the
installer README.
Checking the result
Section titled “Checking the result”kubectl get deployment platform-agent-gateway -n kubeagents-system \ -o jsonpath='{.spec.template.spec.containers[?(@.name=="platform-agent")].image}{"\n"}'kubectl get deployment kube-agents-controller-manager -n kubeagents-system \ -o jsonpath='{.spec.template.spec.containers[*].image}{"\n"}'kubectl get platformagent platform-agent -n kubeagents-system \ -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}{"\n"}'kubectl get deployment platform-agent-gateway -n kubeagents-system \ -o jsonpath='{range .spec.template.spec.initContainers[*]}{.image}{"\n"}{end}{range .spec.template.spec.volumes[*]}{.image.reference}{"\n"}{end}'kubectl get pods -n kubeagents-systemhelm history kube-agents -n kubeagents-systemBoth images end in :<N-1>, so does every plugin image the fourth command lists (the lines naming
pubsub-platform or gke-stockout-investigator; a plugin installed on its own, outside the
release, keeps its own tag and is not the rollback’s to move), the Ready condition reads True,
the gateway pod is Running,
and the newest Helm revision is deployed at chart version N-1, with the operator step’s
revision superseded just before it. kubeagents-system is the default namespace; an install
that set NAMESPACE in install.env uses that one. platform-agent is the chart’s default
platformAgent.name value.
When a rollback is refused
Section titled “When a rollback is refused”The first three refusals happen before anything on the cluster moves, and the fourth before any of
N-1 is applied. The last two land in the operator step after N-1’s CRDs are applied, except
that from the first release after 0.7.0 the script makes the schema check itself, before the CRD
apply. Helm checks before it renders or applies anything, so the release itself keeps its last
revision.
-
The sources do not match the tag. The checkout’s
HEADis not the tag’s commit, the tree has uncommitted changes, or the bundle’s baked version is not the--image-taggiven. Start again from a clean checkout or bundle ofN-1. -
No install configuration. No
install.envwas found inKUBE_AGENTS_INSTALL_ENV, the checkout the script runs from, the working directory, or (when run from outside a checkout)$HOME/kube-agents— orKUBE_AGENTS_INSTALL_ENVnames a file that is not there, which is reported by that path rather than searched past. Supply the install’s own file; a fresh one written from memory re-renders thePlatformAgentwith whatever it forgets. -
A commit between
NandN-1is marked as a breaking change. The script names each one, as a short SHA and its subject, afterkubectlhas been pointed at the cluster and before the Secret backfills. It reads the history from the checkout when that is a full (not shallow) clone with both commits, and otherwise from GitHub; when a rollback between two release tags cannot read it, the run also stops. Read the named commits, then re-run with--allow-breaking-downgrade.--planreports the same commits as a warning and does not stop. -
The memory store cannot be checked and the configuration does not name one. A rollback runs
upgrade.sh, and itsterraform.tfvarsis regenerated in every mode, so aninstall.envwith noMEMORYline makes the run ask the cluster whether it is running Hindsight rather than default to a value that would plan the store away. If the cluster cannot be asked, the run stops. This lands afterkubectlhas been pointed at the cluster and, on a real run, after the Secret backfills, but beforeN-1’s CRDs are applied. RecordMEMORY=hindsight|file|offininstall.env, or restore access to the cluster and re-run. See Upgrade. -
N-1’s chart carries a values schema andNadded a chart value. Every release after0.5.0ships avalues.schema.jsonthat closes each level of the chart’s values. Through0.7.0the re-tag reuses the values the release recorded with no way to drop one, so a keyN’s install set thatN-1’s chart does not declare fails Helm’s schema check. Helm checks before it renders, so the release keeps its last revision, but in the operator step the CRD apply has already run: putN’s CRDs back from theNcheckout with the same command the script uses,kubectl apply --server-side --force-conflicts -f charts/kube-agents/crds/. For a pair whoseN-1is0.7.0or earlier the Helm-only rollback therefore does not complete. From the first release after0.7.0the re-tag checks the recorded values against the schema before any ofN-1is applied, and names and stops on each key the schema refuses as undeclared. Given--drop-undeclared-valuesit drops those keys instead, so with the flag the refusal no longer happens whereN-1is that release or later. The full mode completes either way, because its Helm release renders from the composition’s values rather than the recorded ones (the section above), at the price of a GCP-level apply and the plan read that goes before it.Two pairs are refused this way, each on an install the newer release’s composition applied (a fresh install of it, or a full-mode upgrade to it).
0.7.0rolling back to0.6.0:0.7.0’s composition recordslitellm.maxTokenson every apply, and0.6.0’s chart does not declare it. The first release after0.7.0rolling back to0.7.0: its composition recordsplatformAgent.scopeon every apply, empty lists included, andplatformAgent.harness.driftDetectoron an install withenable_drift_pubsub, and0.7.0’s chart declares neither. Either stops at the operator step with a schema error naming the key:at '/litellm': additional properties 'maxTokens' not allowedfrom a current Helm,litellm: Additional property maxTokens is not allowedfrom an older Helm 3 release. Take either through the full mode from theN-1checkout:./upgrade.sh --plan --image-tag <N-1>, read the plan, then./upgrade.sh --upgrade-mode=full --image-tag <N-1>. -
N’s operator owns an object thatN-1’s chart renders. Helm refuses to adopt an object that carries another manager’s ownership labels (exists and cannot be imported into the current release: invalid ownership metadata). Thelitellm-policyNetworkPolicy is the case in hand: every chart through0.5.0renders it, and from the first release after0.5.0the operator creates and labels it instead, so a rollback from that release to0.5.0or earlier is refused at the operator step’shelm upgrade, withN-1’s CRDs already applied and the release still at its last revision. The way through is the chart README’s handoff: scaleN’s operator to zero, relabel and annotate the object for Helm, then run the two commands; the operator step adopts it. PutN’s CRDs back first if the refusal has already happened, as for the schema case.
A further refusal is a future one. The API server rejects a CRD whose spec.versions drops a
version still listed in status.storedVersions, so if N introduced a new API version of the
PlatformAgent CRD and objects were stored in it, the operator step fails at the CRD apply,
before anything moves. Every published release serves v1alpha1 alone; it is here so that the
day it happens the error is recognised as the rollback boundary it is.
Two things this page does not do. helm rollback kube-agents <revision> reverts the chart and
values to an earlier revision without applying that revision’s CRDs and without the source check,
which is why the runbook goes through upgrade.sh; upgrade.sh itself uses helm rollback only
to un-stick a release left in a pending-* state. And nothing here restores data: the agent’s
volume, the Terraform state bucket and anything N’s agent wrote to a repository or a cluster are
where N left them.