---
title: Kubernetes workloads
description: License Kubernetes pods in OpenLM Platform through OpenLM LicOps, with the admission webhook or by calling the admission API from the job.
product: OpenLM Platform
---

To make a Kubernetes workload wait for a license seat, either let the LicOps admission webhook handle annotated pods for you, or have the job call the admission API itself. Neither way changes your application image. The following table compares the two ways:

| Aspect | Admission webhook | The job calls the API |
| --- | --- | --- |
| What you add to the manifest | Four annotations | An init container and a release step |
| Cluster prerequisite | The webhook installed, and the namespace labeled | None beyond LicOps itself |
| A denied request | The pod is rejected when it is created | Yours to handle: wait, fail, or retry |
| Status | Pre-release | Available |
| Suits | Many licensed workloads, with cluster-admin access | A few workloads, or no permission to install webhooks |

## License pods with the admission webhook

Install the webhook first, as described in [Deploy on Kubernetes](https://www.openlm.com/documentation/cloud/deployment-operations/licops/deploy-kubernetes#install-the-admission-webhook). Then add both opt-ins:

1. Label the namespace the workloads run in:

   ```bash
   kubectl label namespace my-eda-workloads openlm.io/licensing=enabled
   ```

2. Annotate the pod template of each licensed workload:

   ```yaml
   spec:
     template:
       metadata:
         annotations:
           openlm.io/needs-licensing: "true"
           openlm.io/vendor: "MLM"
           openlm.io/features: "MATLAB:1"
           openlm.io/job-id: "nightly-regression-0042"
   ```

| Annotation | Required | Value |
| --- | --- | --- |
| `openlm.io/needs-licensing` | Yes | Exactly `"true"`. Any other value and the pod is left alone. |
| `openlm.io/vendor` | Yes | The pool's vendor, exactly as `GET /api/v1/stats` shows it. |
| `openlm.io/features` | Yes | The feature and seat count, as `name:count`, such as `"Innovus:1"`. Use one feature per pod: if you list several, only the first is licensed. |
| `openlm.io/job-id` | No | A job name to record with the pod's license use. |

When such a pod is created, the webhook asks LicOps for the seats. If LicOps denies them, the pod is rejected, and its controller, a Job for example, tries to create it again later. If LicOps admits them, the webhook adds init containers that dispatch the seat and redirect the pod's license traffic, and a sidecar that passes the checkouts to LicOps. The sidecar releases the seats when the pod ends and Kubernetes stops the sidecar with `SIGTERM`. If the sidecar ends without that signal, for example when its node fails or it runs out of memory, the seats stay held until the session times out.

In the pod's first container only, the webhook sets `LM_LICENSE_FILE` and the vendor's own variable, such as `MLM_LICENSE_FILE`, to point at the sidecar, and adds a `FEATURES` variable. An `LM_LICENSE_FILE` you set in the manifest is kept as the sidecar's fallback. Put the licensed application in the first container.

If LicOps cannot be reached within 1.5 seconds, the webhook lets the pod through unchanged: it checks out with its own license settings, outside LicOps. If LicOps answers with an error or denies the request, the pod is rejected. This behavior cannot be changed.

:::warning[A missing opt-in fails silently]

If the namespace label or the `needs-licensing` annotation is missing, the pod is created untouched, with no error, no event, and no warning. When licensing "does not work", check both first.

:::

To confirm that a pod was licensed, list its containers. A licensed pod has more than you wrote:

```bash
kubectl get pod <pod> -o jsonpath='{.spec.initContainers[*].name}{"\n"}{.spec.containers[*].name}{"\n"}'
kubectl get namespace my-eda-workloads --show-labels
```

## Call the API from the job

When the job calls the API itself, it needs three steps in the pod:

1. **An init container admits and dispatches.** It generates a session ID, asks for the seats, and then calls dispatch. It waits and asks again while LicOps answers `503` or denies for lack of capacity. It writes the session ID and the `licenseFile` that dispatch returned to a shared `emptyDir` volume.
2. **The work container uses that server.** Before it starts the application, it sets `LM_LICENSE_FILE` from the file the init container wrote.
3. **The work container releases the seat.** A `trap` on exit calls release with the session ID, however the application ends.

The following outline shows the shape. The commands of the init container are left out:

```yaml
initContainers:
  - name: openlm-admit
    image: curlimages/curl:8.10.1
    # Admit (retrying), then dispatch; write /openlm/session-id and /openlm/env.sh.
containers:
  - name: work
    image: your-registry/your-licensed-app:1.0
    env:
      - name: OL_URL
        value: "http://openlm-router-api.<namespace>.svc.cluster.local:8100"
    command: ["/bin/sh", "-c"]
    args:
      - |
        release() {
          curl -s --max-time 5 -X POST "$OL_URL/api/v1/admission-controller/release" \
            -H 'Content-Type: application/json' \
            -d "{\"sessionId\":\"$(cat /openlm/session-id)\"}" || true
        }
        trap release EXIT TERM INT
        . /openlm/env.sh
        your-application --your --flags
```

The integration kit includes a complete, commented Job, `kubernetes/job-direct-api.yaml`, built this way. Point its address at your LicOps Service, `http://openlm-router-api.<namespace>.svc.cluster.local:8100`, and use the Service address, never a pod IP or a `kubectl port-forward`.

Release from the work container's own trap, not from a separate sidecar that waits for the work to finish. A Job's pod is complete only when every container has exited, so a waiting sidecar keeps the Job running forever. On Kubernetes 1.29 or higher, a native sidecar, an init container with `restartPolicy: Always`, avoids that, and the sample Job carries that variant as a commented block.

A release that never arrives, because the node failed, for example, is not lost for good: LicOps reclaims the seat when the session times out.
