Skip to main content

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:

AspectAdmission webhookThe job calls the API
What you add to the manifestFour annotationsAn init container and a release step
Cluster prerequisiteThe webhook installed, and the namespace labeledNone beyond LicOps itself
A denied requestThe pod is rejected when it is createdYours to handle: wait, fail, or retry
StatusPre-releaseAvailable
SuitsMany licensed workloads, with cluster-admin accessA 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:

  1. Label the namespace the workloads run in:

    kubectl label namespace my-eda-workloads openlm.io/licensing=enabled
  2. 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"
AnnotationRequiredValue
openlm.io/needs-licensingYesExactly "true". Any other value and the pod is left alone.
openlm.io/vendorYesThe pool's vendor, exactly as GET /api/v1/stats shows it.
openlm.io/featuresYesThe 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-idNoA 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.

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:

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:

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.