# Automate Catalyst region upgrades

Diagrid works out which release a self-managed region should install next from the region's [release channel](https://docs.diagrid.io/operate/platform-operations/release-channels) and policy. You can read that decision in two places:

- **The Control Plane API** tells you whether an upgrade is waiting and which version it is. Call it with an API key from anywhere.
- **The `catalyst-release-availability` ConfigMap** lives in your cluster, and the Catalyst agent keeps it up to date. It also names the exact chart to install, pinned by digest.

:::important

Diagrid never applies an upgrade to a self-managed region. It does not run Helm, and it does not edit your Argo CD `Application` or anything else in your cluster. Your tooling reads the decision, and you choose when and how to apply it.

:::

## Read the decision from the API

Every region read includes the decision in `status.release`:

```bash
diagrid region get my-region --output json
```

The release policy endpoint returns the same decision together with the region's channel and mode. Authenticate with an [API key](https://docs.diagrid.io/operate/platform-operations/identity-and-access#service-accounts):

```bash
curl -s https://api.r1.diagrid.io/apis/cra.diagrid.io/v1beta2/regions/my-region/releasesubscription \
  -H "x-diagrid-api-key: ${DIAGRID_API_KEY}"
```

```json
{
  "region": "my-region",
  "component": "catalyst-dataplane",
  "channel": "stable",
  "mode": "follow",
  "decisionRequired": false,
  "status": {
    "upgradeAvailable": true,
    "reason": "NEXT_HOP_SELECTED",
    "currentVersion": "1.133.0",
    "nextVersion": "1.134.0",
    "destinationVersion": "1.135.0",
    "channelHeadVersion": "1.135.0"
  }
}
```

| Field | Meaning |
| --- | --- |
| `upgradeAvailable` | `true` when there is a release to move to now. |
| `reason` | Why the region can or cannot move. See [Reason codes](#reason-codes). |
| `currentVersion` | The release Diagrid observed the region running. Empty when the region is older than 1.115.0. |
| `nextVersion` | The one release to install now. It can be an intermediate step on the way to `destinationVersion`. Empty when the region must not move. |
| `destinationVersion` | The release the region is heading for, which can be several upgrades away. Install `nextVersion` instead. |
| `channelHeadVersion` | The newest release on the region's channel. |
| `frozenUntil` | When a freeze window lifts. Set only with `FREEZE_WINDOW_ACTIVE`. |

The same endpoint accepts a `PUT` to change the channel or pause upgrades, which is mode `hold`. The request sets `channel` and `mode` together, so read the policy first and send the channel back unchanged when you only want to pause upgrades. See [Get a region's release policy](https://docs.diagrid.io/api/control-plane/get-region-release-subscription) and [Set a region's release policy](https://docs.diagrid.io/api/control-plane/put-region-release-subscription).

### Reason codes

`NEXT_HOP_SELECTED` means a release is waiting. Every other code means "do nothing now":

| Code | Meaning |
| --- | --- |
| `ALREADY_AT_DESTINATION` | The region runs the newest release it can move to. |
| `FREEZE_WINDOW_ACTIVE` | A freeze window applies. `frozenUntil` says when it lifts. |
| `SUBSCRIPTION_HELD`, `SUBSCRIPTION_PINNED` | Paused upgrades or a pin keep the region where it is. |
| `RELEASE_BLOCKED` | Diagrid has stopped rollouts of that release. |
| `CHANNEL_HEAD_UNKNOWN` | Diagrid has not recorded a release for the channel yet. |
| `CURRENT_RELEASE_UNKNOWN` | Diagrid cannot tell what the region runs. Upgrade the region to 1.115.0 or later. |
| `NO_SUPPORTED_UPGRADE_PATH`, `AGENT_VERSION_TOO_OLD`, `NOT_ENTITLED`, `RELEASE_RETIRED`, `PINNED_RELEASE_UNRESOLVED`, `DOWNGRADE_NOT_ALLOWED` | These do not clear on their own. Contact [Diagrid support](mailto:support@diagrid.io). |

## Read the decision in your cluster

The API does not say where to install a release from. For that, turn on the `catalyst-release-availability` ConfigMap. The Catalyst agent asks Diagrid for the decision every five minutes and writes it into the `cra-agent` namespace, where your tooling reads it with ordinary Kubernetes access.

### Turn on the ConfigMap

The ConfigMap requires a Catalyst region version 1.117.0 or later. Add these values to your `catalyst-values.yaml`:

```yaml
agent:
  config:
    release_availability:
      enabled: true
      endpoint: https://catalyst-releases.r1.diagrid.io
      # Optional. Seconds between refreshes. The default is 300.
      interval_in_sec: 300
      # Optional. Grants read access to the ConfigMap.
      readers:
        - name: catalyst-upgrade   # the ServiceAccount your upgrade job runs as
          namespace: ci
```

Set both `enabled` and `endpoint`. With `enabled` alone the agent logs a warning and writes no ConfigMap. The agent calls the endpoint over mutual TLS, so allow outbound HTTPS to `catalyst-releases.r1.diagrid.io` without TLS inspection. See [Outbound network connections](https://docs.diagrid.io/operate/hosting/enterprise-self-hosted/system-architecture#outbound-network-connections).

Each entry in `readers` gets a `Role` that grants `get` on this one ConfigMap, and a `RoleBinding` to that ServiceAccount. To manage RBAC yourself, leave `readers` unset. Grant `get` with `resourceNames: ["catalyst-release-availability"]`, and do not grant `list` or `watch`. Kubernetes cannot limit those to one name, so they would expose every ConfigMap in `cra-agent`.

Upgrade the region as usual. The ConfigMap appears within a minute of the agent restarting.

### What the ConfigMap contains

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: catalyst-release-availability
  namespace: cra-agent
data:
  state: current
  reason: NEXT_HOP_SELECTED
  component: catalyst-dataplane
  channel: stable
  mode: follow
  nextVersion: 1.134.0
  nextReleaseDigest: sha256:65d4…
  nextChartReference: oci://public.ecr.aws/diagrid/catalyst@sha256:b048…
  nextChartRepository: oci://public.ecr.aws/diagrid/catalyst
  nextChartDigest: sha256:b048…
  currentVersion: 1.133.0
  currentReleaseDigest: sha256:2c80…
  issuedAt: "2026-10-07T09:00:00Z"
  expiresAt: "2026-10-07T09:15:00Z"
  lastRefreshAt: "2026-10-07T09:01:00Z"
  refreshError: ""
  answer.json: |
    { … }
```

Every key except `answer.json` is a single-line string, so `kubectl` with `jsonpath` reads any of them:

```bash
kubectl get configmap catalyst-release-availability -n cra-agent \
  -o jsonpath='{.data.nextVersion}'
```

| Key | Meaning |
| --- | --- |
| `state` | Read this first. See the table below. |
| `reason` | The [reason code](#reason-codes), as in the API. |
| `nextVersion` | The one release to install now. Empty when the region must not move. |
| `nextChartReference` | The chart to install, pinned by digest. `nextChartRepository` and `nextChartDigest` give the same thing in parts. |
| `currentVersion` | The release Diagrid observed the region running. |
| `issuedAt`, `expiresAt` | When Diagrid made the decision, and when it stops being valid. A decision is valid for about 15 minutes. |
| `lastRefreshAt`, `refreshError` | When the agent last asked, and why that failed. `refreshError` is empty after a success. |
| `answer.json` | The full decision as Diagrid sent it, including `frozenUntil`. |

| `state` | Meaning | What to do |
| --- | --- | --- |
| `current` | The decision had not expired when the agent wrote it. | Act on it. |
| `expired` | The decision is past `expiresAt`, because the agent could not reach Diagrid for a while. | Nothing. Check `refreshError`. |
| `unavailable` | The agent has never received a decision. | Nothing. Check `refreshError`. |

When a refresh fails, the agent keeps the last decision and records the failure in `refreshError`. It never deletes the ConfigMap or writes half a decision. Setting `enabled` back to `false` stops the refreshes but leaves the ConfigMap in place, so delete it if nothing should read it.

### Rules for your tooling

- **Read it on a schedule.** `nextVersion` is one step. After you install it, the next refresh can offer the next one.
- **Check `expiresAt` yourself.** `state` is only as fresh as the agent's last refresh. If the agent stops, nothing marks the decision as expired.
- **Install by digest.** A version tag can be moved to another chart, but a digest cannot.
- **Do nothing when there is no `nextVersion`.** Never fall back to the newest chart in the registry. It skips every block, freeze window and tested upgrade path.

## Example: upgrade with Argo CD

This example keeps the Catalyst version in Git. A scheduled job reads the ConfigMap and opens a pull request. When you merge it, Argo CD rolls out the new release.

**Diagram: Upgrading a self-managed region with Argo CD.** Diagrid publishes the decision into your cluster. Your job turns it into a pull request, and Argo CD applies it once you merge.

- **Diagrid Cloud**: Control plane
- **Your cluster**: Catalyst agent, ConfigMap (cra-agent)
- Argo CD, Git repository, Upgrade job (your schedule)

- Control plane → Catalyst agent (mTLS)
- Catalyst agent → ConfigMap (cra-agent)
- ConfigMap (cra-agent) → Upgrade job (your schedule) (reads)
- Upgrade job (your schedule) → Git repository (opens PR)
- Git repository → Argo CD (sync)
- Argo CD → Your cluster (async)

1. Register the Diagrid chart registry with Argo CD:

   ```yaml
   apiVersion: v1
   kind: Secret
   metadata:
     name: diagrid-charts
     namespace: argocd
     labels:
       argocd.argoproj.io/secret-type: repository
   stringData:
     type: helm
     name: diagrid
     url: public.ecr.aws/diagrid
     enableOCI: "true"
   ```

2. Create an `Application` that pins the region to an exact version, and commit it to the Git repository Argo CD watches, for example as `apps/catalyst.yaml`:

   ```yaml
   apiVersion: argoproj.io/v1alpha1
   kind: Application
   metadata:
     name: catalyst
     namespace: argocd
   spec:
     project: default
     source:
       repoURL: public.ecr.aws/diagrid
       chart: catalyst
       # Written by the upgrade job. Always an exact version, never a range.
       targetRevision: 1.133.0
       helm:
         releaseName: catalyst
         values: |
           # the contents of your catalyst-values.yaml
     destination:
       server: https://kubernetes.default.svc
       namespace: cra-agent
     syncPolicy:
       automated:
         prune: false
         selfHeal: true
   ```

3. Run an upgrade job on a schedule, such as every 15 minutes. It reads the ConfigMap and opens a pull request when a release is waiting. The job needs `get` on the ConfigMap (see `readers` in [Turn on the ConfigMap](#turn-on-the-configmap)), push access to your repository, and [`crane`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md), `yq` and `gh`:

   ```bash
   #!/usr/bin/env bash
   set -euo pipefail

   read_key() {
     kubectl get configmap catalyst-release-availability -n cra-agent \
       -o "jsonpath={.data.$1}"
   }

   STATE=$(read_key state)
   if [ "${STATE}" != "current" ]; then
     echo "no usable decision (state: ${STATE}): $(read_key refreshError)" >&2
     exit 1
   fi

   # The timestamps are RFC 3339 in UTC, so a string comparison is enough.
   if [[ ! "$(date -u +%Y-%m-%dT%H:%M:%SZ)" < "$(read_key expiresAt)" ]]; then
     echo "the published decision has expired" >&2
     exit 1
   fi

   VERSION=$(read_key nextVersion)
   if [ -z "${VERSION}" ]; then
     echo "no upgrade waiting: $(read_key reason)"
     exit 0
   fi

   # Argo CD installs by version, so check the tag still points at the chart Diagrid named.
   if [ "$(crane digest "public.ecr.aws/diagrid/catalyst:${VERSION}")" != "$(read_key nextChartDigest)" ]; then
     echo "the ${VERSION} tag no longer matches the published digest" >&2
     exit 1
   fi

   yq -i ".spec.source.targetRevision = \"${VERSION}\"" apps/catalyst.yaml
   git checkout -b "catalyst-${VERSION}"
   git commit -am "Upgrade Catalyst to ${VERSION}"
   git push origin "catalyst-${VERSION}"
   gh pr create --fill
   ```

   A run with nothing to do exits without touching the repository.

4. Review and merge the pull request. Argo CD syncs the `Application` and upgrades the region to the new version.

A region several releases behind gets one pull request at a time. After Argo CD applies one, the agent reports the new version and the job's next run opens a pull request for the next step.

If your job cannot reach the cluster, read `status.nextVersion` from the [API](#read-the-decision-from-the-api) instead. The API does not return the chart digest, so you lose the digest check.

## Next steps

- [Release channels](https://docs.diagrid.io/operate/platform-operations/release-channels) to choose a channel or pause upgrades.
- [Lifecycle management](https://docs.diagrid.io/operate/hosting/enterprise-self-hosted/upgrades-operations) for manual `helm upgrade` and rollback.
- [Helm chart reference](https://docs.diagrid.io/operate/hosting/enterprise-self-hosted/reference) for the other chart values.
