diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..66ff6378 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,70 @@ +# =================== +# Git +# =================== +.git +.gitignore + +# =================== +# Build artifacts +# =================== +bin/ +gomodel +coverage.out + +# =================== +# Tests +# =================== +tests/ +**/*_test.go + +# =================== +# Documentation +# =================== +*.md +LICENSE + +# =================== +# CI/CD & Release +# =================== +.github/ +.goreleaser.yaml + +# =================== +# Development tooling +# =================== +.golangci.yml +.pre-commit-config.yaml +Makefile + +# =================== +# Docker & Kubernetes +# =================== +Dockerfile +.dockerignore +docker-compose.yaml +helm/ +prometheus.yml + +# =================== +# IDE & Editor configs +# =================== +.claude/ +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# =================== +# Environment & Secrets +# =================== +.env +.env.* + +# =================== +# Miscellaneous +# =================== +.cache/ +*.bck.yml +repomix-output.* +session-*.md diff --git a/.gitignore b/.gitignore index 8eac208d..ecd34744 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ /*.bck.yml /repomix-output.* /coverage.out + +# Helm chart dependencies +**/charts/*.tgz diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..b35e2462 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Build stage +FROM golang:1.24-alpine3.23 AS builder + +WORKDIR /app + +# Install ca-certificates for HTTPS requests +RUN apk add --no-cache ca-certificates=20251003-r0 + +# Download dependencies first for better layer caching +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source and build +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /gomodel ./cmd/gomodel + +# Runtime stage +FROM gcr.io/distroless/static-debian12:nonroot + +# Copy binary and ca-certificates +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /gomodel /gomodel +COPY --from=builder /app/config/*.yaml /app/config/ + +WORKDIR /app + +EXPOSE 8080 + +ENTRYPOINT ["/gomodel"] diff --git a/helm/Chart.lock b/helm/Chart.lock new file mode 100644 index 00000000..c76da93d --- /dev/null +++ b/helm/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: redis + repository: oci://registry-1.docker.io/bitnamicharts + version: 24.1.0 +digest: sha256:46e96c6627f431805f1ded87a1d15a25c3aaf648a1242343f52b8c6a51e2cb2f +generated: "2026-01-08T06:38:01.742616425+01:00" diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 00000000..95cdc50f --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,30 @@ +apiVersion: v2 +name: gomodel +description: High-performance AI gateway for multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, xAI) +type: application +version: 0.1.0 +appVersion: "1.0.0" + +keywords: + - llm + - ai + - gateway + - openai + - anthropic + - gemini + - groq + - xai + - proxy + +home: https://github.com/ENTERPILOT/GOModel +sources: + - https://github.com/ENTERPILOT/GOModel + +maintainers: + - name: GOModel Team + +dependencies: + - name: redis + version: "^24.0.0" + repository: oci://registry-1.docker.io/bitnamicharts + condition: redis.enabled diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 00000000..966443a6 --- /dev/null +++ b/helm/README.md @@ -0,0 +1,144 @@ +# GOModel Helm Chart + +High-performance AI gateway for multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, xAI). + +## Prerequisites + +- Kubernetes 1.29+ (for Gateway API v1 support) +- Helm 3.x +- (Optional) Prometheus Operator for ServiceMonitor support + +## Installation + +### Add the Helm repository (if published) + +```bash +helm repo add gomodel https://your-org.github.io/gomodel +helm repo update +``` + +### Install from local chart + +```bash +# Basic install with OpenAI +helm install gomodel ./helm \ + -n gomodel --create-namespace \ + --set providers.openai.enabled=true \ + --set providers.openai.apiKey="sk-..." + +# Multi-provider setup with Redis cache +helm install gomodel ./helm \ + -n gomodel --create-namespace \ + --set providers.openai.enabled=true \ + --set providers.openai.apiKey="sk-..." \ + --set providers.anthropic.enabled=true \ + --set providers.anthropic.apiKey="sk-ant-..." \ + --set redis.enabled=true + +# Using existing secrets (GitOps-friendly) +helm install gomodel ./helm \ + -n gomodel --create-namespace \ + --set providers.existingSecret="llm-api-keys" \ + --set providers.openai.enabled=true \ + --set providers.anthropic.enabled=true +``` + +## Configuration + +### Key Values + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `replicaCount` | Number of replicas | `2` | +| `image.repository` | Image repository | `enterpilot/gomodel` | +| `image.tag` | Image tag | `""` (uses appVersion) | +| `server.port` | Server port | `8080` | +| `server.bodySizeLimit` | Max request body size | `"10M"` | +| `auth.masterKey` | Master key for auth | `""` | +| `auth.existingSecret` | Existing secret for auth | `""` | +| `providers.existingSecret` | Existing secret for API keys | `""` | +| `providers.openai.enabled` | Enable OpenAI | `false` | +| `providers.anthropic.enabled` | Enable Anthropic | `false` | +| `providers.gemini.enabled` | Enable Gemini | `false` | +| `providers.groq.enabled` | Enable Groq | `false` | +| `providers.xai.enabled` | Enable xAI | `false` | +| `cache.type` | Cache type (local/redis) | `"redis"` | +| `redis.enabled` | Deploy Redis subchart | `true` | +| `metrics.enabled` | Enable Prometheus metrics | `true` | +| `metrics.serviceMonitor.enabled` | Create ServiceMonitor | `false` | +| `ingress.enabled` | Enable Ingress | `false` | +| `gateway.enabled` | Enable Gateway API HTTPRoute | `false` | +| `autoscaling.enabled` | Enable HPA | `false` | + +### Using Existing Secrets + +Create a secret with your API keys: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: llm-api-keys +type: Opaque +stringData: + OPENAI_API_KEY: "sk-..." + ANTHROPIC_API_KEY: "sk-ant-..." + GEMINI_API_KEY: "..." +``` + +Then reference it: + +```bash +helm install gomodel ./helm \ + --set providers.existingSecret="llm-api-keys" \ + --set providers.openai.enabled=true +``` + +### Ingress Example + +```yaml +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: gomodel.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: gomodel-tls + hosts: + - gomodel.example.com +``` + +### Gateway API Example + +```yaml +gateway: + enabled: true + parentRef: + name: my-gateway + namespace: gateway-system + hostnames: + - gomodel.example.com +``` + +## Upgrading + +```bash +helm upgrade gomodel ./helm -n gomodel -f values.yaml +``` + +## Uninstalling + +```bash +helm uninstall gomodel -n gomodel +``` + +# Todo + +- Add a values-demo.yaml file with a demo setup ready to run +- Consider adding prometheus + grafana stack as an optional subchart +- Add an example for production-ready redis configuration with persistence and authentication enabled \ No newline at end of file diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt new file mode 100644 index 00000000..af33024f --- /dev/null +++ b/helm/templates/NOTES.txt @@ -0,0 +1,57 @@ +Thank you for installing {{ .Chart.Name }}! + +Your release is named: {{ .Release.Name }} + +To get the application URL: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ (first $host.paths).path }} +{{- end }} +{{- else if .Values.gateway.enabled }} + Configure your Gateway to route traffic to the service. + Service: {{ include "gomodel.fullname" . }}:{{ .Values.service.port }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "gomodel.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status with: kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "gomodel.fullname" . }} + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "gomodel.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "gomodel.fullname" . }} 8080:{{ .Values.service.port }} + echo "Visit http://127.0.0.1:8080 to use the API" +{{- end }} + +{{- if not .Values.auth.masterKey }} +{{- if not .Values.auth.existingSecret }} + +⚠️ WARNING: No authentication configured! + The gateway is running in UNSAFE MODE without authentication. + Set auth.masterKey or auth.existingSecret for production use. +{{- end }} +{{- end }} + +{{- $enabledProviders := list }} +{{- if .Values.providers.openai.enabled }}{{ $enabledProviders = append $enabledProviders "openai" }}{{ end }} +{{- if .Values.providers.anthropic.enabled }}{{ $enabledProviders = append $enabledProviders "anthropic" }}{{ end }} +{{- if .Values.providers.gemini.enabled }}{{ $enabledProviders = append $enabledProviders "gemini" }}{{ end }} +{{- if .Values.providers.groq.enabled }}{{ $enabledProviders = append $enabledProviders "groq" }}{{ end }} +{{- if .Values.providers.xai.enabled }}{{ $enabledProviders = append $enabledProviders "xai" }}{{ end }} + +{{- if eq (len $enabledProviders) 0 }} + +⚠️ WARNING: No providers enabled! + Enable at least one provider (e.g., providers.openai.enabled=true) +{{- else }} + +Enabled providers: {{ join ", " $enabledProviders }} +{{- end }} + +To check the logs: + kubectl logs --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "gomodel.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -f + +To test the health endpoint: + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "gomodel.fullname" . }} 8080:{{ .Values.service.port }} & + curl http://127.0.0.1:8080/health diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl new file mode 100644 index 00000000..24083bf3 --- /dev/null +++ b/helm/templates/_helpers.tpl @@ -0,0 +1,137 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "gomodel.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "gomodel.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "gomodel.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "gomodel.labels" -}} +helm.sh/chart: {{ include "gomodel.chart" . }} +{{ include "gomodel.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "gomodel.selectorLabels" -}} +app.kubernetes.io/name: {{ include "gomodel.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the secret containing provider API keys +*/}} +{{- define "gomodel.providerSecretName" -}} +{{- if .Values.providers.existingSecret }} +{{- .Values.providers.existingSecret }} +{{- else }} +{{- include "gomodel.fullname" . }}-providers +{{- end }} +{{- end }} + +{{/* +Create the name of the secret containing auth credentials +*/}} +{{- define "gomodel.authSecretName" -}} +{{- if .Values.auth.existingSecret }} +{{- .Values.auth.existingSecret }} +{{- else }} +{{- include "gomodel.fullname" . }}-auth +{{- end }} +{{- end }} + +{{/* +Determine the Redis URL - either from values or auto-generated for subchart +*/}} +{{- define "gomodel.redisUrl" -}} +{{- if .Values.cache.redis.url }} +{{- .Values.cache.redis.url }} +{{- else if .Values.redis.enabled }} +{{- printf "redis://%s-redis-master:6379" .Release.Name }} +{{- else }} +{{- "" }} +{{- end }} +{{- end }} + +{{/* +Determine the cache type - auto-set to redis if subchart is enabled +*/}} +{{- define "gomodel.cacheType" -}} +{{- if .Values.redis.enabled }} +{{- "redis" }} +{{- else }} +{{- .Values.cache.type }} +{{- end }} +{{- end }} + +{{/* +Create the image reference +*/}} +{{- define "gomodel.image" -}} +{{- $tag := .Values.image.tag | default .Chart.AppVersion }} +{{- printf "%s:%s" .Values.image.repository $tag }} +{{- end }} + +{{/* +Generate provider API key entries for the Secret stringData. +*/}} +{{- define "gomodel.providerSecretData" -}} +{{- range $name, $config := .Values.providers }} + {{- if and (kindIs "map" $config) (hasKey $config "enabled") }} + {{- if and $config.enabled $config.apiKey }} +{{ upper $name }}_API_KEY: {{ $config.apiKey | quote }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} + +{{/* +Generate provider environment variables for the Deployment. +*/}} +{{- define "gomodel.providerEnvVars" -}} +{{- $secretName := include "gomodel.providerSecretName" . -}} +{{- range $name, $config := .Values.providers }} + {{- if and (kindIs "map" $config) (hasKey $config "enabled") $config.enabled }} +- name: {{ upper $name }}_API_KEY + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ upper $name }}_API_KEY + {{- if $config.baseUrl }} +- name: {{ upper $name }}_BASE_URL + value: {{ $config.baseUrl | quote }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml new file mode 100644 index 00000000..54ada0d5 --- /dev/null +++ b/helm/templates/configmap.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gomodel.fullname" . }} + labels: + {{- include "gomodel.labels" . | nindent 4 }} +data: + PORT: {{ .Values.server.port | quote }} + BODY_SIZE_LIMIT: {{ .Values.server.bodySizeLimit | quote }} + CACHE_TYPE: {{ include "gomodel.cacheType" . | quote }} + {{- if or .Values.redis.enabled (eq .Values.cache.type "redis") }} + REDIS_KEY: {{ .Values.cache.redis.key | quote }} + {{- end }} + METRICS_ENABLED: {{ .Values.metrics.enabled | quote }} + METRICS_ENDPOINT: {{ .Values.metrics.endpoint | quote }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 00000000..036ad7bb --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,119 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gomodel.fullname" . }} + labels: + {{- include "gomodel.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "gomodel.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "gomodel.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: {{ include "gomodel.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.server.port }} + protocol: TCP + env: + # Server configuration + - name: PORT + valueFrom: + configMapKeyRef: + name: {{ include "gomodel.fullname" . }} + key: PORT + - name: BODY_SIZE_LIMIT + valueFrom: + configMapKeyRef: + name: {{ include "gomodel.fullname" . }} + key: BODY_SIZE_LIMIT + # Cache configuration + - name: CACHE_TYPE + valueFrom: + configMapKeyRef: + name: {{ include "gomodel.fullname" . }} + key: CACHE_TYPE + {{- if or .Values.redis.enabled (eq .Values.cache.type "redis") }} + - name: REDIS_URL + valueFrom: + secretKeyRef: + name: {{ include "gomodel.providerSecretName" . }} + key: REDIS_URL + optional: true + - name: REDIS_KEY + valueFrom: + configMapKeyRef: + name: {{ include "gomodel.fullname" . }} + key: REDIS_KEY + {{- end }} + # Metrics configuration + - name: METRICS_ENABLED + valueFrom: + configMapKeyRef: + name: {{ include "gomodel.fullname" . }} + key: METRICS_ENABLED + - name: METRICS_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "gomodel.fullname" . }} + key: METRICS_ENDPOINT + # Authentication + {{- if or .Values.auth.masterKey .Values.auth.existingSecret }} + - name: GOMODEL_MASTER_KEY + valueFrom: + secretKeyRef: + name: {{ include "gomodel.authSecretName" . }} + key: {{ .Values.auth.existingSecretKey | default "master-key" }} + {{- end }} + # Provider API keys (conditional based on enabled providers) +{{- include "gomodel.providerEnvVars" . | nindent 12 }} + volumeMounts: + - name: cache + mountPath: /cache + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + - name: cache + emptyDir: {} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/templates/gateway.yaml b/helm/templates/gateway.yaml new file mode 100644 index 00000000..7afbbc2f --- /dev/null +++ b/helm/templates/gateway.yaml @@ -0,0 +1,28 @@ +{{- if .Values.gateway.enabled -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "gomodel.fullname" . }} + labels: + {{- include "gomodel.labels" . | nindent 4 }} +spec: + parentRefs: + - name: {{ .Values.gateway.parentRef.name }} + {{- if .Values.gateway.parentRef.namespace }} + namespace: {{ .Values.gateway.parentRef.namespace }} + {{- end }} + {{- if .Values.gateway.hostnames }} + hostnames: + {{- range .Values.gateway.hostnames }} + - {{ . | quote }} + {{- end }} + {{- end }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: {{ include "gomodel.fullname" . }} + port: {{ .Values.service.port }} +{{- end }} diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml new file mode 100644 index 00000000..526247f1 --- /dev/null +++ b/helm/templates/hpa.yaml @@ -0,0 +1,35 @@ +{{- if .Values.autoscaling.enabled }} +{{- if not (or .Values.autoscaling.targetCPUUtilizationPercentage .Values.autoscaling.targetMemoryUtilizationPercentage) }} +{{- fail "autoscaling.targetCPUUtilizationPercentage or autoscaling.targetMemoryUtilizationPercentage must be set when autoscaling is enabled" }} +{{- end }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "gomodel.fullname" . }} + labels: + {{- include "gomodel.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "gomodel.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml new file mode 100644 index 00000000..92dd127e --- /dev/null +++ b/helm/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "gomodel.fullname" . }} + labels: + {{- include "gomodel.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "gomodel.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/templates/pdb.yaml b/helm/templates/pdb.yaml new file mode 100644 index 00000000..4849a3d9 --- /dev/null +++ b/helm/templates/pdb.yaml @@ -0,0 +1,21 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "gomodel.fullname" . }} + labels: + {{- include "gomodel.labels" . | nindent 4 }} +spec: + {{- if and (not .Values.podDisruptionBudget.minAvailable) (not .Values.podDisruptionBudget.maxUnavailable) }} + {{- fail "PodDisruptionBudget requires either minAvailable or maxUnavailable to be set" }} + {{- end }} + {{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- end }} + {{- if .Values.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} + selector: + matchLabels: + {{- include "gomodel.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml new file mode 100644 index 00000000..1f4047c9 --- /dev/null +++ b/helm/templates/secret.yaml @@ -0,0 +1,26 @@ +{{- if not .Values.providers.existingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "gomodel.fullname" . }}-providers + labels: + {{- include "gomodel.labels" . | nindent 4 }} +type: Opaque +stringData: +{{- include "gomodel.providerSecretData" . | nindent 2 }} + {{- if or .Values.redis.enabled (eq .Values.cache.type "redis") }} + REDIS_URL: {{ include "gomodel.redisUrl" . | quote }} + {{- end }} +{{- end }} +--- +{{- if and .Values.auth.masterKey (not .Values.auth.existingSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "gomodel.fullname" . }}-auth + labels: + {{- include "gomodel.labels" . | nindent 4 }} +type: Opaque +stringData: + master-key: {{ .Values.auth.masterKey | quote }} +{{- end }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 00000000..170a5a54 --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gomodel.fullname" . }} + labels: + {{- include "gomodel.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "gomodel.selectorLabels" . | nindent 4 }} diff --git a/helm/templates/servicemonitor.yaml b/helm/templates/servicemonitor.yaml new file mode 100644 index 00000000..49ba6b2c --- /dev/null +++ b/helm/templates/servicemonitor.yaml @@ -0,0 +1,19 @@ +{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "gomodel.fullname" . }} + labels: + {{- include "gomodel.labels" . | nindent 4 }} + {{- with .Values.metrics.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "gomodel.selectorLabels" . | nindent 6 }} + endpoints: + - port: http + path: {{ .Values.metrics.endpoint }} + interval: {{ .Values.metrics.serviceMonitor.interval }} +{{- end }} diff --git a/helm/values.schema.json b/helm/values.schema.json new file mode 100644 index 00000000..f75a74ba --- /dev/null +++ b/helm/values.schema.json @@ -0,0 +1,586 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "GOModel Helm Chart Values", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "gateway": { + "properties": { "enabled": { "const": true } } + } + } + }, + "then": { + "properties": { + "gateway": { + "properties": { + "parentRef": { + "properties": { + "name": { "minLength": 1 } + }, + "required": ["name"] + } + }, + "required": ["parentRef"] + } + } + } + }, + { + "if": { + "properties": { + "cache": { + "properties": { "type": { "const": "redis" } } + }, + "redis": { + "properties": { "enabled": { "const": false } } + } + } + }, + "then": { + "properties": { + "cache": { + "properties": { + "redis": { + "properties": { + "url": { "minLength": 1 } + }, + "required": ["url"] + } + }, + "required": ["redis"] + } + } + } + } + ], + "anyOf": [ + { + "properties": { + "providers": { + "properties": { + "existingSecret": { "minLength": 1 } + }, + "required": ["existingSecret"] + } + } + }, + { + "properties": { + "providers": { + "properties": { + "openai": { + "properties": { + "enabled": { "const": true } + } + } + }, + "if": { + "properties": { + "openai": { + "properties": { "enabled": { "const": true } } + } + } + }, + "then": { + "anyOf": [ + { + "properties": { + "existingSecret": { "minLength": 1 } + }, + "required": ["existingSecret"] + }, + { + "properties": { + "openai": { + "properties": { + "apiKey": { "minLength": 1 } + }, + "required": ["apiKey"] + } + } + } + ] + } + } + } + }, + { + "properties": { + "providers": { + "properties": { + "anthropic": { + "properties": { + "enabled": { "const": true } + } + } + }, + "if": { + "properties": { + "anthropic": { + "properties": { "enabled": { "const": true } } + } + } + }, + "then": { + "anyOf": [ + { + "properties": { + "existingSecret": { "minLength": 1 } + }, + "required": ["existingSecret"] + }, + { + "properties": { + "anthropic": { + "properties": { + "apiKey": { "minLength": 1 } + }, + "required": ["apiKey"] + } + } + } + ] + } + } + } + }, + { + "properties": { + "providers": { + "properties": { + "gemini": { + "properties": { + "enabled": { "const": true } + } + } + }, + "if": { + "properties": { + "gemini": { + "properties": { "enabled": { "const": true } } + } + } + }, + "then": { + "anyOf": [ + { + "properties": { + "existingSecret": { "minLength": 1 } + }, + "required": ["existingSecret"] + }, + { + "properties": { + "gemini": { + "properties": { + "apiKey": { "minLength": 1 } + }, + "required": ["apiKey"] + } + } + } + ] + } + } + } + }, + { + "properties": { + "providers": { + "properties": { + "groq": { + "properties": { + "enabled": { "const": true } + } + } + }, + "if": { + "properties": { + "groq": { + "properties": { "enabled": { "const": true } } + } + } + }, + "then": { + "anyOf": [ + { + "properties": { + "existingSecret": { "minLength": 1 } + }, + "required": ["existingSecret"] + }, + { + "properties": { + "groq": { + "properties": { + "apiKey": { "minLength": 1 } + }, + "required": ["apiKey"] + } + } + } + ] + } + } + } + }, + { + "properties": { + "providers": { + "properties": { + "xai": { + "properties": { + "enabled": { "const": true } + } + } + }, + "if": { + "properties": { + "xai": { + "properties": { "enabled": { "const": true } } + } + } + }, + "then": { + "anyOf": [ + { + "properties": { + "existingSecret": { "minLength": 1 } + }, + "required": ["existingSecret"] + }, + { + "properties": { + "xai": { + "properties": { + "apiKey": { "minLength": 1 } + }, + "required": ["apiKey"] + } + } + } + ] + } + } + } + } + ], + "properties": { + "replicaCount": { + "type": "integer", + "minimum": 1 + }, + "image": { + "type": "object", + "properties": { + "repository": { "type": "string" }, + "pullPolicy": { + "type": "string", + "enum": ["Always", "IfNotPresent", "Never"] + }, + "tag": { "type": "string" } + } + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" } + } + } + }, + "nameOverride": { "type": "string" }, + "fullnameOverride": { "type": "string" }, + "server": { + "type": "object", + "properties": { + "port": { "type": "integer" }, + "bodySizeLimit": { "type": "string" } + } + }, + "auth": { + "type": "object", + "properties": { + "masterKey": { "type": "string" }, + "existingSecret": { "type": "string" }, + "existingSecretKey": { "type": "string" } + } + }, + "providers": { + "type": "object", + "properties": { + "existingSecret": { "type": "string" }, + "openai": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "apiKey": { "type": "string" }, + "baseUrl": { "type": "string" } + } + }, + "anthropic": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "apiKey": { "type": "string" }, + "baseUrl": { "type": "string" } + } + }, + "gemini": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "apiKey": { "type": "string" }, + "baseUrl": { "type": "string" } + } + }, + "groq": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "apiKey": { "type": "string" }, + "baseUrl": { "type": "string" } + } + }, + "xai": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "apiKey": { "type": "string" }, + "baseUrl": { "type": "string" } + } + } + } + }, + "cache": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["local", "redis"] + }, + "redis": { + "type": "object", + "properties": { + "url": { "type": "string" }, + "key": { "type": "string" }, + "ttl": { "type": "integer" } + } + } + } + }, + "redis": { + "type": "object", + "additionalProperties": true + }, + "metrics": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "endpoint": { "type": "string" }, + "serviceMonitor": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "interval": { "type": "string" }, + "labels": { "type": "object" } + } + } + } + }, + "service": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["ClusterIP", "LoadBalancer", "NodePort"] + }, + "port": { "type": "integer" }, + "annotations": { "type": "object" } + } + }, + "ingress": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "className": { "type": "string" }, + "annotations": { "type": "object" }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "host": { "type": "string" }, + "paths": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "pathType": { + "type": "string", + "enum": ["Prefix", "Exact", "ImplementationSpecific"] + } + } + } + } + } + } + }, + "tls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "secretName": { "type": "string" }, + "hosts": { + "type": "array", + "items": { "type": "string" } + } + } + } + } + } + }, + "gateway": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "parentRef": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "namespace": { "type": "string" } + } + }, + "hostnames": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "resources": { + "type": "object", + "properties": { + "requests": { + "type": "object", + "properties": { + "cpu": { "type": ["string", "number"] }, + "memory": { "type": "string" } + } + }, + "limits": { + "type": "object", + "properties": { + "cpu": { "type": ["string", "number"] }, + "memory": { "type": "string" } + } + } + } + }, + "autoscaling": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "minReplicas": { "type": "integer", "minimum": 1 }, + "maxReplicas": { "type": "integer", "minimum": 1 }, + "targetCPUUtilizationPercentage": { "type": "integer" }, + "targetMemoryUtilizationPercentage": { "type": "integer" } + } + }, + "podDisruptionBudget": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "minAvailable": { "type": ["integer", "string"] }, + "maxUnavailable": { "type": ["integer", "string"] } + } + }, + "livenessProbe": { + "type": "object", + "properties": { + "httpGet": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "port": { "type": ["string", "integer"] } + } + }, + "initialDelaySeconds": { "type": "integer" }, + "periodSeconds": { "type": "integer" }, + "timeoutSeconds": { "type": "integer" }, + "failureThreshold": { "type": "integer" }, + "successThreshold": { "type": "integer" } + } + }, + "readinessProbe": { + "type": "object", + "properties": { + "httpGet": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "port": { "type": ["string", "integer"] } + } + }, + "initialDelaySeconds": { "type": "integer" }, + "periodSeconds": { "type": "integer" }, + "timeoutSeconds": { "type": "integer" }, + "failureThreshold": { "type": "integer" }, + "successThreshold": { "type": "integer" } + } + }, + "podSecurityContext": { + "type": "object", + "properties": { + "runAsNonRoot": { "type": "boolean" }, + "runAsUser": { "type": "integer" }, + "runAsGroup": { "type": "integer" }, + "fsGroup": { "type": "integer" } + } + }, + "securityContext": { + "type": "object", + "properties": { + "allowPrivilegeEscalation": { "type": "boolean" }, + "readOnlyRootFilesystem": { "type": "boolean" }, + "runAsNonRoot": { "type": "boolean" }, + "runAsUser": { "type": "integer" }, + "runAsGroup": { "type": "integer" }, + "capabilities": { + "type": "object", + "properties": { + "drop": { + "type": "array", + "items": { "type": "string" } + }, + "add": { + "type": "array", + "items": { "type": "string" } + } + } + } + } + }, + "nodeSelector": { "type": "object" }, + "tolerations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "operator": { "type": "string" }, + "value": { "type": "string" }, + "effect": { "type": "string" }, + "tolerationSeconds": { "type": "integer" } + } + } + }, + "affinity": { "type": "object" }, + "podAnnotations": { "type": "object" }, + "podLabels": { "type": "object" } + } +} diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 00000000..fd6f7de8 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,250 @@ +# Default values for gomodel +# This is a YAML-formatted file. + +# -- Number of replicas (ignored if autoscaling.enabled is true) +replicaCount: 2 + +image: + # -- Image repository + repository: enterpilot/gomodel + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Overrides the image tag (default is the chart appVersion) + tag: "" + +# -- Image pull secrets +imagePullSecrets: [] +# -- Override the name of the chart +nameOverride: "" +# -- Override the full name of the chart +fullnameOverride: "" + +# Server configuration +server: + # -- Server port + port: 8080 + # -- Maximum request body size (e.g., "10M", "1G", "500K") + bodySizeLimit: "10M" + +# Authentication configuration +auth: + # -- Master key for API authentication (leave empty to disable auth - NOT recommended for production) + masterKey: "" + # -- Use an existing secret for the master key + existingSecret: "" + # -- Key in the existing secret containing the master key + existingSecretKey: "master-key" + +# LLM Provider configuration +providers: + # -- Use an existing secret for all provider API keys + # Secret should contain keys: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, GROQ_API_KEY, XAI_API_KEY + existingSecret: "" + + openai: + # -- Enable OpenAI provider + enabled: false + # -- OpenAI API key (ignored if providers.existingSecret is set) + apiKey: "" + # -- Optional: Override OpenAI base URL + baseUrl: "" + + anthropic: + # -- Enable Anthropic provider + enabled: false + # -- Anthropic API key (ignored if providers.existingSecret is set) + apiKey: "" + # -- Optional: Override Anthropic base URL + baseUrl: "" + + gemini: + # -- Enable Google Gemini provider + enabled: false + # -- Gemini API key (ignored if providers.existingSecret is set) + apiKey: "" + # -- Optional: Override Gemini base URL + baseUrl: "" + + groq: + # -- Enable Groq provider + enabled: false + # -- Groq API key (ignored if providers.existingSecret is set) + apiKey: "" + # -- Optional: Override Groq base URL + baseUrl: "" + + xai: + # -- Enable xAI (Grok) provider + enabled: false + # -- xAI API key (ignored if providers.existingSecret is set) + apiKey: "" + # -- Optional: Override xAI base URL + baseUrl: "" + +# Cache configuration +cache: + # -- Cache type: "local" or "redis" + type: "redis" + + redis: + # -- Redis connection URL (e.g., "redis://redis:6379") + # If redis.enabled is true, this is auto-configured to use the subchart + url: "" + # -- Redis key for storing the model cache + key: "gomodel:models" + # -- TTL for cached data in seconds (default: 24 hours) + ttl: 86400 + +# Redis subchart configuration (Bitnami Redis) +redis: + # -- Deploy Redis subchart + enabled: true + # -- Redis architecture: standalone or replication + architecture: standalone + auth: + # -- Disable Redis authentication for simplicity + enabled: false + master: + persistence: + # -- Disable persistence for standalone mode + enabled: false + +# Prometheus metrics configuration +metrics: + # -- Enable Prometheus metrics + enabled: true + # -- Metrics endpoint path + endpoint: "/metrics" + + serviceMonitor: + # -- Create a ServiceMonitor for Prometheus Operator + enabled: false + # -- Scrape interval + interval: "15s" + # -- Additional labels for the ServiceMonitor + labels: {} + +# Kubernetes Service configuration +service: + # -- Service type: ClusterIP, LoadBalancer, NodePort + type: ClusterIP + # -- Service port + port: 8080 + # -- Service annotations + annotations: {} + +# Ingress configuration +ingress: + # -- Enable Ingress + enabled: false + # -- Ingress class name + className: "" + # -- Ingress annotations + annotations: {} + # -- Ingress hosts configuration + hosts: + - host: gomodel.local + paths: + - path: / + pathType: Prefix + # -- Ingress TLS configuration + tls: [] + # - secretName: gomodel-tls + # hosts: + # - gomodel.local + +# Gateway API configuration +gateway: + # -- Enable Gateway API HTTPRoute + enabled: false + # -- Parent Gateway reference + parentRef: + # -- Gateway name + name: "" + # -- Gateway namespace (defaults to release namespace) + namespace: "" + # -- Hostnames for the HTTPRoute + hostnames: [] + # - gomodel.example.com + +# Resource requests and limits +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 1000m + memory: 512Mi + +# Horizontal Pod Autoscaler configuration +autoscaling: + # -- Enable HPA + enabled: false + # -- Minimum replicas + minReplicas: 2 + # -- Maximum replicas + maxReplicas: 10 + # -- Target CPU utilization percentage + targetCPUUtilizationPercentage: 70 + # -- Target memory utilization percentage (optional) + # targetMemoryUtilizationPercentage: 80 + +# Pod Disruption Budget configuration +podDisruptionBudget: + # -- Enable PDB + enabled: true + # -- Minimum available pods + minAvailable: 1 + # -- Maximum unavailable pods (alternative to minAvailable) + # maxUnavailable: 1 + +# Liveness probe configuration +livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +# Readiness probe configuration +readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + +# Pod security context +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + +# Container security context +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + capabilities: + drop: + - ALL + +# -- Node selector +nodeSelector: {} + +# -- Tolerations +tolerations: [] + +# -- Affinity rules +affinity: {} + +# -- Additional pod annotations +podAnnotations: {} + +# -- Additional pod labels +podLabels: {}