feat(instance): attach to an externally-managed LiteLLM deployment - #32
Open
davidgibbons wants to merge 1 commit into
Open
feat(instance): attach to an externally-managed LiteLLM deployment#32davidgibbons wants to merge 1 commit into
davidgibbons wants to merge 1 commit into
Conversation
…alenaAI#29) Adds `spec.workload.managed: false`, which stops the operator provisioning the proxy workload so the entity CRDs (LiteLLMTeam, LiteLLMVirtualKey, LiteLLMBudget, LiteLLMModel, ...) can be used against a proxy owned by a Helm chart, a GitOps pipeline or an internal platform. Nothing is created and nothing existing is adopted or mutated, replacing the RBAC-denial workaround that worked but left the instance permanently Degraded. Adds `spec.workload.endpoint` alongside it, so an unmanaged instance no longer has to be named after a Service it does not own and can attach to a proxy in another namespace or outside the cluster. A CEL rule rejects it when the workload is managed; the pattern requires a host, since the value becomes an outbound request URL. Readiness for an unmanaged instance now comes from the admin API answering at that endpoint rather than a name-matched Deployment, which also makes a StatefulSet-backed or off-cluster proxy work, and stops a Deployment that merely shares the CR's name from faking readiness. The Ready condition reports ProxyReachable / ProxyNotReachable and no PodsHealthy condition is set, because the operator owns no pods. status.version is not taken from spec.image.tag, which describes nothing the operator deployed and would print a fabricated "latest". It is filled from the litellm_version the proxy reports on /health/readiness, which LiteLLM includes only when its own general_settings sets allow_public_health_readiness_details: true — that endpoint is unauthenticated, so the master key does not unlock it. Absent that the field stays empty, which is honest: the operator does not know. spec.database.migration is ignored entirely. An externally-managed proxy owns its own schema: LiteLLM migrates on startup and whatever deployed it ships its own migration hook, so the operator would be a second migrator racing the real one. BuildMigrationJob also takes its image from spec.image.tag (defaulting to "latest"), so the Job would run prisma migrate deploy at an arbitrary schema version against a database the operator does not own. DatabaseReady reports WorkloadUnmanaged, and says the migration was ignored rather than skipping it silently when one was configured anyway. Health probing, config sync, license detection and finalizer-based cleanup of upstream entities are unaffected. Only workload reconciliation, auto-rollback and database migration are gated. The master-key-with-autogenerate-fallback block was duplicated in resolveInstance and probeInstanceHealth; it is now one masterKeyRef helper, which the new readiness probe also uses. Signed-off-by: David Gibbons <david@dgibbons.net>
davidgibbons
force-pushed
the
feat/unmanaged-workload
branch
from
September 5, 2026 16:45
0b95a2c to
05645fe
Compare
davidgibbons
marked this pull request as ready for review
September 5, 2026 17:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #29.
Adds
spec.workload.managed: falseso aLiteLLMInstancecan describe a LiteLLM proxy the operator did not deploy, making the entity CRDs usable against an existing installation without handing the operator the workload.This implements the issue's proposal, plus the
endpointhalf of "worth considering alongside".readinessRefis deliberately not implemented — see below.What changes
spec.workload.managed(*bool, defaultstrue). When false,reconcileResourcesandreconcileAutoRollbackare skipped entirely. Nothing is created, nothing existing is adopted or mutated, and noForbiddenis logged on every loop — the instance reports its actual state instead of sittingDegradedabout resources it doesn't manage.spec.workload.endpoint(optional, unmanaged only, CEL-enforced). Without it the endpoint stays today's formula, which requires the CR to be named after a Service it doesn't own. With it you can attach to a Service under another name, in another namespace, or to a proxy outside the cluster.Readiness for an unmanaged instance comes from the admin API answering at
status.endpoint(CheckLiveness, whichprobeInstanceHealthalready performs) rather than a name-matched Deployment. This is what makesendpointcoherent: with an explicit endpoint there may be no Deployment of that name to look at, and the proxy may be a StatefulSet or off-cluster. It also closes a hole in the RBAC workaround — a Deployment that merely shares the CR's name can no longer make an unreachable proxy reportready: true.updateInstanceStatuscomputes the endpoint before deriving readiness, since readiness now depends on it.Status differences when unmanaged
status.readystatus.endpointstatus.replicas/readyReplicas0status.versionspec.image.taglitellm_versionPodsHealthyconditionReadyreasonAllResourcesReady/DeploymentNotReadyProxyReachable/ProxyNotReachablestatus.versionis worth calling out.spec.image.tagdescribes nothing the operator deployed, so leaving it in place would print a fabricatedlatestin theVERSIONprint column.probeInstanceHealthalready fetches/health/readinessand discarded the payload, so it now readslitellm_versionoff it — but that field is only present when the proxy's owngeneral_settingssetsallow_public_health_readiness_details: true. The endpoint is unauthenticated, so the master key does not unlock it, and the default payload is{"status", "db"}. In the common casestatus.versionis therefore empty for an unmanaged instance. That is deliberate: empty means "the operator does not know", which is true, wherelatestwould be a guess. Both payload shapes are covered by tests. Say the word if you would rather it fell back to the image tag.Decisions worth a look
Managedis*bool, notbool. An envtest spec caught this: with a plainbooland noomitempty, a Go-typed client constructingWorkloadSpec{Endpoint: ...}marshals"managed": falseexplicitly, so the API-server default never fires and the caller silently gets an unmanaged instance. Nil now means managed, matchingenableServiceLinksand friends upstream. YAML users see no difference —workload: {}still defaults totrue.No
readinessRef. Probing the endpoint covers Deployment, StatefulSet and off-cluster uniformly, so kind-switching would add surface without adding capability. Happy to add it if you'd rather have the Kubernetes-object signal.spec.database.migrationis gated too, and ignored entirely when unmanaged. An externally-managed proxy owns its own schema — LiteLLM migrates on startup, and whatever deployed it ships its own migration hook, so the operator would be a second migrator racing the real one. Mechanically it is worse than that:BuildMigrationJobtakes its image fromspec.image.tag(falling back tolatest), which for an unmanaged instance describes nothing the operator deployed. Leaving it open meant runningprisma migrate deployat an arbitrary schema version against a database the operator does not own.DatabaseReadyreportsWorkloadUnmanaged, and the message says the migration was ignored rather than skipping it silently when one was configured anyway.Tests
internal/controller/litellminstance_unmanaged_test.go— no resources created; a name-colliding Helm-owned Deployment left byte-identical, un-mutated and un-adopted; readiness tracking the probe in both directions; a name-matched Deployment not faking readiness; version read from the proxy;workloadManagedandinstanceEndpointtables.internal/controller/litellminstance_controller_test.go— four envtest specs for the CEL rule and themanageddefault, since only envtest runs API-server validation.What was verified against upstream
/health/livelinessand/health/readinessare the paths the operator's shipped client already uses for every managed instance, so the readiness signal reuses a proven code path rather than a new one./health/livelinessreturns the bare stringI'm alive!;CheckLivenesspasses a nil result and so never unmarshals it, which is why it is safe as a reachability probe. Thelitellm_versiongating described above was checked againstlitellm/proxy/health_endpoints/_health_endpoints.pyonmain, not assumed — the field had been declared in this repo'sReadinessResponsesince the initial commit but never read by anything until now.spec.workload.endpointis trimmed of trailing slashes by the client (strings.TrimRight(endpoint, "/")), sohttp://host:4000/andhttp://host:4000behave identically. The CRD pattern is^https?://[^\s/?#]+, which requires a host — the value becomes an outbound request URL, so a scheme alone should not pass.Housekeeping
make manifests generate sync-helm-crdsandmake bundleare committed.make test,make lint,helm lintandhelm templatepass locally. Commit is signed off, README anddocs/reference/litellminstance.mddocument the field, and there's a sample atconfig/samples/litellm_v1alpha1_litellminstance_unmanaged.yaml.A small drive-by: the master-key-with-autogenerate-fallback block was copy-pasted in
resolveInstanceandprobeInstanceHealth; it's now onemasterKeyRefhelper that the new readiness probe also uses.