Skip to content

LinearB On-Prem Agent v5 Upgrade Guide

This guide covers all configuration changes required when upgrading the LinearB On-Prem Agent from v4 to v5.

Before upgrading, back up your local-values.yaml and run a helm diff or Argo CD dry-run to preview the changes against your current release.


Breaking changes — action required before upgrading

1. Scheduler worker subcharts removed

v5 moves to native Kubernetes Jobs for processing work. The three Celery-based worker subcharts are removed.

Remove these keys from your local-values.yaml:

# Delete any overrides for these — the subcharts no longer exist
scheduler-worker: ...
scheduler-pm-worker: ...
scheduler-sensors-worker: ...

If your values file has resource tuning (WORKER_REQUEST_CPU, WORKER_LIMIT_MEMORY, etc.) under any of these keys, remove those blocks entirely. Worker pod resource tuning in v5 is done through global.workerPods.

2. Renamed keys

Old key (v4) New key (v5)
global.versions.pods_cleaner global.versions.job_lifecycle_manager
minio.DeploymentUpdate minio.deploymentUpdate

Update your local-values.yaml wherever these appear.

3. Deprecated concurrency keys

The global job concurrency limit is replaced by per-queue limits.

Remove:

global:
  MAX_CONCURRENT_JOBS: "10"    # remove
  OPS_RESERVED_SLOTS: "2"      # remove

Add:

global:
  MAX_GIT_JOBS: "5"       # concurrent git (code analysis) jobs
  MAX_PM_JOBS: "1"        # concurrent PM (Jira/Linear/etc.) jobs
  MAX_SENSORS_JOBS: "5"   # concurrent sensors jobs

The defaults shown above match v4 behaviour for most installations. Ops jobs bypass concurrency limits in both versions.

4. Datadog chart API changes

If you have Datadog enabled, remove the following keys — they no longer exist in the upgraded Datadog subchart:

datadog-agent:
  datadog:
    kubeStateMetricsEnabled: true          # remove
    containerExcludeLogs: "image:..."      # remove
  kubeStateMetrics:
    image: ...                             # remove entire block

You must also explicitly disable the Datadog Operator to prevent it from being installed on upgrade:

datadog-agent:
  datadog:
    operator:
      enabled: false

MinIO update strategy migration (Argo CD with ServerSideApply only)

If you use plain helm upgrade, skip this section.

See README-upgrade-to-v4.md for background. The same issue applies on the v4 → v5 upgrade path when using Argo CD with ServerSideApply. Enable the migrator once on the upgrading release:

infra:
  minio:
    strategyMigrator:
      enabled: true

Disable it again after the upgrade completes.


New features

Per-queue job tuning

You can tune the job lifecycle behaviour per environment. All of the following have working defaults and only need to be set if you want to change them:

global:
  # How long before a stuck job is forcibly terminated (seconds)
  JOB_ACTIVE_DEADLINE_SECONDS: "3600"

  # How long completed / failed job pods are kept before cleanup
  JOB_TTL_SUCCESS_SECONDS: "60"
  JOB_TTL_FAILED_SECONDS: "21600"

  # Retry behaviour
  MAX_JLM_RETRY_ATTEMPTS: "5"
  SCHEDULER_REQUEUE_DELAY: "600"
  MAX_SCHEDULER_RETRY_ATTEMPTS: "10"

Argo CD namespace RBAC

If you deploy via Argo CD with the application controller running in a separate namespace, the chart can create the necessary RoleBinding for you:

global:
  argocdNamespaceRbac:
    enabled: true
    controllerServiceAccountName: "argocd-application-controller"
    controllerNamespace: "argocd"
    clusterRoleName: "admin"

Default is false. Leave disabled if you manage RBAC out-of-band.

OpenShift + Argo CD (openshift-gitops): The GitOps service account typically does not have escalation rights to bind admin. Keep this disabled and grant RBAC to the GitOps SA via your cluster's infrastructure tooling instead.

Secret management via External Secrets Operator (ESO)

v5 adds support for supplying secrets through the External Secrets Operator. Set global.infra.commonSecret.mode to choose how the chart-secrets Kubernetes Secret is populated:

Mode Description
helm Default. Helm renders the Secret from your values file.
externalSecret ESO syncs the Secret from a ClusterSecretStore. Requires ESO installed in your cluster.
existing You pre-create the Secret outside the chart (e.g. Terraform, Vault agent).

ESO example — sync from AWS Secrets Manager:

global:
  infra:
    commonSecret:
      mode: externalSecret
    externalSecrets:
      refreshInterval: "1h"
      secretStoreRef:
        name: my-cluster-secret-store   # name of your ClusterSecretStore
        kind: ClusterSecretStore
      envName: prod
      data:
        - secretKey: LINEARB_PUBLIC_API_KEY
          remoteRef:
            property: LINEARB_PUBLIC_API_KEY
        - secretKey: DD_API_KEY
          remoteRef:
            property: DD_API_KEY
        - secretKey: api-key
          remoteRef:
            property: DD_API_KEY
        - secretKey: JFROG_KEY
          remoteRef:
            property: JFROG_KEY
        - secretKey: JFROG_USER
          remoteRef:
            property: JFROG_USER

The image pull secret (regcred) can also be managed by ESO:

infra:
  createRegcred: false
  useExternalRegcred: true
  regcredExternalSecret:
    enabled: true

Custom root CA — ESO mode

If you supply a custom root certificate and want it sourced from a secret store rather than embedded in your values file:

global:
  CUSTOM_ROOT_CERTIFICATES: true
  customCa:
    secretMode: eso
    externalSecret:
      refreshInterval: 1h
      secretStoreRef:
        name: my-cluster-secret-store
        kind: ClusterSecretStore
      remoteRef:
        key: my-secret-id
        property: CUSTOM_SSL_CERT

The secret store property must be populated before the release is applied. If the ExternalSecret remains in Pending state, the CA bundle init container will fail to start.


OpenShift: security context requirements

OpenShift's restricted-v2 SCC requires pods to run with UIDs within your namespace's allocated range. v5 retains the same security context requirements as v4.

Find your namespace UID range:

oc get ns <your-namespace> -o jsonpath='{.metadata.annotations.openshift\.io/sa\.scc\.uid-range}{"\n"}'
# example: 1000900000/10000  →  use 1000900000 as your base UID

Set the global OpenShift UID

LinearB's own workloads pick up security context automatically when K8S_PLATFORM: openshift is set. Configure the namespace UID once globally:

global:
  K8S_PLATFORM: openshift
  openshiftRunAsUser: 1000900000   # replace with your namespace UID range low bound
  openshiftRunAsGroup: 1000900000

Bitnami subchart overrides (MinIO, RabbitMQ, Redis)

The MinIO, RabbitMQ, and Redis subcharts do not read the global OpenShift UID and must be overridden explicitly. Replace 1000900000 throughout with your namespace's UID range low bound.

minio:
  persistence:
    subPath: minio-export
  securityContext:
    enabled: true
    runAsNonRoot: true
    runAsUser: 1000900000
    runAsGroup: 1000900000
    fsGroup: 1000900000
    fsGroupChangePolicy: "Always"
  containerSecurityContext:
    runAsNonRoot: true
    runAsUser: 1000900000
    runAsGroup: 1000900000
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]
    seccompProfile:
      type: RuntimeDefault
  postJob:
    securityContext:
      enabled: true
      runAsNonRoot: true
      runAsUser: 1000900000
      runAsGroup: 1000900000
      fsGroup: 1000900000
  makeBucketJob:
    securityContext:
      enabled: true
    containerSecurityContext:
      runAsNonRoot: true
      runAsUser: 1000900000
      runAsGroup: 1000900000
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]
      seccompProfile:
        type: RuntimeDefault
  makeUserJob:
    securityContext:
      enabled: true
    containerSecurityContext:
      runAsNonRoot: true
      runAsUser: 1000900000
      runAsGroup: 1000900000
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]
      seccompProfile:
        type: RuntimeDefault

rabbitmq:
  podSecurityContext:
    runAsNonRoot: true
    runAsUser: 1000900000
    runAsGroup: 1000900000
    fsGroup: 1000900000
    fsGroupChangePolicy: Always
  containerSecurityContext:
    runAsNonRoot: true
    runAsUser: 1000900000
    runAsGroup: 1000900000
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]
    seccompProfile:
      type: RuntimeDefault

redis:
  master:
    podSecurityContext:
      runAsNonRoot: true
      runAsUser: 1000900000
      runAsGroup: 1000900000
      fsGroup: 1000900000
      fsGroupChangePolicy: Always
    containerSecurityContext:
      runAsNonRoot: true
      runAsUser: 1000900000
      runAsGroup: 1000900000
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]
      seccompProfile:
        type: RuntimeDefault

fsGroupChangePolicy: Always on MinIO and RabbitMQ forces kubelet to sync PVC ownership on every mount. This prevents startup failures ("Unable to write to the backend") if the UID changed between deployments.

minio.persistence.subPath: minio-export mounts MinIO's data into a subdirectory of the PVC so new data is created under the current UID. Without this, data written before a UID change may be inaccessible after upgrade.

Supplemental group for analysis workloads

The sensors, onprem-receiver, jobs-suite-worker, and jobs-suite-dispatcher containers write to paths that require membership in group 0. Add the following to your OpenShift values layer:

sensors:
  podSecurityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
    supplementalGroups: [0]

onprem-receiver:
  podSecurityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
    supplementalGroups: [0]

jobs-suite-worker:
  podSecurityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
    supplementalGroups: [0]

jobs-suite-dispatcher:
  podSecurityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
    supplementalGroups: [0]

agent-api runtime flags

agent-api:
  image:
    env:
      plain:
        - name: "PORT"
          value: "8080"
        - name: "GUNICORN_CMD_ARGS"
          value: "--no-control-socket --worker-tmp-dir /tmp"

Forward-proxy DNS resolver

OpenShift uses a different internal DNS service than standard Kubernetes. Set the resolver so the forward proxy can resolve upstream hostnames:

forward-proxy:
  resolver: "dns-default.openshift-dns.svc.cluster.local"

OpenShift Route for webhook receiver

On OpenShift the webhook receiver is exposed via an OpenShift Route rather than an Ingress. Configure it under infra:

infra:
  openshift:
    onpremReceiverRoute:
      annotations:
        haproxy.router.openshift.io/timeout: 120s
      # host: ""  # optional — omit to use the cluster-assigned hostname

Ensure global.RECEIVER_INGRESS: false and ingress.installController: false are set so the bundled ingress-nginx controller is not installed.


Upgrade steps

  1. Update your local-values.yaml with the breaking changes listed above.
  2. Pull the latest chart:
    helm repo update
    
  3. Run a dry-run to preview changes:
    helm diff upgrade <release-name> linearb/on-prem-agent \
      --namespace <namespace> \
      -f local-values.yaml
    
  4. Apply the upgrade:
    helm upgrade <release-name> linearb/on-prem-agent \
      --namespace <namespace> \
      -f local-values.yaml \
      --wait
    
  5. Verify all pods come up:
    kubectl get pods -n <namespace>
    

For issues during or after the upgrade, refer to the Diagnostics Guide.