diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..2e580fed --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +# Makefile for LightRAG Helm packaging + +# Configuration +CHART_NAME := lightrag-minimal +CHART_DIR := k8s-deploy/$(CHART_NAME) +CHART_PACKAGE_DIR := dist/charts +HELM_REGISTRY := ghcr.io/neuro-inc/helm-charts + +RAW_VERSION := $(if $(VERSION),$(VERSION),$(shell git describe --tags --always --dirty 2>/dev/null)) +SANITIZED_VERSION := $(shell RAW="$(RAW_VERSION)" python - <<'PY' +import os, re +raw = os.environ.get("RAW", "").strip() +if not raw: + raw = "0.0.0" +raw = raw.lstrip("v") +sanitized = re.sub(r"[^0-9A-Za-z\\.\\-]", "-", raw) +print(sanitized or "0.0.0") +PY +) +CHART_VERSION := $(SANITIZED_VERSION) +CHART_PACKAGE := $(CHART_PACKAGE_DIR)/$(CHART_NAME)-$(CHART_VERSION).tgz + +GITHUB_USERNAME := $(shell echo "$$APOLO_GITHUB_TOKEN" | base64 -d 2>/dev/null | cut -d: -f1 2>/dev/null || echo "oauth2") + +.PHONY: help helm-package helm-push clean + +help: + @echo "Available targets:" + @echo " helm-package - Package the LightRAG Helm chart (version: $(CHART_VERSION))" + @echo " helm-push - Package and push the chart to $(HELM_REGISTRY)" + @echo " clean - Remove packaged charts from $(CHART_PACKAGE_DIR)" + @echo "\nSet VERSION=1.2.3 to override the git-derived chart version." + +helm-package: + @if [ -z "$(CHART_VERSION)" ]; then \ + echo "Error: unable to determine chart version."; \ + exit 1; \ + fi + @echo "Packaging $(CHART_NAME) chart version $(CHART_VERSION)..." + @mkdir -p $(CHART_PACKAGE_DIR) + helm dependency update $(CHART_DIR) >/dev/null + helm package $(CHART_DIR) \ + --version $(CHART_VERSION) \ + --app-version $(CHART_VERSION) \ + -d $(CHART_PACKAGE_DIR) + @echo "โœ… Chart packaged at $(CHART_PACKAGE)" + +helm-push: helm-package + @if [ -z "$(APOLO_GITHUB_TOKEN)" ]; then \ + echo "Error: APOLO_GITHUB_TOKEN not set. Please export a token with write:packages."; \ + exit 1; \ + fi + @echo "Logging into Helm registry ghcr.io as $(GITHUB_USERNAME)..." + echo "$(APOLO_GITHUB_TOKEN)" | helm registry login ghcr.io -u $(GITHUB_USERNAME) --password-stdin >/dev/null + @echo "Pushing chart $(CHART_NAME):$(CHART_VERSION) to $(HELM_REGISTRY)..." + helm push $(CHART_PACKAGE) oci://$(HELM_REGISTRY) + @echo "โœ… Chart pushed to $(HELM_REGISTRY)" + +clean: + @echo "Removing packaged charts..." + rm -rf $(CHART_PACKAGE_DIR) + @echo "โœ… Cleaned" diff --git a/k8s-deploy/lightrag-minimal/README.md b/k8s-deploy/lightrag-minimal/README.md index 6d15aeb2..0a658fe4 100644 --- a/k8s-deploy/lightrag-minimal/README.md +++ b/k8s-deploy/lightrag-minimal/README.md @@ -22,31 +22,23 @@ This chart provides a comprehensive LightRAG deployment with: ## Validated Installation Steps -### Development/Local Setup (Minikube) +### Deploying the Chart -1. **Prepare Helm repositories**: ```bash cd lightrag-minimal helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update helm dependency update -``` -2. **Set your OpenAI API key**: -```bash -export OPENAI_API_KEY="your-openai-api-key-here" -``` +# (Optional) create a copy of values.yaml and customize it for your environment +cp values.yaml my-values.yaml +# edit my-values.yaml as needed (OpenAI keys, storage class, Postgres password, etc.) -3. **Deploy for development**: -```bash -# Substitute environment variables and deploy -envsubst < values-dev.yaml > values-dev-final.yaml helm install lightrag-minimal . \ - -f values-dev-final.yaml \ + -f my-values.yaml \ --namespace lightrag \ --create-namespace -# Wait for deployment kubectl wait --namespace lightrag \ --for=condition=ready pod \ -l app.kubernetes.io/name=postgresql \ @@ -57,31 +49,14 @@ kubectl wait --namespace lightrag \ -l app.kubernetes.io/name=lightrag-minimal \ --timeout=120s -# Clean up temporary file -rm values-dev-final.yaml - -# Start port forwarding +# Optional: expose the service locally kubectl port-forward --namespace lightrag svc/lightrag-minimal 9621:9621 & ``` -### Production Setup - -```bash -# Customize values-prod.yaml first (domain, storage classes, etc.) -envsubst < values-prod.yaml > values-prod-final.yaml -helm install lightrag-minimal . \ - -f values-prod-final.yaml \ - --namespace lightrag \ - --create-namespace -rm values-prod-final.yaml -``` - ## Configuration Options ### Validated Environment Configuration -Both `values-dev.yaml` and `values-prod.yaml` include these critical settings: - ```yaml env: # OpenAI API Configuration (REQUIRED) @@ -376,4 +351,4 @@ kubectl delete namespace lightrag | Persistence | Local volumes | PersistentVolumeClaims | | Monitoring | Manual | Kubernetes native | -This chart maintains the same conservative, working configuration as the Docker Compose setup while adding Kubernetes-native features for production deployment. \ No newline at end of file +This chart maintains the same conservative, working configuration as the Docker Compose setup while adding Kubernetes-native features for production deployment. diff --git a/k8s-deploy/lightrag-minimal/templates/_helpers.tpl b/k8s-deploy/lightrag-minimal/templates/_helpers.tpl index f7d213f8..b6fac69b 100644 --- a/k8s-deploy/lightrag-minimal/templates/_helpers.tpl +++ b/k8s-deploy/lightrag-minimal/templates/_helpers.tpl @@ -77,4 +77,15 @@ PostgreSQL connection string {{- else }} {{- .Values.env.POSTGRES_HOST }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} + +{{/* +Generate AUTH_ACCOUNTS string from configured accounts +*/}} +{{- define "lightrag-minimal.authAccounts" -}} +{{- if .Values.auth.accounts }} +{{- range $index, $account := .Values.auth.accounts -}} +{{- if gt $index 0 }},{{ end -}}{{ $account.username }}:{{ $account.password }} +{{- end -}} +{{- end -}} +{{- end }} diff --git a/k8s-deploy/lightrag-minimal/templates/deployment.yaml b/k8s-deploy/lightrag-minimal/templates/deployment.yaml index b0ec5bfb..9f62ed22 100644 --- a/k8s-deploy/lightrag-minimal/templates/deployment.yaml +++ b/k8s-deploy/lightrag-minimal/templates/deployment.yaml @@ -77,7 +77,26 @@ spec: secretKeyRef: name: {{ include "lightrag-minimal.secretName" . }} key: embedding-api-key - + - name: LIGHTRAG_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "lightrag-minimal.secretName" . }} + key: lightrag-api-key + {{- if and .Values.auth.enabled (gt (len .Values.auth.accounts) 0) }} + - name: AUTH_ACCOUNTS + valueFrom: + secretKeyRef: + name: {{ include "lightrag-minimal.secretName" . }} + key: auth-accounts + {{- end }} + {{- if and .Values.auth.enabled .Values.auth.tokenSecret }} + - name: TOKEN_SECRET + valueFrom: + secretKeyRef: + name: {{ include "lightrag-minimal.secretName" . }} + key: token-secret + {{- end }} + # Storage configuration - name: LIGHTRAG_KV_STORAGE value: {{ .Values.env.LIGHTRAG_KV_STORAGE | quote }} @@ -156,4 +175,4 @@ spec: {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/k8s-deploy/lightrag-minimal/templates/secret.yaml b/k8s-deploy/lightrag-minimal/templates/secret.yaml index a8349dd9..58b1c774 100644 --- a/k8s-deploy/lightrag-minimal/templates/secret.yaml +++ b/k8s-deploy/lightrag-minimal/templates/secret.yaml @@ -9,4 +9,11 @@ data: openai-api-key: {{ .Values.secrets.openaiApiKey | b64enc | quote }} llm-api-key: {{ .Values.secrets.llmApiKey | b64enc | quote }} embedding-api-key: {{ .Values.secrets.embeddingApiKey | b64enc | quote }} - postgres-password: {{ .Values.postgresql.auth.password | b64enc | quote }} \ No newline at end of file + lightrag-api-key: {{ .Values.secrets.lightragApiKey | default "" | b64enc | quote }} + postgres-password: {{ .Values.postgresql.auth.password | b64enc | quote }} + {{- if and .Values.auth.enabled (gt (len .Values.auth.accounts) 0) }} + auth-accounts: {{ include "lightrag-minimal.authAccounts" . | b64enc | quote }} + {{- end }} + {{- if and .Values.auth.enabled .Values.auth.tokenSecret }} + token-secret: {{ .Values.auth.tokenSecret | b64enc | quote }} + {{- end }} diff --git a/k8s-deploy/lightrag-minimal/values-dev.yaml b/k8s-deploy/lightrag-minimal/values-dev.yaml deleted file mode 100644 index a5e5f7bd..00000000 --- a/k8s-deploy/lightrag-minimal/values-dev.yaml +++ /dev/null @@ -1,75 +0,0 @@ -# Development/Minikube Values -# Optimized for local development with reduced resource requirements - -# Environment configuration -env: - LLM_MODEL: "gpt-4o" - WEBUI_TITLE: "Apolo Copilot - LightRAG (Development)" - WEBUI_DESCRIPTION: "Development LightRAG for Apolo Documentation" - - # OpenAI API Configuration - LLM_BINDING: "openai" - LLM_BINDING_HOST: "https://api.openai.com/v1" - EMBEDDING_BINDING: "openai" - EMBEDDING_BINDING_HOST: "https://api.openai.com/v1" - EMBEDDING_MODEL: "text-embedding-ada-002" - EMBEDDING_DIM: "1536" - - # Concurrency settings (conservative for API stability) - MAX_ASYNC: "4" - MAX_PARALLEL_INSERT: "2" - - # LLM Configuration - ENABLE_LLM_CACHE: "true" - ENABLE_LLM_CACHE_FOR_EXTRACT: "true" - TIMEOUT: "240" - TEMPERATURE: "0" - MAX_TOKENS: "32768" - -# Reduced resources for local development -resources: - limits: - cpu: 1000m - memory: 2Gi - requests: - cpu: 250m - memory: 512Mi - -# Smaller storage for development -persistence: - ragStorage: - size: 5Gi - inputs: - size: 2Gi - -# PostgreSQL with reduced resources -postgresql: - # Use pgvector image for vector support - image: - registry: docker.io - repository: pgvector/pgvector - tag: pg16 - auth: - password: "dev-lightrag-pass" - primary: - persistence: - size: 5Gi - resources: - limits: - cpu: 500m - memory: 1Gi - requests: - cpu: 100m - memory: 256Mi - -# OpenAI API key (set via environment variable) -secrets: - openaiApiKey: "${OPENAI_API_KEY}" - -# Disable ingress for local development (use port-forward) -ingress: - enabled: false - -# Disable autoscaling for development -autoscaling: - enabled: false \ No newline at end of file diff --git a/k8s-deploy/lightrag-minimal/values-prod.yaml b/k8s-deploy/lightrag-minimal/values-prod.yaml deleted file mode 100644 index 92c9eb3f..00000000 --- a/k8s-deploy/lightrag-minimal/values-prod.yaml +++ /dev/null @@ -1,120 +0,0 @@ -# Production Values -# Optimized for production with HA, scaling, and monitoring - -# Environment configuration -env: - LLM_MODEL: "gpt-4o" - WEBUI_TITLE: "Apolo Copilot - LightRAG" - WEBUI_DESCRIPTION: "Production LightRAG for Apolo Documentation" - - # OpenAI API Configuration - LLM_BINDING: "openai" - LLM_BINDING_HOST: "https://api.openai.com/v1" - EMBEDDING_BINDING: "openai" - EMBEDDING_BINDING_HOST: "https://api.openai.com/v1" - EMBEDDING_MODEL: "text-embedding-ada-002" - EMBEDDING_DIM: "1536" - - # Concurrency settings (conservative for API stability) - MAX_ASYNC: "4" - MAX_PARALLEL_INSERT: "2" - - # LLM Configuration - ENABLE_LLM_CACHE: "true" - ENABLE_LLM_CACHE_FOR_EXTRACT: "true" - TIMEOUT: "240" - TEMPERATURE: "0" - MAX_TOKENS: "32768" - -# Production resources -resources: - limits: - cpu: 4000m - memory: 8Gi - requests: - cpu: 1000m - memory: 2Gi - -# Production storage with fast storage class -persistence: - ragStorage: - size: 100Gi - storageClass: "fast-ssd" # Adjust for your cluster - inputs: - size: 50Gi - storageClass: "fast-ssd" - -# PostgreSQL with production resources -postgresql: - # Use pgvector image for vector support - image: - registry: docker.io - repository: pgvector/pgvector - tag: pg16 - auth: - password: "secure-production-password" # Use external secret in real production - primary: - persistence: - size: 200Gi - storageClass: "fast-ssd" - resources: - limits: - cpu: 2000m - memory: 4Gi - requests: - cpu: 500m - memory: 1Gi - -# OpenAI API key (use external secret manager in production) -secrets: - openaiApiKey: "${OPENAI_API_KEY}" - -# Enable ingress for production -ingress: - enabled: true - className: "nginx" - annotations: - cert-manager.io/cluster-issuer: "letsencrypt-prod" - nginx.ingress.kubernetes.io/proxy-body-size: "100m" - nginx.ingress.kubernetes.io/ssl-redirect: "true" - hosts: - - host: lightrag.yourdomain.com - paths: - - path: / - pathType: Prefix - tls: - - secretName: lightrag-tls - hosts: - - lightrag.yourdomain.com - -# Enable autoscaling for production -autoscaling: - enabled: true - minReplicas: 2 - maxReplicas: 10 - targetCPUUtilizationPercentage: 70 - targetMemoryUtilizationPercentage: 80 - -# Production security context -securityContext: - runAsNonRoot: true - runAsUser: 1000 - fsGroup: 1000 - -podSecurityContext: - seccompProfile: - type: RuntimeDefault - -# Node affinity for production workloads -affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: In - values: - - lightrag-minimal - topologyKey: kubernetes.io/hostname \ No newline at end of file diff --git a/k8s-deploy/lightrag-minimal/values.yaml b/k8s-deploy/lightrag-minimal/values.yaml index 28b34c4e..d4c60b0c 100644 --- a/k8s-deploy/lightrag-minimal/values.yaml +++ b/k8s-deploy/lightrag-minimal/values.yaml @@ -60,7 +60,10 @@ postgresql: auth: database: lightrag username: lightrag_user - password: lightrag_pass + password: "" + existingSecret: "" + secretKeys: + userPasswordKey: postgres-password primary: persistence: enabled: true @@ -132,7 +135,13 @@ secrets: openaiApiKey: "" # Legacy field, kept for backward compatibility llmApiKey: "" # API key for LLM service (e.g., OpenRouter) embeddingApiKey: "" # API key for embedding service (e.g., Google Gemini) - + lightragApiKey: "" + +auth: + enabled: false + accounts: [] + tokenSecret: "" + # Node selector and affinity nodeSelector: {} diff --git a/load_docs.py b/load_docs.py index dfc6601e..fd41959f 100755 --- a/load_docs.py +++ b/load_docs.py @@ -8,22 +8,27 @@ import asyncio import httpx import argparse import sys +import os from pathlib import Path -from typing import List, Optional +from typing import Dict, List, Optional async def load_document_to_lightrag( content: str, title: str, doc_url: str, - endpoint: str = "http://localhost:9621" + endpoint: str = "http://localhost:9621", + headers: Optional[Dict[str, str]] = None ) -> bool: """Load a single document to LightRAG with URL reference""" try: async with httpx.AsyncClient(timeout=30.0) as client: + request_headers = {"Content-Type": "application/json"} + if headers: + request_headers.update(headers) response = await client.post( f"{endpoint}/documents/text", - headers={"Content-Type": "application/json"}, + headers=request_headers, json={ "text": content, "file_source": doc_url @@ -148,11 +153,14 @@ Source: {source_info} return documents -async def test_lightrag_health(endpoint: str = "http://localhost:9621") -> bool: +async def test_lightrag_health( + endpoint: str = "http://localhost:9621", + headers: Optional[Dict[str, str]] = None +) -> bool: """Test if LightRAG is accessible""" try: async with httpx.AsyncClient(timeout=10.0) as client: - response = await client.get(f"{endpoint}/health") + response = await client.get(f"{endpoint}/health", headers=headers) if response.status_code == 200: health_data = response.json() print(f"โœ… LightRAG is healthy: {health_data.get('status')}") @@ -165,14 +173,20 @@ async def test_lightrag_health(endpoint: str = "http://localhost:9621") -> bool: return False -async def test_query(endpoint: str = "http://localhost:9621") -> None: +async def test_query( + endpoint: str = "http://localhost:9621", + headers: Optional[Dict[str, str]] = None +) -> None: """Test a sample query""" print(f"\n๐Ÿงช Testing query...") try: async with httpx.AsyncClient(timeout=30.0) as client: + request_headers = {"Content-Type": "application/json"} + if headers: + request_headers.update(headers) response = await client.post( f"{endpoint}/query", - headers={"Content-Type": "application/json"}, + headers=request_headers, json={"query": "What is this documentation about?", "mode": "local"} ) @@ -247,6 +261,12 @@ Examples: ) args = parser.parse_args() + api_key = os.getenv("LIGHTRAG_API_KEY") + if api_key: + auth_headers = {"X-API-Key": api_key} + else: + auth_headers = None + print("โ„น๏ธ LIGHTRAG_API_KEY not set, continuing without authentication.") print("๐Ÿš€ Loading Documentation into LightRAG") print("=" * 60) @@ -262,7 +282,7 @@ Examples: print() # Test LightRAG connectivity - if not await test_lightrag_health(args.endpoint): + if not await test_lightrag_health(args.endpoint, headers=auth_headers): print("โŒ Cannot connect to LightRAG. Please ensure it's running and accessible.") sys.exit(1) @@ -292,7 +312,13 @@ Examples: print(f"\n๐Ÿ”„ Starting to load documents...") for i, (content, title, doc_url) in enumerate(documents): - success = await load_document_to_lightrag(content, title, doc_url, args.endpoint) + success = await load_document_to_lightrag( + content, + title, + doc_url, + args.endpoint, + headers=auth_headers + ) if success: successful += 1 @@ -312,8 +338,8 @@ Examples: # Test query unless disabled if not args.no_test and successful > 0: - await test_query(args.endpoint) + await test_query(args.endpoint, headers=auth_headers) if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main())