Managed resources
The resource layer lives in internal/resources/ and produces native Kubernetes objects — Deployment, StatefulSet, Service, Secret, ConfigMap, Ingress. Anything that isn't delegated to an upstream operator lives here.
Design rules
- Pure builders. Like the wiring layer, these are pure functions: take a Palena CR, return a typed Kubernetes object. No
client.Client, no I/O. - Standard labels everywhere. Every resource gets the same base label set so
kubectl get all -l operator.palena.ai/gateway=productionworks consistently. - Secrets are generated, not hard-coded. For resources that need a random password (Redis, MongoDB root, MeiliSearch master key), Palena generates a 32-char random string on first reconciliation and writes it to a Secret. The Secret is never overwritten — rotation is an explicit, separate operation.
Components
Redis (redis.go)
Managed Redis for LiteLLM rate limiting, caching, and routing.
| Resource | Name | Purpose |
|---|---|---|
Secret | <gw>-palena-redis | Contains host, port, password. |
StatefulSet | <gw>-palena-redis | Redis pods with PVC per replica. |
Service (headless) | <gw>-palena-redis | Pod DNS for StatefulSet. |
Service (ClusterIP) | <gw>-palena-redis-svc | Client-facing. |
image: redis:7-alpine
command: ["redis-server", "--requirepass", "$(REDIS_PASSWORD)", "--appendonly", "yes"]
ports: [6379]
volumeClaimTemplate:
storage: <spec.redis.managed.storageSize>
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
readinessProbe:
exec: { command: ["redis-cli", "-a", "$(REDIS_PASSWORD)", "ping"] }LibreChat (librechat/)
Four files:
deployment.go— the LibreChatDeployment, with config hash annotation for rolling restarts.configmap.go— the generatedlibrechat.yamlConfigMap.service.go— ClusterIP Service on port 3080.ingress.go— Ingress (or Route on OpenShift).
Deployment shape
image: <spec.image or "ghcr.io/danny-avila/librechat:latest">
replicas: <spec.replicas>
ports: [3080]
env:
- MONGO_URI: from MongoDB Secret
- MEILI_HOST: http://<ui>-palena-meilisearch:7700
- MEILI_MASTER_KEY: from MeiliSearch Secret
volumes:
- name: config
configMap: { name: <ui>-palena-librechat-config }
mountPath: /app/librechat.yaml
subPath: librechat.yaml
- name: branding # if logoConfigMapRef
configMap: { name: <logoConfigMapRef.name> }
mountPath: /app/client/public/img/custom
podAnnotations:
operator.palena.ai/config-hash: <sha256 of librechat.yaml>librechat.yaml generation
The operator builds the full LibreChat config from CR spec + gateway status + MCP server status:
func BuildLibreChatConfig(ui *PalenaUI, gatewayEndpoint string, mcpEndpoints map[string]string) string {
config := LibreChatConfig{
Version: "1.2.6",
Cache: true,
Endpoints: map[string]EndpointConfig{
"custom": {
APIKey: "${LITELLM_API_KEY}",
BaseURL: gatewayEndpoint + "/v1",
Models: map[string]ModelConfig{"default": {FetchModels: true}},
},
},
}
if len(mcpEndpoints) > 0 {
config.MCPServers = map[string]MCPServerConfig{}
for name, endpoint := range mcpEndpoints {
config.MCPServers[name] = MCPServerConfig{Type: "sse", URL: endpoint}
}
}
if ui.Spec.Branding != nil {
config.AppTitle = ui.Spec.Branding.AppTitle
config.AppDescription = ui.Spec.Branding.AppDescription
}
if ui.Spec.Auth != nil && ui.Spec.Auth.DisableSignup {
config.Registration.SocialLogins = []string{"openid"}
}
return marshalYAML(config)
}The SHA-256 of the marshaled YAML is attached to the pod template so that any config change (new model, new MCP endpoint, new branding) triggers a rolling restart automatically.
OIDC env vars
When auth.oidc.enabled, the operator adds:
OPENID_ISSUER=<spec.auth.oidc.issuer>
OPENID_CLIENT_ID=<from secret>
OPENID_CLIENT_SECRET=<from secret>
OPENID_CALLBACK_URL=https://<ingress.host>/oauth/openid/callback
OPENID_SCOPE=openid profile emailMongoDB (mongodb.go)
| Resource | Name |
|---|---|
Secret | <ui>-palena-mongodb (root password, connection URI) |
StatefulSet | <ui>-palena-mongodb |
Service | <ui>-palena-mongodb |
image: mongo:7
ports: [27017]
env:
- MONGO_INITDB_ROOT_USERNAME: admin
- MONGO_INITDB_ROOT_PASSWORD: <from Secret>
volumeClaimTemplate:
storage: <spec.mongodb.managed.storageSize>
readinessProbe:
exec: { command: ["mongosh", "--eval", "db.adminCommand('ping')"] }Connection URI stored in the Secret:
mongodb://admin:<pw>@<ui>-palena-mongodb.<ns>.svc.cluster.local:27017/librechat?authSource=adminMeiliSearch (meilisearch.go)
| Resource | Name |
|---|---|
Secret | <ui>-palena-meilisearch (master key) |
Deployment | <ui>-palena-meilisearch |
Service | <ui>-palena-meilisearch |
PVC | <ui>-palena-meilisearch-data |
image: getmeili/meilisearch:latest
ports: [7700]
env:
- MEILI_MASTER_KEY: <from Secret>
- MEILI_DB_PATH: /meili_data
readinessProbe:
httpGet: { path: /health, port: 7700 }Websearch MCP (websearch/)
The websearch MCP stack is a small bundle of cooperating deployments. Each file is an independent builder:
| File | What it builds |
|---|---|
deployment.go | The MCP server Deployment + Service (<mcp>-palena-websearch) |
searxng.go | SearXNG Deployment + Service + settings.yml ConfigMap |
chromium.go | Optional Chromium scraper Deployment + Service |
presidio.go | Optional Presidio analyzer + anonymizer Deployments |
flashrank.go | Optional FlashRank reranker Deployment |
configmap.go | Websearch config ConfigMap pointing at enabled sidecars |
common.go | Default image constants, port constants, labels |
MCP server deployment
image: <spec.image or "ghcr.io/palenaai/palena-websearch-mcp:latest">
replicas: <spec.replicas>
ports: [8080]
env:
- CONFIG_PATH: /etc/palena/config.yaml
volumeMounts:
- name: config
configMap: { name: <mcp>-config }
mountPath: /etc/palena
readinessProbe:
httpGet: { path: /health, port: 8080 }SearXNG
image: searxng/searxng:latest
replicas: <spec.websearch.searxng.replicas>
ports: [8080]
volumeMounts:
- name: settings
configMap: { name: <mcp>-searxng-settings }
mountPath: /etc/searxng/settings.yml
subPath: settings.yml
env:
- SEARXNG_BASE_URL: "http://localhost:8080"settings.yml is generated from spec.websearch.searxng.engines.
Chromium scraper
image: ghcr.io/browserless/chromium:latest
replicas: <spec.websearch.scraper.chromiumReplicas>
ports: [3000]
env:
- MAX_CONCURRENT_SESSIONS: <spec.websearch.scraper.maxConcurrency>
- CONNECTION_TIMEOUT: 30000
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { cpu: "2", memory: "2Gi" }Presidio (PII)
# Analyzer
image: mcr.microsoft.com/presidio-analyzer:latest
ports: [5002]
# Anonymizer
image: mcr.microsoft.com/presidio-anonymizer:latest
ports: [5001]FlashRank
image: ghcr.io/palenaai/palena-flashrank:latest
replicas: 1
ports: [8000]
resources:
requests: { cpu: 500m, memory: 512Mi }
limits: { cpu: "1", memory: "1Gi" }
readinessProbe:
httpGet: { path: /health, port: 8000 }Websearch config generation
func BuildWebsearchConfig(mcp *PalenaMCPServer) string {
ws := mcp.Spec.Websearch
config := WebsearchConfig{
Search: SearchConfig{
SearXNG: SearXNGConfig{
Endpoint: fmt.Sprintf("http://%s-searxng.%s.svc:8080", mcp.Name, mcp.Namespace),
},
},
}
if ws.Scraper != nil && ws.Scraper.ChromiumEnabled {
config.Scraper.Chromium.Endpoint = fmt.Sprintf("http://%s-chromium.%s.svc:3000", mcp.Name, mcp.Namespace)
config.Scraper.Chromium.Enabled = true
}
if ws.Presidio != nil && ws.Presidio.Enabled {
config.PII.Enabled = true
config.PII.Mode = ws.Presidio.Mode
config.PII.AnalyzerEndpoint = fmt.Sprintf("http://%s-presidio-analyzer.%s.svc:5002", mcp.Name, mcp.Namespace)
config.PII.AnonymizerEndpoint = fmt.Sprintf("http://%s-presidio-anonymizer.%s.svc:5001", mcp.Name, mcp.Namespace)
}
if ws.Reranker != nil {
config.Reranker.Provider = ws.Reranker.Provider
if ws.Reranker.Provider == "flashrank" {
config.Reranker.FlashRank.Endpoint = fmt.Sprintf("http://%s-flashrank.%s.svc:8000", mcp.Name, mcp.Namespace)
}
}
return marshalYAML(config)
}NetworkPolicies (networkpolicy.go)
Generated for PalenaGateway when security.networkPolicies.enabled.
Default deny
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: <gw>-palena-default-deny
spec:
podSelector:
matchLabels:
app.kubernetes.io/part-of: palena
operator.palena.ai/gateway: <gw>
policyTypes: [Ingress, Egress]
ingress: []
egress: []Per-path allow rules
One NetworkPolicy per allowed communication path:
<gw>-allow-litellm-to-pg— LiteLLM → PostgreSQL (5432)<gw>-allow-litellm-to-redis— LiteLLM → Redis (6379)<gw>-allow-ingress-to-litellm— Ingress → LiteLLM (4000)
The UI controller adds:
<ui>-allow-librechat-to-litellm— LibreChat → LiteLLM (4000)<ui>-allow-librechat-to-mongodb— LibreChat → MongoDB (27017)<ui>-allow-ingress-to-librechat— Ingress → LibreChat (3080)
Common helpers
Standard labels
func StandardLabels(component, instance, role, gatewayName string) map[string]string {
labels := map[string]string{
"app.kubernetes.io/name": component,
"app.kubernetes.io/instance": instance,
"app.kubernetes.io/component": role,
"app.kubernetes.io/part-of": "palena",
"app.kubernetes.io/managed-by": "palena-operator",
}
if gatewayName != "" {
labels["operator.palena.ai/gateway"] = gatewayName
}
return labels
}Resource naming
func ResourceName(crName, suffix string) string {
return fmt.Sprintf("%s-palena-%s", crName, suffix)
}Random string (for generated Secrets)
func generateRandomString(length int) string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, length)
for i := range b {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
b[i] = chars[n.Int64()]
}
return string(b)
}Uses crypto/rand, not math/rand — Palena generates credentials for production workloads, so the entropy matters.