Skip to content

Update the Operator to work with an existing instance of LiteLLM already running #29

Description

@adamdolman

Problem

Support attaching a LiteLLMInstance to an existing, externally-managed deployment

Ask

An opt-out that lets a LiteLLMInstance describe a LiteLLM deployment the operator did not create, so the entity CRDs can be used against an existing installation without handing the operator the workload.

We have this working today by blocking workload reconciliation with RBAC. It works well enough to be worth supporting properly — and badly enough to be adopted as a proper feature!

Use case

A LiteLLM proxy already exists and is managed by something else — a Helm chart, a GitOps pipeline, an internal platform, whatever. What's wanted from the operator is the entity CRDs: LiteLLMTeam, LiteLLMVirtualKey, LiteLLMBudget, LiteLLMModel. Declarative teams and keys reconciled from Kubernetes beats scripting the admin API.

What isn't wanted is for the operator to own the Deployment, as it already exists.

Today, the entity CRDs are unreachable, even though nothing about them depends on the operator owning a Deployment.

Observation

The entity controllers' entire dependency on the instance is resolveInstance (internal/controller/common.go), which needs three things:

  • status.ready
  • status.endpoint
  • the master key from spec.masterKey.secretRef

Both status fields are derived from the instance name, not from anything the operator created:

// updateInstanceStatus
var dep appsv1.Deployment
if err := r.Get(ctx, types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, &dep); err == nil {
    instance.Status.Ready = dep.Status.ReadyReplicas > 0
}
instance.Status.Endpoint = fmt.Sprintf("%s://%s.%s.svc:%d", scheme, instance.Name, instance.Namespace, port)

The Get carries no ownership check and the endpoint is a formula rather than a lookup. So a LiteLLMInstance named after an existing Deployment and Service already resolves correctly.

The only thing in the way is that reconcileResources runs first and adopts those objects by name — reconcileDeployment and reconcileService both Get-then-overwrite rather than declining to touch a resource they didn't create.

What we validated

We have got this working with an existing deployment, by using RBAC to prevent the operator from being able to deploy the instance. We specified the CR with the existing deployment's name, port, key etc, and the workload reconciliation was blocked by RBAC, but other than logging the failure proceeded to then work fine:

  • status.ready: true and status.endpoint resolved against the existing deployment
  • the existing Deployment, Service and ConfigMap were byte-identical before and after — same containers, same generation, no new ReplicaSet, no pod restart
  • a LiteLLMTeam reached Synced with a real team ID from the admin API
  • deleting that CR propagated through to LiteLLM — recreating it produced a new ID
  • health probing still worked, since it only needs the endpoint

So the operator is entirely capable of this. It just has no way to be told to do it.
It also reports failure permanently: reconcileResources fails on every loop, so the instance sits Degraded and logs a Forbidden per resource forever. status.ready is true while the Ready condition is False with a ReconcileError. This doesn't stop it working, it just doesn't look nice, especially to someone unfamiliar with this workaround.

Proposed Solution

Proposal

apiVersion: litellm.palena.ai/v1alpha1
kind: LiteLLMInstance
metadata:
  name: litellm
spec:
  workload:
    managed: false        # default true — operator provisions the workload as today
  masterKey:
    secretRef: {name: litellm-master-key, key: LITELLM_MASTER_KEY}
  service:
    port: 80

When managed: false, skip workload reconciliation and keep everything else:

var reconcileErr error
if instance.Spec.Workload == nil || instance.Spec.Workload.Managed {
    reconcileErr = r.reconcileResources(ctx, &instance, labels, licenseSecretName, guardrails)
    r.reconcileAutoRollback(ctx, &instance)
}
r.updateInstanceStatus(ctx, &instance, reconcileErr)

updateInstanceStatus still populates ready and endpoint, health probing still runs, and config sync is a separate controller making API calls, so none of it is affected. Status would describe the instance honestly instead of reporting Degraded about resources it doesn't manage.

Worth considering alongside

Resolving the endpoint from the instance name works, but it makes correctness depend on a naming coincidence — the CR has to be named after a Deployment and Service it doesn't own. Extend to specify the endpoint fully:

spec:
  workload:
    managed: false
    endpoint: http://my-litellm.my-namespace.svc:80   # optional; defaults to today's formula
    readinessRef:                                      # optional; defaults to metadata.name
      kind: Deployment
      name: my-litellm

That would also allow attaching to a proxy that isn't a Deployment — a StatefulSet, something outside the cluster, or a Service fronting either.

Alternatives Considered

No response

Additional Context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions