Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`LiteLLMInstance.spec.workload.managed: false`** ([#29](https://github.com/PalenaAI/litellm-operator/issues/29)) — attaches an instance to a LiteLLM proxy the operator did not deploy, 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. Workload reconciliation and auto-rollback are skipped entirely: nothing is created, and no existing object is adopted or mutated, replacing the RBAC-denial workaround that left the instance permanently `Degraded` while it worked. `spec.database.migration` is ignored: an externally-managed proxy owns its own schema, and the migration Job would otherwise run `prisma migrate deploy` from `spec.image.tag` (defaulting to `latest`) against a database the operator does not own. Health probing, config sync, and finalizer-based cleanup of upstream entities are unaffected.
- **`LiteLLMInstance.spec.workload.endpoint`** — sets the admin API URL explicitly instead of deriving `http(s)://<metadata.name>.<namespace>.svc:<service.port>`, 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. Valid only when `managed` is `false`; rejected by a CEL rule otherwise. Readiness for an unmanaged instance now comes from the admin API answering at that endpoint rather than from a name-matched Deployment, which also makes a StatefulSet-backed or off-cluster proxy work; the `Ready` condition reports `ProxyReachable` / `ProxyNotReachable` and no `PodsHealthy` condition is set, because the operator owns no pods. `status.version` is left empty instead of echoing `spec.image.tag`, which describes nothing the operator deployed; it is filled in only when the proxy discloses `litellm_version` on `/health/readiness` (LiteLLM gates that behind its own `allow_public_health_readiness_details`).

## [0.23.0] - 2026-08-30

### Added
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,44 @@ spec:
port: 4000
```

### Attaching to an Existing LiteLLM Deployment

Already running LiteLLM from a Helm chart, a GitOps pipeline or an internal platform? Set `workload.managed: false` and the operator provisions **nothing** — no Deployment, Service, ConfigMap or ServiceAccount is created, and nothing existing is adopted or mutated. You get the entity CRDs (`LiteLLMTeam`, `LiteLLMVirtualKey`, `LiteLLMBudget`, `LiteLLMModel`, ...) against the proxy you already have.

```yaml
apiVersion: litellm.palena.ai/v1alpha1
kind: LiteLLMInstance
metadata:
name: my-gateway
spec:
workload:
managed: false
# Optional. Defaults to http(s)://<name>.<namespace>.svc:<service.port>
endpoint: http://litellm.platform.svc:4000
masterKey:
secretRef:
name: litellm-master-key
key: LITELLM_MASTER_KEY
database: {}
```

Two fields matter:

- **`endpoint`** — where the operator reaches the admin API. Omit it and the operator derives `http(s)://<metadata.name>.<namespace>.svc:<spec.service.port>`, which requires this CR to be named after the existing Service. Set it explicitly to attach to a Service under a different name, in another namespace, or to a proxy outside the cluster entirely.
- **`masterKey`** — the admin key of the *existing* proxy. `autoGenerate: true` makes no sense here: the operator would mint a key the running proxy has never heard of.

Readiness comes from the admin API answering (`/health/liveliness`), not from a Deployment the operator does not own, so a StatefulSet or an off-cluster proxy works the same way:

```bash
kubectl get litellminstance my-gateway
# NAME READY ENDPOINT VERSION AGE
# my-gateway True http://litellm.platform.svc:4000 30s
```

`status.version` is left empty rather than echoing an image tag the operator never chose. It is populated only when the proxy discloses `litellm_version` on `/health/readiness`, which LiteLLM does only if its own `general_settings` sets `allow_public_health_readiness_details: true` — that endpoint takes no auth, so the master key does not unlock it.

Everything else keeps working: health probing, config sync, and finalizer-based cleanup of upstream entities. Only workload provisioning and auto-rollback are skipped. `endpoint` is rejected when `managed` is true.

### 5. Register a model

```yaml
Expand Down
37 changes: 37 additions & 0 deletions api/v1alpha1/litellminstance_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ type LiteLLMInstanceSpec struct {
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Image"
Image ImageSpec `json:"image,omitempty"`

// Workload controls whether the operator provisions the proxy workload.
// Omit it (the default) to have the operator create and own the
// Deployment, Service, ConfigMap and ServiceAccount as usual.
// +optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Workload"
Workload *WorkloadSpec `json:"workload,omitempty"`

// Number of LiteLLM proxy replicas.
// +kubebuilder:default=1
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Replicas"
Expand Down Expand Up @@ -613,6 +620,36 @@ type ImageSpec struct {
PullSecrets []SecretRef `json:"pullSecrets,omitempty"`
}

// WorkloadSpec controls whether the operator provisions the LiteLLM proxy
// workload, or merely attaches to one that already exists.
//
// With managed=false the operator creates nothing: no Deployment, Service,
// ConfigMap, ServiceAccount or optional resource is reconciled, and no
// existing object is adopted or mutated. The instance still resolves an
// endpoint and a master key, so the entity CRDs (LiteLLMTeam,
// LiteLLMVirtualKey, LiteLLMBudget, LiteLLMModel, ...) work against a proxy
// deployed by a Helm chart, a GitOps pipeline or anything else.
// +kubebuilder:validation:XValidation:rule="!has(self.endpoint) || (has(self.managed) && !self.managed)",message="workload.endpoint is only valid when workload.managed is false"
type WorkloadSpec struct {
// Managed indicates the operator owns the proxy workload. Set false to
// attach to a deployment managed elsewhere. Defaults to true, including
// when unset, so an omitted field never silently orphans a workload.
// +optional
// +kubebuilder:default=true
Managed *bool `json:"managed,omitempty"`

// Endpoint is the base URL of the existing proxy, e.g.
// "http://litellm.platform.svc:4000". Only valid when managed is false.
// Defaults to the in-cluster Service address derived from the instance
// name, namespace and spec.service.port, which requires this CR to be
// named after the existing Service. Set it explicitly to attach to a
// Service under a different name, in another namespace, or to a proxy
// outside the cluster.
// +optional
// +kubebuilder:validation:Pattern=`^https?://[^\s/?#]+`
Endpoint string `json:"endpoint,omitempty"`
}

// AutoscalingSpec defines horizontal pod autoscaling settings.
type AutoscalingSpec struct {
// Enable autoscaling.
Expand Down
25 changes: 25 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion bundle/manifests/litellm-operator.clusterserviceversion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ metadata:
capabilities: Deep Insights
categories: AI/Machine Learning
containerImage: ghcr.io/palenaai/litellm-operator:v0.23.0
createdAt: "2026-08-30T14:56:20Z"
createdAt: "2026-09-05T16:13:51Z"
description: Kubernetes operator for deploying and managing production-ready LiteLLM
AI Gateway instances.
operators.operatorframework.io/builder: operator-sdk-v1.42.1
Expand Down Expand Up @@ -903,6 +903,12 @@ spec:
- description: Upgrade strategy configuration.
displayName: Upgrade
path: upgrade
- description: |-
Workload controls whether the operator provisions the proxy workload.
Omit it (the default) to have the operator create and own the
Deployment, Service, ConfigMap and ServiceAccount as usual.
displayName: Workload
path: workload
statusDescriptors:
- description: |-
UnhealthyPods explains why proxy pods are not running — crash loops, image
Expand Down
29 changes: 29 additions & 0 deletions bundle/manifests/litellm.palena.ai_litellminstances.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4578,6 +4578,35 @@ spec:
- recreate
type: string
type: object
workload:
description: |-
Workload controls whether the operator provisions the proxy workload.
Omit it (the default) to have the operator create and own the
Deployment, Service, ConfigMap and ServiceAccount as usual.
properties:
endpoint:
description: |-
Endpoint is the base URL of the existing proxy, e.g.
"http://litellm.platform.svc:4000". Only valid when managed is false.
Defaults to the in-cluster Service address derived from the instance
name, namespace and spec.service.port, which requires this CR to be
named after the existing Service. Set it explicitly to attach to a
Service under a different name, in another namespace, or to a proxy
outside the cluster.
pattern: ^https?://[^\s/?#]+
type: string
managed:
default: true
description: |-
Managed indicates the operator owns the proxy workload. Set false to
attach to a deployment managed elsewhere. Defaults to true, including
when unset, so an omitted field never silently orphans a workload.
type: boolean
type: object
x-kubernetes-validations:
- message: workload.endpoint is only valid when workload.managed is
false
rule: '!has(self.endpoint) || (has(self.managed) && !self.managed)'
required:
- database
- masterKey
Expand Down
29 changes: 29 additions & 0 deletions config/crd/bases/litellm.palena.ai_litellminstances.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4578,6 +4578,35 @@ spec:
- recreate
type: string
type: object
workload:
description: |-
Workload controls whether the operator provisions the proxy workload.
Omit it (the default) to have the operator create and own the
Deployment, Service, ConfigMap and ServiceAccount as usual.
properties:
endpoint:
description: |-
Endpoint is the base URL of the existing proxy, e.g.
"http://litellm.platform.svc:4000". Only valid when managed is false.
Defaults to the in-cluster Service address derived from the instance
name, namespace and spec.service.port, which requires this CR to be
named after the existing Service. Set it explicitly to attach to a
Service under a different name, in another namespace, or to a proxy
outside the cluster.
pattern: ^https?://[^\s/?#]+
type: string
managed:
default: true
description: |-
Managed indicates the operator owns the proxy workload. Set false to
attach to a deployment managed elsewhere. Defaults to true, including
when unset, so an omitted field never silently orphans a workload.
type: boolean
type: object
x-kubernetes-validations:
- message: workload.endpoint is only valid when workload.managed is
false
rule: '!has(self.endpoint) || (has(self.managed) && !self.managed)'
required:
- database
- masterKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,12 @@ spec:
- description: Upgrade strategy configuration.
displayName: Upgrade
path: upgrade
- description: |-
Workload controls whether the operator provisions the proxy workload.
Omit it (the default) to have the operator create and own the
Deployment, Service, ConfigMap and ServiceAccount as usual.
displayName: Workload
path: workload
statusDescriptors:
- description: |-
UnhealthyPods explains why proxy pods are not running — crash loops, image
Expand Down
31 changes: 31 additions & 0 deletions config/samples/litellm_v1alpha1_litellminstance_unmanaged.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Attaching to a LiteLLM proxy the operator did not deploy.
#
# With workload.managed=false the operator creates nothing — no Deployment,
# Service, ConfigMap or ServiceAccount — and adopts nothing. It only resolves
# an endpoint and a master key, which is all the entity CRDs (LiteLLMTeam,
# LiteLLMVirtualKey, LiteLLMBudget, LiteLLMModel, ...) need. Use it when the
# proxy is owned by a Helm chart, a GitOps pipeline or an internal platform.
apiVersion: litellm.palena.ai/v1alpha1
kind: LiteLLMInstance
metadata:
labels:
app.kubernetes.io/name: litellm-operator
app.kubernetes.io/managed-by: kustomize
name: litellminstance-unmanaged-sample
spec:
workload:
managed: false
# Optional. Defaults to http(s)://<metadata.name>.<namespace>.svc:<service.port>,
# which requires this CR to be named after the existing Service. Set it to
# attach to a Service under another name or namespace, or to a proxy
# outside the cluster.
endpoint: http://litellm.platform.svc:4000

# The operator needs the admin key of the existing proxy to manage entities.
masterKey:
secretRef:
name: litellm-master-key
key: LITELLM_MASTER_KEY

# The existing proxy owns its own database; nothing to configure here.
database: {}
Original file line number Diff line number Diff line change
Expand Up @@ -4578,6 +4578,35 @@ spec:
- recreate
type: string
type: object
workload:
description: |-
Workload controls whether the operator provisions the proxy workload.
Omit it (the default) to have the operator create and own the
Deployment, Service, ConfigMap and ServiceAccount as usual.
properties:
endpoint:
description: |-
Endpoint is the base URL of the existing proxy, e.g.
"http://litellm.platform.svc:4000". Only valid when managed is false.
Defaults to the in-cluster Service address derived from the instance
name, namespace and spec.service.port, which requires this CR to be
named after the existing Service. Set it explicitly to attach to a
Service under a different name, in another namespace, or to a proxy
outside the cluster.
pattern: ^https?://[^\s/?#]+
type: string
managed:
default: true
description: |-
Managed indicates the operator owns the proxy workload. Set false to
attach to a deployment managed elsewhere. Defaults to true, including
when unset, so an omitted field never silently orphans a workload.
type: boolean
type: object
x-kubernetes-validations:
- message: workload.endpoint is only valid when workload.managed is
false
rule: '!has(self.endpoint) || (has(self.managed) && !self.managed)'
required:
- database
- masterKey
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`LiteLLMInstance.spec.workload.managed: false`** ([#29](https://github.com/PalenaAI/litellm-operator/issues/29)) — attaches an instance to a LiteLLM proxy the operator did not deploy, 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. Workload reconciliation and auto-rollback are skipped entirely: nothing is created, and no existing object is adopted or mutated, replacing the RBAC-denial workaround that left the instance permanently `Degraded` while it worked. `spec.database.migration` is ignored: an externally-managed proxy owns its own schema, and the migration Job would otherwise run `prisma migrate deploy` from `spec.image.tag` (defaulting to `latest`) against a database the operator does not own. Health probing, config sync, and finalizer-based cleanup of upstream entities are unaffected.
- **`LiteLLMInstance.spec.workload.endpoint`** — sets the admin API URL explicitly instead of deriving `http(s)://<metadata.name>.<namespace>.svc:<service.port>`, 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. Valid only when `managed` is `false`; rejected by a CEL rule otherwise. Readiness for an unmanaged instance now comes from the admin API answering at that endpoint rather than from a name-matched Deployment, which also makes a StatefulSet-backed or off-cluster proxy work; the `Ready` condition reports `ProxyReachable` / `ProxyNotReachable` and no `PodsHealthy` condition is set, because the operator owns no pods. `status.version` is left empty instead of echoing `spec.image.tag`, which describes nothing the operator deployed; it is filled in only when the proxy discloses `litellm_version` on `/health/readiness` (LiteLLM gates that behind its own `allow_public_health_readiness_details`).

## [0.23.0] - 2026-08-30

### Added
Expand Down
Loading