Upgrading
Before you upgrade
Section titled “Before you upgrade”Back up the App resources. Apps are the only state the pack owns; every Deployment, Service, ConfigMap and NebariApp is re-created from them.
kubectl get apps -A -o yaml > apps-backup-$(date +%F).yamlInline sources live inside the App, so the backup restores them too. Git sources re-clone
on restore. Data on a PersistentVolumeClaim (pvc sources) is not in the App: back up the
claim with your usual volume tooling.
Read the CHANGELOG for the versions you are skipping; behavior changes are listed there.
Upgrade with Helm
Section titled “Upgrade with Helm”helm upgrade installs a CRD only on first install and never updates it, so apply the
new chart’s CRD first, then upgrade:
VERSION=<X.Y.Z>helm show crds oci://ghcr.io/nebari-dev/apps-pack/charts/nebari-apps --version "$VERSION" \ | kubectl apply --server-side --force-conflicts -f -helm upgrade nebari-apps oci://ghcr.io/nebari-dev/apps-pack/charts/nebari-apps \ --version "$VERSION" --namespace nebari-apps --reuse-values--force-conflicts takes the CRD’s fields over from the field manager that installed it
(Helm or Argo CD). Skipping the CRD step leaves the old schema in place: the API server then silently drops
any new App field, and validation the new version relies on is missing.
Upgrade the chart and images together. A released chart pins all five images to the same build, and newer operator or API images can depend on RBAC that only the matching chart grants.
Upgrade with Argo CD
Section titled “Upgrade with Argo CD”Bump targetRevision in the Application (see
examples/argocd-application.yaml).
Argo CD renders the chart’s crds/ directory with the rest of the chart, so the CRD is
updated on sync. Back up the Apps first all the same.
Check the result
Section titled “Check the result”kubectl -n nebari-apps rollout status deploy/nebari-apps-operator deploy/nebari-apps-apikubectl get apps -A # every App should return to Runningcurl -s https://apps.<cluster-domain>/api/v1/versionThe UI’s Settings page (account menu → Settings) shows that version and build next to the UI’s own: the UI’s version is built into its bundle and its build is the UI image tag the chart rendered. During a rollout the two can differ for a minute, and the page says so; if they still differ afterwards, one of the Deployments did not roll.
Roll back
Section titled “Roll back”helm history nebari-apps -n nebari-appshelm rollback nebari-apps <REVISION> -n nebari-appshelm rollback restores the workloads, RBAC and values, but not the CRD. CRD changes
within v1alpha1 are additive (versioning policy,
checked in CI), so the newer CRD keeps working with the older operator. The exception is
group-only access, which removed access.users: rolling
back does not restore the lists the newer CRD pruned.
If Apps were lost, re-apply the backup:
kubectl apply -f apps-backup-<date>.yamlStatus fields in the backup are ignored; the operator recomputes them.
Version notes
Section titled “Version notes”Upgrading to group-only access
Section titled “Upgrading to group-only access”Breaking: Apps restrict who may open them by Keycloak group only. spec.access.users
(the per-user allow list) is removed from the CRD, the API, the MCP launch_app tool, the
UI and the apps-authz sidecar (AUTHZ_USERS, AUTHZ_USERS_CLAIM).
spec.access.managers.users, the users who may manage an App, is unchanged.
-
An App that restricted only by users becomes open to every signed-in user. The new CRD’s structural schema prunes
access.usersfrom stored Apps, so an App that listed users and no groups is left with neither list: the operator drops its authz sidecar and any user who can sign in to the realm gets in. An App that listed both keeps its groups, and the people it let in only by username are refused (403). -
Before upgrading, find the Apps that list users:
Terminal window kubectl get apps.apps.nebari.dev -A -o json | jq -r '.items[] | select(.spec.access.users // [] | length > 0) | "\(.metadata.namespace)/\(.metadata.name)"'For each one, put those users in a Keycloak group (an existing one, or a new group per App) and add the group to the App’s
access.groupsbefore you apply the new CRD. Group membership reaches users’ tokens on their next refresh (5 minutes by default). Take the backup after this step too: a rollback does not bring the pruned lists back, the backup does. -
Clients that still send
access.usersget a422from the API (and from the upload route’s manifest) instead of an App that is silently open to more people. Update scripts,nebari-app.yamlmanifests and GitOps manifests to use groups; for GitOps andkubectlthe apiserver prunes the field without an error, so check those by hand.
Upgrading to container images
Section titled “Upgrading to container images”- Re-apply the CRD first. It adds
source.type: imageandsource.image, plus CEL rules that requiresource.imagefor that type and keep its port off the sidecar’s 8080 and 8082. Existing Apps are unaffected. - Off unless you turn it on.
apps.image.allowedRegistriesdefaults to empty, which refuses every image App (UI, API, MCP andkubectlalike). Set it, andapps.image.pullSecretsfor private registries, to enable the source; see Image registries. - No RBAC changes: the operator only sets
imagePullSecretsby name on the pods it already creates.
Upgrading to App secrets
Section titled “Upgrading to App secrets”-
Re-apply the CRD first. It adds
runtime.secretKeys, capsruntime.envat 256 entries, and adds a CEL rule that refusesruntime.env[].valueFrom.secretKeyRef. -
Apps that read a Secret through
runtime.envstop being served. Before upgrading, find them:Terminal window kubectl get apps -A -o json | jq -r '.items[] | select(any(.spec.runtime.env[]?;.valueFrom.secretKeyRef or (.valueFrom.configMapKeyRef.name // "" | test("^app-.*-(content|server)$"))))| "\(.metadata.namespace)/\(.metadata.name)"'Such a reference let anyone who could write an App read any Secret in its namespace, so the upgraded operator marks these Apps
Failed(Validated=False, reasonForbiddenEnvReference, plus a Warning event) and stops updating their Deployment. Pods already running keep their old spec until the App is fixed, so fix them promptly. Move each credential into the App’s own Secret,app-<name>-secrets, list its key inruntime.secretKeys, and drop thevalueFromentry. AconfigMapKeyRefto another App’s generated ConfigMap is refused the same way; references to your own ConfigMaps are fine. Stored Apps stay editable as long as an edit leavesruntimealone; aruntimeedit (fromkubectlor the UI) must remove the reference. -
The API’s ServiceAccount can create and patch Secrets in app namespaces (never read them), to store App secrets (details).
Upgrading to scale to zero
Section titled “Upgrading to scale to zero”- Re-apply the CRD first. It adds
runtime.idleTimeoutMinutes,status.lastActivityTimeand theIdlephase; an operator running against the old CRD cannot reportIdle. - Off unless you turn it on.
apps.idle.timeoutMinutesdefaults to0, so nothing idles until you set it or an App setsruntime.idleTimeoutMinutes. Turning it on rolls the pods of every App that can idle once: they gain the authz sidecar, and Python tasks there move to port 8081 ($PORT). Setruntime.keepAlive: truefirst on Apps that must stay up. - Gateway API
ReferenceGrant. Idle Apps route to the activator Service in the release namespace through aReferenceGrantthe operator creates there; the operator gains read-only access to pods and write access toreferencegrantsin its own namespace (details).
0.1.0 → 0.2.0
Section titled “0.1.0 → 0.2.0”- Namespaces first. The API gets a RoleBinding in each namespace in
api.allowedNamespaces(defaultapps); each must exist before the upgrade. - PVC sources need an opt-in. A
pvcApp goesFaileduntil its claim is labeledapps.nebari.dev/mountable=true. - Subdomains are unique and immutable. If two Apps already share a subdomain, the
older keeps it and the other goes
Failed(SubdomainTaken). access.groupsandaccess.usersare enforced. A private App that lists them gets the apps-authz sidecar and returns403to everyone else, so check who your existing Apps list. Python tasks of restricted Apps must listen on$PORT(8081 for them). The operator needs the chart’skeycloak.url(orkeycloak.internalUrl), or such Apps fail validation;authz.imageships with the chart.- App pods get default requests and limits (
apps.defaultResources) and at most 10 replicas. - Auth is stricter. Tokens must come from the pack’s own Keycloak clients, and
api.auth.enabled=falseis refused on TLS installs. - MCP:
remove_appneedsconfirm=true;get_app_logsandlist_appsreturn a different shape. - Releases are cut from a release PR and published to the org Helm repository and GHCR; see CI/CD & releasing.