Kubernetes workloads
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. Then add both opt-ins:
-
Label the namespace the workloads run in:
kubectl label namespace my-eda-workloads openlm.io/licensing=enabled -
Annotate the pod template of each licensed workload:
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.
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:
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:
- 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
503or denies for lack of capacity. It writes the session ID and thelicenseFilethat dispatch returned to a sharedemptyDirvolume. - The work container uses that server. Before it starts the application, it sets
LM_LICENSE_FILEfrom the file the init container wrote. - The work container releases the seat. A
trapon 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:
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.
The documentation team might draft this page using a large language model. A technical writer reviews every page and verifies it against the product before publishing it. A human makes the final decision.