diff --git a/infra/qlever/deploy-to-k8s.sh b/infra/qlever/deploy-to-k8s.sh new file mode 100644 index 0000000..49e9bc6 --- /dev/null +++ b/infra/qlever/deploy-to-k8s.sh @@ -0,0 +1,284 @@ +#!/bin/bash +set -euo pipefail + +######################################### +# QLever Kubernetes Deployment Script +######################################### +# +# This script automates the deployment of QLever to Kubernetes +# with Caddy Ingress Controller and Calico Network Policies +# +# Usage: +# ./deploy.sh install # Full installation (Caddy + QLever) +# ./deploy.sh qlever # Deploy QLever only +# ./deploy.sh caddy # Deploy Caddy only +# ./deploy.sh status # Check deployment status +# ./deploy.sh logs # Follow QLever logs +# ./deploy.sh shell # Open shell in QLever pod +# ./deploy.sh delete # Delete QLever resources +# ./deploy.sh uninstall # Delete everything (Caddy + QLever) +# + +# Configuration +NAMESPACE="qlever" +CADDY_NAMESPACE="caddy-system" +DOMAIN="${QLEVER_DOMAIN:-qlever.example.com}" +EMAIL="${QLEVER_EMAIL:-your-email@example.com}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Helper functions +info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" + exit 1 +} + +check_requirements() { + info "Checking requirements..." + + if ! command -v kubectl &> /dev/null; then + error "kubectl not found. Please install kubectl." + fi + + if ! command -v helm &> /dev/null; then + warn "helm not found. Caddy installation will be skipped." + fi + + if ! kubectl cluster-info &> /dev/null; then + error "Cannot connect to Kubernetes cluster. Check your kubeconfig." + fi + + info "✓ Requirements satisfied" +} + +install_caddy() { + info "Installing Caddy Ingress Controller..." + + if ! command -v helm &> /dev/null; then + error "helm is required to install Caddy. Please install helm first." + fi + + # Check if values file exists + if [ ! -f "k8s/helm/caddy-ingress-values.yaml" ]; then + error "k8s/helm/caddy-ingress-values.yaml not found" + fi + + # Update email in values file + if [ "$EMAIL" != "your-email@example.com" ]; then + info "Configuring email: $EMAIL" + sed -i.bak "s/your-email@example.com/$EMAIL/" k8s/helm/caddy-ingress-values.yaml + else + warn "Using default email. Set QLEVER_EMAIL environment variable to change." + fi + + # Create namespace + kubectl create namespace $CADDY_NAMESPACE --dry-run=client -o yaml | kubectl apply -f - + kubectl label namespace $CADDY_NAMESPACE name=$CADDY_NAMESPACE --overwrite + + # Install Caddy via Helm + helm upgrade --install \ + --namespace $CADDY_NAMESPACE \ + --repo https://caddyserver.github.io/ingress/ \ + mycaddy \ + caddy-ingress-controller \ + -f k8s/helm/caddy-ingress-values.yaml + + info "✓ Caddy Ingress Controller installed" + info "Waiting for LoadBalancer IP..." + + # Wait for LoadBalancer IP + kubectl wait --for=condition=Ready pod \ + -l app.kubernetes.io/name=caddy-ingress-controller \ + -n $CADDY_NAMESPACE \ + --timeout=120s || warn "Caddy pods not ready yet" + + LB_IP=$(kubectl get svc -n $CADDY_NAMESPACE -o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "pending") + if [ "$LB_IP" != "pending" ] && [ -n "$LB_IP" ]; then + info "✓ LoadBalancer IP: $LB_IP" + info "Configure DNS: $DOMAIN -> $LB_IP" + else + warn "LoadBalancer IP not assigned yet. Check with: kubectl get svc -n $CADDY_NAMESPACE" + fi +} + +generate_access_token() { + info "Generating secure access token..." + + # Generate random token + TOKEN=$(openssl rand -base64 32) + + # Update secrets file + sed -i.bak "s/CHANGE-ME-TO-SECURE-TOKEN/$TOKEN/" base/01-secrets.yaml + + info "✓ Access token generated and saved to base/01-secrets.yaml" + info "Token (save this): $TOKEN" +} + +configure_domain() { + info "Configuring domain: $DOMAIN" + + # Update ingress file + sed -i.bak "s/qlever.example.com/$DOMAIN/g" base/06-ingress.yaml + + info "✓ Domain configured in base/06-ingress.yaml" +} + +deploy_qlever() { + info "Deploying QLever..." + + # Check if secrets have been configured + if grep -q "CHANGE-ME-TO-SECURE-TOKEN" base/01-secrets.yaml; then + warn "Access token not configured. Generating one..." + generate_access_token + fi + + # Check if domain has been configured + if grep -q "qlever.example.com" base/06-ingress.yaml && [ "$DOMAIN" != "qlever.example.com" ]; then + configure_domain + fi + + # Apply all resources + kubectl apply -k base/ + + info "✓ QLever deployed" + info "Waiting for pods to be ready..." + + kubectl wait --for=condition=Ready pod \ + -l app=qlever-server \ + -n $NAMESPACE \ + --timeout=600s || warn "QLever pod not ready yet (may still be indexing)" + + info "✓ Deployment complete" +} + +show_status() { + info "Checking deployment status..." + + echo "" + echo "=== Caddy Ingress Controller ===" + kubectl get pods,svc -n $CADDY_NAMESPACE + + echo "" + echo "=== QLever ===" + kubectl get all -n $NAMESPACE + + echo "" + echo "=== Ingress ===" + kubectl get ingress -n $NAMESPACE + + echo "" + echo "=== Network Policies ===" + kubectl get networkpolicies -n $NAMESPACE + + echo "" + info "Access QLever at: https://$DOMAIN" +} + +show_logs() { + info "Tailing QLever logs..." + kubectl logs -n $NAMESPACE -l app=qlever-server -f --tail=100 +} + +open_shell() { + info "Opening shell in QLever pod..." + kubectl exec -it -n $NAMESPACE deployment/qlever-server -- /bin/bash +} + +delete_qlever() { + warn "Deleting QLever resources..." + read -p "Are you sure? (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + kubectl delete -k base/ + info "✓ QLever deleted" + else + info "Cancelled" + fi +} + +uninstall_all() { + warn "Uninstalling everything (Caddy + QLever)..." + read -p "Are you sure? This will delete all resources! (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + # Delete QLever + kubectl delete -k base/ || true + + # Delete Caddy + if command -v helm &> /dev/null; then + helm uninstall mycaddy -n $CADDY_NAMESPACE || true + fi + kubectl delete namespace $CADDY_NAMESPACE || true + + info "✓ Everything uninstalled" + else + info "Cancelled" + fi +} + +# Main command handler +case "${1:-help}" in + install) + check_requirements + install_caddy + deploy_qlever + show_status + ;; + caddy) + check_requirements + install_caddy + ;; + qlever) + check_requirements + deploy_qlever + show_status + ;; + status) + show_status + ;; + logs) + show_logs + ;; + shell) + open_shell + ;; + delete) + delete_qlever + ;; + uninstall) + uninstall_all + ;; + help|*) + echo "Usage: $0 {install|caddy|qlever|status|logs|shell|delete|uninstall}" + echo "" + echo "Commands:" + echo " install - Install Caddy + QLever (full setup)" + echo " caddy - Install Caddy Ingress Controller only" + echo " qlever - Deploy QLever only" + echo " status - Show deployment status" + echo " logs - Tail QLever logs" + echo " shell - Open shell in QLever pod" + echo " delete - Delete QLever resources" + echo " uninstall - Delete everything (Caddy + QLever)" + echo "" + echo "Environment variables:" + echo " QLEVER_DOMAIN - Domain for Ingress (default: qlever.example.com)" + echo " QLEVER_EMAIL - Email for Let's Encrypt (default: your-email@example.com)" + echo "" + echo "Example:" + echo " QLEVER_DOMAIN=sparql.mycompany.com QLEVER_EMAIL=admin@mycompany.com ./deploy.sh install" + exit 0 + ;; +esac \ No newline at end of file diff --git a/infra/qlever/k8s/00-namespace.yaml b/infra/qlever/k8s/00-namespace.yaml new file mode 100644 index 0000000..ed1f364 --- /dev/null +++ b/infra/qlever/k8s/00-namespace.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: qlever + labels: + name: qlever + app.kubernetes.io/name: qlever + app.kubernetes.io/component: sparql-database \ No newline at end of file diff --git a/infra/qlever/k8s/01-secrets.yaml b/infra/qlever/k8s/01-secrets.yaml new file mode 100644 index 0000000..b97681b --- /dev/null +++ b/infra/qlever/k8s/01-secrets.yaml @@ -0,0 +1,25 @@ +--- +# QLever Secrets +# +# IMPORTANT: Generate a secure access token before applying: +# +# TOKEN=$(openssl rand -base64 32) +# kubectl create secret generic qlever-secrets \ +# --namespace=qlever \ +# --from-literal=QLEVER_SERVER_ACCESS_TOKEN="$TOKEN" \ +# --dry-run=client -o yaml | kubectl apply -f - +# +# Or use this template with your token: + +apiVersion: v1 +kind: Secret +metadata: + name: qlever-secrets + namespace: qlever + labels: + app.kubernetes.io/name: qlever + app.kubernetes.io/component: sparql-database +type: Opaque +stringData: + # Replace with a secure token generated via: openssl rand -base64 32 + QLEVER_SERVER_ACCESS_TOKEN: "CHANGE-ME-TO-SECURE-TOKEN" \ No newline at end of file diff --git a/infra/qlever/k8s/02-configmaps.yaml b/infra/qlever/k8s/02-configmaps.yaml new file mode 100644 index 0000000..766c109 --- /dev/null +++ b/infra/qlever/k8s/02-configmaps.yaml @@ -0,0 +1,286 @@ +--- +# QLever Configuration (non-sensitive) +apiVersion: v1 +kind: ConfigMap +metadata: + name: qlever-config + namespace: qlever + labels: + app.kubernetes.io/name: qlever + app.kubernetes.io/component: sparql-database +data: + # Config file generation + QLEVER_GENERATE_CONFIG_FILE: "true" + + # [data] + QLEVER_DATA_NAME: "local" + QLEVER_DATA_DESCRIPTION: "Local dataset for QLever" + + # [index] + QLEVER_INDEX_INPUT_FILES: "../input/**/*.ttl" + QLEVER_INDEX_CAT_INPUT_FILES: "cat ../input/**/*.ttl" + QLEVER_INDEX_PARALLEL_PARSING: "false" + QLEVER_INDEX_SETTINGS_JSON: '{ "ascii-prefixes-only": false, "num-triples-per-batch": 100000 }' + + # [server] + QLEVER_SERVER_HOST_NAME: "0.0.0.0" + QLEVER_SERVER_PORT: "7001" + QLEVER_SERVER_MEMORY_FOR_QUERIES: "8G" + QLEVER_SERVER_CACHE_MAX_SIZE: "4G" + QLEVER_SERVER_TIMEOUT: "300s" + + # [runtime] + QLEVER_RUNTIME_SYSTEM: "native" + + # Entrypoint behavior + SHOULD_DOWNLOAD: "false" + FORCE_DOWNLOAD: "false" + SHOULD_INDEX: "true" + FORCE_INDEXING: "false" + STOP_ON_CALL_ENABLED: "false" + START_ADDITIONAL_ARGS: "" + + # User/Group IDs (used by init container) + UID: "1000" + GID: "1000" + +--- +# QLever Scripts (read-only) +apiVersion: v1 +kind: ConfigMap +metadata: + name: qlever-scripts + namespace: qlever + labels: + app.kubernetes.io/name: qlever + app.kubernetes.io/component: sparql-database +data: + generate-qleverfile.sh: | + #!/bin/sh + set -eu + + ################################################################################## + # This script generates a Qleverfile that can be used to start a QLever instance # + ################################################################################## + + QLEVER_FILE_PATH="${QLEVER_FILE_PATH:-/data/Qleverfile}" + + # Have a way to opt-out of generating the Qleverfile + QLEVER_GENERATE_CONFIG_FILE="${QLEVER_GENERATE_CONFIG_FILE:-true}" + if [ "${QLEVER_GENERATE_CONFIG_FILE}" = "auto" ]; then + # Check if the Qleverfile already exists + if [ -f "${QLEVER_FILE_PATH}" ]; then + echo "INFO: Skipping Qleverfile generation, as 'QLEVER_GENERATE_CONFIG_FILE' is set to 'auto' and the file already exists at '${QLEVER_FILE_PATH}'" + exit 0 + else + echo "INFO: Generating Qleverfile, as 'QLEVER_GENERATE_CONFIG_FILE' is set to 'auto' and the file does not exist at '${QLEVER_FILE_PATH}'" + QLEVER_GENERATE_CONFIG_FILE="true" + fi + fi + + if [ "${QLEVER_GENERATE_CONFIG_FILE}" != "true" ]; then + echo "INFO: Skipping Qleverfile generation, as 'QLEVER_GENERATE_CONFIG_FILE' is not set to 'true'" + exit 0 + fi + + dirname "${QLEVER_FILE_PATH}" | xargs mkdir -p + + # Set default values for some configuration fields (could be overridden by other environment variables) + export QLEVER_DATA_NAME="${QLEVER_DATA_NAME:-default}" + export QLEVER_DATA_DESCRIPTION="${QLEVER_DATA_DESCRIPTION:-Default dataset}" + + export QLEVER_INDEX_SETTINGS_JSON="${QLEVER_INDEX_SETTINGS_JSON:-{ \"ascii-prefixes-only\": false, \"num-triples-per-batch\": 100000 }}" + export QLEVER_INDEX_PARALLEL_PARSING="${QLEVER_INDEX_PARALLEL_PARSING:-true}" + + export QLEVER_SERVER_ACCESS_TOKEN="${QLEVER_SERVER_ACCESS_TOKEN:-${QLEVER_DATA_NAME}_7643543846_Zs6nw7yi3Z9m}" + export QLEVER_SERVER_HOST_NAME="${QLEVER_SERVER_HOST_NAME:-127.0.0.1}" + export QLEVER_SERVER_PORT="${QLEVER_SERVER_PORT:-7001}" + export QLEVER_SERVER_MEMORY_FOR_QUERIES="${QLEVER_SERVER_MEMORY_FOR_QUERIES:-5G}" + export QLEVER_SERVER_CACHE_MAX_SIZE="${QLEVER_SERVER_CACHE_MAX_SIZE:-2G}" + export QLEVER_SERVER_TIMEOUT="${QLEVER_SERVER_TIMEOUT:-30s}" + + export QLEVER_RUNTIME_SYSTEM="${QLEVER_RUNTIME_SYSTEM:-native}" + + export QLEVER_UI_UI_CONFIG="${QLEVER_UI_UI_CONFIG:-default}" + export QLEVER_UI_UI_PORT="${QLEVER_UI_UI_PORT:-7002}" + + # Extract all environment variables that start with 'QLEVER_', as we ignore all other variables + QLEVER_ENV_VARS="$(env | grep '^QLEVER_' | sort -u)" + + # Generate a specific section for the Qleverfile (corrected, POSIX-safe) + generate_config_section () { + section="$1" + section_upper="$(echo "${section}" | tr '[:lower:]' '[:upper:]')" + + # Extract all environment variables for the given section + section_env_vars="$(echo "${QLEVER_ENV_VARS}" | grep "^QLEVER_${section_upper}_" | sed "s/^QLEVER_${section_upper}_//")" + + # Skip empty sections + if [ -z "${section_env_vars}" ]; then + return + fi + + echo "[$section]" + echo "${section_env_vars}" + echo "" + } + + # Generate the Qleverfile, with the different sections + { + echo "# Qleverfile auto-generated using environment variables at $(date), while starting the container" + echo "" + + generate_config_section "data" + generate_config_section "index" + generate_config_section "server" + generate_config_section "runtime" + generate_config_section "ui" + } > "${QLEVER_FILE_PATH}" || exit 1 + + echo "INFO: Generated Qleverfile at '${QLEVER_FILE_PATH}'" + + exit 0 + + entrypoint.sh: | + #!/bin/bash + set -euo pipefail + + # Tweak indexing + SHOULD_INDEX="${SHOULD_INDEX:-false}" + FORCE_INDEXING="${FORCE_INDEXING:-false}" + + # Tweak data download + SHOULD_DOWNLOAD="${SHOULD_DOWNLOAD:-true}" + FORCE_DOWNLOAD="${FORCE_DOWNLOAD:-false}" + + # Additional parameters to qlever start (e.g. --kill-existing-with-same-port) + START_ADDITIONAL_ARGS="${START_ADDITIONAL_ARGS:-}" + STOP_ON_CALL_ENABLED="${STOP_ON_CALL_ENABLED:-false}" + + # Display some debug information + echo "INFO: Indexing : should index = ${SHOULD_INDEX} ; force indexing = ${FORCE_INDEXING}" + echo "INFO: Data download : should download = ${SHOULD_DOWNLOAD} ; force download = ${FORCE_DOWNLOAD}" + + # Wait briefly for /data to become writable (named volumes are often root-owned at first). + echo "UID=$(id -u)" + echo "GID=$(id -g)" + ls -al / + + # Generate Qleverfile (or skip, depending on QLEVER_GENERATE_CONFIG_FILE) + /bin/sh /qlever/scripts/generate-qleverfile.sh + + # Go to the data directory + cd /data + + QLEVER_FILE_PATH="${QLEVER_FILE_PATH:-/data/Qleverfile}" + + # Check if the Qleverfile exists + if [ ! -f "${QLEVER_FILE_PATH}" ]; then + echo "ERROR: Qleverfile not found at '${QLEVER_FILE_PATH}'" + exit 1 + fi + + # Display the Qleverfile for debugging + echo "INFO: Qleverfile found at '${QLEVER_FILE_PATH}'" + cat "${QLEVER_FILE_PATH}" + + # Extract INPUT_FILES from the Qleverfile (if any) + INPUT_FILES=$(grep "^INPUT_FILES[[:space:]]*=" "${QLEVER_FILE_PATH}" | head -n1 | sed 's/.*=[[:space:]]*//') + HAS_INPUT_FILES=$(echo "${INPUT_FILES}" | sed '/^[[:space:]]*$/d' | wc -l) + + if [ "${HAS_INPUT_FILES}" -ne 0 ]; then + echo "INFO: Found 'INPUT_FILES' in the Qleverfile" + + # Check if the input files already exist + HAS_MISSING_INPUT_FILES="false" + for INPUT_FILE in ${INPUT_FILES}; do + if [ -f "${INPUT_FILE}" ]; then + echo "INFO: Input file found at '${INPUT_FILE}'" + else + echo "INFO: Input file not found at '${INPUT_FILE}'" + HAS_MISSING_INPUT_FILES="true" + fi + done + + # If all files are present, skip the download + if [ "${HAS_MISSING_INPUT_FILES}" = "false" ]; then + SHOULD_DOWNLOAD="false" # As the input files are already present, no need to download them + fi + + # Display the info in the logs only if the download is enabled + if [ "${SHOULD_DOWNLOAD}" = "true" ]; then + echo "INFO: Trigger download of input files…" + fi + fi + + # If the download of the input files is forced, then download them in all cases + if [ "${FORCE_DOWNLOAD}" = "true" ]; then + echo "INFO: Forcing download of input files…" + SHOULD_DOWNLOAD="true" + fi + + # Check if there is a line that starts with `GET_DATA_CMD` in the Qleverfile + if grep -q "^GET_DATA_CMD" "${QLEVER_FILE_PATH}"; then + echo "INFO: Found 'GET_DATA_CMD' in the Qleverfile" + if [ "${SHOULD_DOWNLOAD}" = "true" ]; then + echo "INFO: Trigger download of data…" + qlever get-data + SHOULD_INDEX="true" # As the data is downloaded, we should index it + else + echo "INFO: Skipping download of data…" + fi + fi + + # Indexing logic + if [ "${SHOULD_INDEX}" = "true" ]; then + echo "INFO: Indexing is enabled" + qlever index --overwrite-existing + elif [ "${FORCE_INDEXING}" = "true" ]; then + echo "INFO: Forcing indexing" + qlever index --overwrite-existing + else + echo "INFO: Indexing is disabled" + fi + + # Start the QLever server + echo "INFO: Starting QLever server..." + if [ "${STOP_ON_CALL_ENABLED}" = "true" ]; then + + # Start QLever in the background + qlever start --run-in-foreground $START_ADDITIONAL_ARGS & + QLEVER_PID=$! + + # Start stop_on_call in the background + stop_on_call & + STOP_ON_CALL_PID=$! + + # Wait for either QLever or stop_on_call to exit + wait -n $QLEVER_PID $STOP_ON_CALL_PID + EXITED_PID=$? + + # Check which one exited + if ! kill -0 $QLEVER_PID 2>/dev/null; then + # QLever exited + wait $QLEVER_PID + QLEVER_EXIT_CODE=$? + echo "qlever exited with code $QLEVER_EXIT_CODE" + kill $STOP_ON_CALL_PID 2>/dev/null + exit $QLEVER_EXIT_CODE + else + # stop_on_call was called, so we stop QLever + echo "" + echo "" + echo "" + echo "[INFO] Stopped using stop_on_call" + qlever stop + + # Stop QLever manually, in case it is still running (it should not be, but just in case) + kill $QLEVER_PID 2>/dev/null || true + wait $QLEVER_PID || true + exit 0 + fi + + else + # Normal case: keep QLever in the foreground, container stays alive + exec qlever start --run-in-foreground $START_ADDITIONAL_ARGS + fi \ No newline at end of file diff --git a/infra/qlever/k8s/03-storage.yaml b/infra/qlever/k8s/03-storage.yaml new file mode 100644 index 0000000..ad6965b --- /dev/null +++ b/infra/qlever/k8s/03-storage.yaml @@ -0,0 +1,18 @@ +--- +# Persistent volume for /data (Qleverfile + index artifacts) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: qlever-data-pvc + namespace: qlever + labels: + app.kubernetes.io/name: qlever + app.kubernetes.io/component: sparql-database +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Gi + # Optional: Uncomment to pin to specific storage class + # storageClassName: standard \ No newline at end of file diff --git a/infra/qlever/k8s/04-deployment.yaml b/infra/qlever/k8s/04-deployment.yaml new file mode 100644 index 0000000..3b52609 --- /dev/null +++ b/infra/qlever/k8s/04-deployment.yaml @@ -0,0 +1,197 @@ +--- +# QLever Deployment with Production Security Best Practices +apiVersion: apps/v1 +kind: Deployment +metadata: + name: qlever-server + namespace: qlever + labels: + app.kubernetes.io/name: qlever + app.kubernetes.io/component: sparql-database + app.kubernetes.io/version: "latest" +spec: + replicas: 1 + strategy: + type: Recreate # Single replica, avoid two pods fighting over ReadWriteOnce PVC + selector: + matchLabels: + app: qlever-server + template: + metadata: + labels: + app: qlever-server + app.kubernetes.io/name: qlever + app.kubernetes.io/component: sparql-database + spec: + # Graceful shutdown + terminationGracePeriodSeconds: 120 + + # Pod-level security context + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + + # Init container to fix volume permissions + initContainers: + - name: fix-permissions + image: busybox:latest + command: + - sh + - -c + - | + echo "Fixing /data ownership to 1000:1000" + chown -R 1000:1000 /data + chmod -R u+rwX,g+rwX,o+rX /data + echo "Permissions fixed successfully" + securityContext: + runAsUser: 0 + runAsGroup: 0 + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "200m" + memory: "128Mi" + + containers: + - name: qlever-server + image: adfreiburg/qlever:latest + imagePullPolicy: IfNotPresent + + # Container-level security context + # Best practice: read-only root filesystem + explicit writable volumes + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true # Immutable infrastructure + capabilities: + drop: + - ALL + + workingDir: /data + + # Custom entrypoint from scripts ConfigMap + command: ["/bin/bash", "/qlever/scripts/entrypoint.sh"] + + # Environment from ConfigMap and Secret + envFrom: + - configMapRef: + name: qlever-config + - secretRef: + name: qlever-secrets + + ports: + - name: http + containerPort: 7001 + protocol: TCP + + # Volume mounts + volumeMounts: + # Data volume - READ/WRITE (database writes here) + - name: data + mountPath: /data + readOnly: false + + # Scripts from ConfigMap - READ-ONLY + - name: scripts + mountPath: /qlever/scripts + readOnly: true + + # Writable temp directory (required with readOnlyRootFilesystem) + - name: tmp + mountPath: /tmp + + # Writable cache directory + - name: cache + mountPath: /var/cache + + # Resource limits + resources: + requests: + cpu: "2" + memory: "10Gi" + limits: + cpu: "4" + memory: "18Gi" + + # Health probes using SPARQL queries + # Startup probe: Allows up to 30 minutes for initial indexing + startupProbe: + exec: + command: + - /bin/sh + - -c + - | + curl -sf http://localhost:7001 > /dev/null + periodSeconds: 10 + failureThreshold: 180 # 30 minutes (180 * 10s) + timeoutSeconds: 5 + + # Readiness probe: Confirms database is responding to queries + readinessProbe: + exec: + command: + - /bin/sh + - -c + - | + curl -sf http://localhost:7001 \ + -H "Accept: text/tab-separated-values" \ + --data-urlencode 'query=SELECT * WHERE { ?s ?p ?o } LIMIT 1' \ + > /dev/null + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 10 + failureThreshold: 3 + + # Liveness probe: Ensures server stays responsive + livenessProbe: + exec: + command: + - /bin/sh + - -c + - | + curl -sf http://localhost:7001 \ + -H "Accept: text/tab-separated-values" \ + --data-urlencode 'query=SELECT * WHERE { ?s ?p ?o } LIMIT 1' \ + > /dev/null + initialDelaySeconds: 120 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + + # Graceful shutdown + lifecycle: + preStop: + exec: + command: + - /bin/bash + - -c + - | + cd /data + qlever stop || true + + volumes: + # Persistent data volume + - name: data + persistentVolumeClaim: + claimName: qlever-data-pvc + + # Scripts from ConfigMap (read-only) + - name: scripts + configMap: + name: qlever-scripts + defaultMode: 0555 # r-xr-xr-x (read + execute) + + # Ephemeral volumes for writable directories + # (required when readOnlyRootFilesystem: true) + - name: tmp + emptyDir: {} + + - name: cache + emptyDir: {} \ No newline at end of file diff --git a/infra/qlever/k8s/05-service.yaml b/infra/qlever/k8s/05-service.yaml new file mode 100644 index 0000000..5082fdf --- /dev/null +++ b/infra/qlever/k8s/05-service.yaml @@ -0,0 +1,31 @@ +--- +# QLever Service +# ClusterIP by default - access via Ingress +apiVersion: v1 +kind: Service +metadata: + name: qlever-server + namespace: qlever + labels: + app.kubernetes.io/name: qlever + app.kubernetes.io/component: sparql-database +spec: + type: ClusterIP + selector: + app: qlever-server + ports: + - name: http + port: 7001 + targetPort: 7001 + protocol: TCP + + # Optional: Uncomment for external access without Ingress + # type: LoadBalancer + # OR + # type: NodePort + # ports: + # - name: http + # port: 7001 + # targetPort: 7001 + # nodePort: 30701 # 30000-32767 + # protocol: TCP \ No newline at end of file diff --git a/infra/qlever/k8s/06-ingress.yaml b/infra/qlever/k8s/06-ingress.yaml new file mode 100644 index 0000000..4673dc1 --- /dev/null +++ b/infra/qlever/k8s/06-ingress.yaml @@ -0,0 +1,62 @@ +--- +# QLever Ingress with Caddy (Automatic HTTPS via Let's Encrypt) +# +# Prerequisites: +# 1. Install Caddy Ingress Controller (see helm/caddy-ingress-values.yaml) +# 2. Configure DNS to point your domain to the LoadBalancer IP +# 3. Update spec.rules[0].host with your actual domain +# +# Caddy automatically obtains and renews Let's Encrypt certificates + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: qlever-ingress + namespace: qlever + labels: + app.kubernetes.io/name: qlever + app.kubernetes.io/component: sparql-database + annotations: + kubernetes.io/ingress.class: caddy + # Optional: Configure Caddy behavior + # caddy.ingress.kubernetes.io/max-body-size: "100m" +spec: + rules: + - host: qlever.example.com # ⚠️ CHANGE THIS to your domain + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: qlever-server + port: + number: 7001 + + # TLS configuration + # Caddy automatically obtains Let's Encrypt certificates for the hosts + tls: + - hosts: + - qlever.example.com # ⚠️ CHANGE THIS to your domain + # secretName is optional - if omitted, Caddy generates cert automatically + # secretName: qlever-tls # Uncomment to use your own certificate + +# --- +# Optional: Custom TLS certificate +# Uncomment and apply if you want to use your own certificate instead of Let's Encrypt +# +# apiVersion: v1 +# kind: Secret +# metadata: +# name: qlever-tls +# namespace: qlever +# type: kubernetes.io/tls +# stringData: +# tls.crt: | +# -----BEGIN CERTIFICATE----- +# +# -----END CERTIFICATE----- +# tls.key: | +# -----BEGIN PRIVATE KEY----- +# +# -----END PRIVATE KEY----- \ No newline at end of file diff --git a/infra/qlever/k8s/07-network-policies.yaml b/infra/qlever/k8s/07-network-policies.yaml new file mode 100644 index 0000000..79eddc3 --- /dev/null +++ b/infra/qlever/k8s/07-network-policies.yaml @@ -0,0 +1,148 @@ +# Zero-Trust Network Policies for QLever +# +# Architecture: +# 1. Default deny all traffic +# 2. Explicitly allow QLever ingress from Caddy +# 3. Explicitly allow QLever egress for DNS and external data +# +# Best Practice: Start with deny-all, add allow rules incrementally + +--- +# Policy 1: Default Deny All Traffic +# Applied to all pods in qlever namespace +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all + namespace: qlever + labels: + app.kubernetes.io/name: qlever +spec: + podSelector: {} # Applies to all pods + policyTypes: + - Ingress + - Egress + +--- +# Policy 2: Allow Ingress from Caddy Ingress Controller +# QLever must accept traffic from Caddy to serve queries +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-ingress-from-caddy + namespace: qlever + labels: + app.kubernetes.io/name: qlever +spec: + podSelector: + matchLabels: + app: qlever-server + policyTypes: + - Ingress + ingress: + # Allow from Caddy ingress controller pods + - from: + - namespaceSelector: + matchLabels: + name: caddy-system + # Alternatively, match by pod labels if Caddy is in same namespace + # - podSelector: + # matchLabels: + # app: caddy-ingress-controller + ports: + - protocol: TCP + port: 7001 + +--- +# Policy 3: Allow DNS Queries +# QLever needs DNS resolution for external data downloads +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-dns-egress + namespace: qlever + labels: + app.kubernetes.io/name: qlever +spec: + podSelector: + matchLabels: + app: qlever-server + policyTypes: + - Egress + egress: + # Allow DNS queries to kube-dns/coredns + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + - podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + +--- +# Policy 4: Allow External HTTPS for Data Downloads +# QLever may need to download RDF data from external sources +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-external-https + namespace: qlever + labels: + app.kubernetes.io/name: qlever +spec: + podSelector: + matchLabels: + app: qlever-server + policyTypes: + - Egress + egress: + # Allow HTTPS to any external IP + # Restrict this further if you know the specific IPs/CIDRs + - to: + - namespaceSelector: {} + ports: + - protocol: TCP + port: 443 + - protocol: TCP + port: 80 + +# --- +# Optional: Calico GlobalNetworkPolicy for stricter control +# Requires Calico CNI (not standard Kubernetes) +# Uncomment if you have Calico installed +# +# apiVersion: projectcalico.org/v3 +# kind: GlobalNetworkPolicy +# metadata: +# name: qlever-global-deny +# spec: +# # Deny all traffic by default, higher precedence +# order: 100 +# selector: projectcalico.org/namespace == 'qlever' +# types: +# - Ingress +# - Egress +# ingress: [] +# egress: [] + +# --- +# Optional: Allow internal pod-to-pod communication +# Uncomment if you have multiple services in qlever namespace that need to communicate +# +# apiVersion: networking.k8s.io/v1 +# kind: NetworkPolicy +# metadata: +# name: allow-same-namespace +# namespace: qlever +# spec: +# podSelector: {} +# policyTypes: +# - Ingress +# ingress: +# - from: +# - podSelector: {} \ No newline at end of file diff --git a/infra/qlever/k8s/helm/caddy-ingress-value.yaml b/infra/qlever/k8s/helm/caddy-ingress-value.yaml new file mode 100644 index 0000000..b0b7da3 --- /dev/null +++ b/infra/qlever/k8s/helm/caddy-ingress-value.yaml @@ -0,0 +1,114 @@ +# Caddy Ingress Controller - Helm Values +# +# Installation: +# helm install \ +# --namespace caddy-system \ +# --create-namespace \ +# --repo https://caddyserver.github.io/ingress/ \ +# mycaddy \ +# caddy-ingress-controller \ +# -f helm/caddy-ingress-values.yaml +# +# Automatic HTTPS with Let's Encrypt + +ingressController: + # Ingress controller configuration + config: + # ⚠️ REQUIRED: Your email for Let's Encrypt notifications + email: "your-email@example.com" + + # Enable automatic HTTPS (default: true) + # Caddy automatically obtains and renews certificates + autoHttps: true + + # On-demand TLS (generates certs on-the-fly) + # Generally not needed - automatic HTTPS is better + onDemandTLS: false + + # ACME server (default: Let's Encrypt production) + # Use staging for testing: https://acme-staging-v02.api.letsencrypt.org/directory + # acmeServer: "https://acme-v02.api.letsencrypt.org/directory" + + # Debug mode + debug: false + + # Resource limits for ingress controller + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + + # Replica count + replicaCount: 2 + + # Image configuration + image: + repository: ghcr.io/caddyserver/gateway + # tag: latest # Uncomment to pin version + pullPolicy: IfNotPresent + +# LoadBalancer service configuration +loadBalancer: + # Service type + type: LoadBalancer + + # Annotations (cloud provider specific) + annotations: {} + # AWS example: + # service.beta.kubernetes.io/aws-load-balancer-type: "nlb" + # service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing" + + # GCP example: + # cloud.google.com/load-balancer-type: "External" + + # Azure example: + # service.beta.kubernetes.io/azure-load-balancer-resource-group: "my-rg" + # service.beta.kubernetes.io/azure-dns-label-name: "qlever-ingress" + + # External traffic policy + # "Local" preserves source IP but may cause uneven load distribution + # "Cluster" distributes evenly but source IP is lost + externalTrafficPolicy: Cluster + + # LoadBalancer IP (optional - cloud provider assigns if omitted) + # loadBalancerIP: "1.2.3.4" + + # Source IP ranges allowed to access LoadBalancer + # Default: allow all (0.0.0.0/0) + loadBalancerSourceRanges: [] + # - "10.0.0.0/8" # Internal network + # - "203.0.113.0/24" # Office IP range + +# Service configuration +service: + # HTTP port + httpPort: 80 + + # HTTPS port + httpsPort: 443 + +# Pod security context +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +# Container security context +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + capabilities: + drop: + - ALL + +# Node selection (optional) +# nodeSelector: {} +# tolerations: [] +# affinity: {} \ No newline at end of file diff --git a/infra/qlever/k8s/kustomization.yaml b/infra/qlever/k8s/kustomization.yaml new file mode 100644 index 0000000..409c074 --- /dev/null +++ b/infra/qlever/k8s/kustomization.yaml @@ -0,0 +1,58 @@ +--- +# Kustomization for QLever Deployment +# +# Usage: +# kubectl apply -k base/ +# +# This deploys all resources in the correct order + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: qlever + +# Resource files in deployment order +resources: + - 00-namespace.yaml + - 01-secrets.yaml + - 02-configmaps.yaml + - 03-storage.yaml + - 04-deployment.yaml + - 05-service.yaml + - 06-ingress.yaml + - 07-network-policies.yaml + +# Common labels applied to all resources +commonLabels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/part-of: qlever + +# Images (uncomment to override image versions) +# images: +# - name: adfreiburg/qlever +# newTag: v1.0.0 + +# ConfigMap generator (alternative to 02-configmaps.yaml) +# Uncomment if you prefer to manage scripts as local files +# configMapGenerator: +# - name: qlever-scripts +# files: +# - scripts/entrypoint.sh +# - scripts/generate-qleverfile.sh + +# Secret generator (for local development only) +# ⚠️ DO NOT use in production - use sealed secrets or external secret managers +# secretGenerator: +# - name: qlever-secrets +# literals: +# - QLEVER_SERVER_ACCESS_TOKEN=dev-token-replace-in-prod + +# Patches (optional customizations) +# patches: +# - target: +# kind: Deployment +# name: qlever-server +# patch: |- +# - op: replace +# path: /spec/replicas +# value: 2 \ No newline at end of file diff --git a/infra/qlever/qlever-k8s.yaml b/infra/qlever/qlever-k8s.yaml deleted file mode 100644 index 3fc463c..0000000 --- a/infra/qlever/qlever-k8s.yaml +++ /dev/null @@ -1,444 +0,0 @@ ---- -# Namespace (optional - can deploy to default namespace instead) -apiVersion: v1 -kind: Namespace -metadata: - name: qlever - labels: - name: qlever - ---- -# ConfigMap for environment variables -# Translates your qlever.env file into Kubernetes config -apiVersion: v1 -kind: ConfigMap -metadata: - name: qlever-config - namespace: qlever -data: - # User/Group IDs for permission management - UID: "1000" - GID: "1000" - - # QLever configuration generation - QLEVER_GENERATE_CONFIG_FILE: "auto" - QLEVER_FILE_PATH: "/data/Qleverfile" - - # Data configuration - QLEVER_DATA_NAME: "default" - QLEVER_DATA_DESCRIPTION: "Default dataset" - - # Index settings - QLEVER_INDEX_SETTINGS_JSON: '{ "ascii-prefixes-only": false, "num-triples-per-batch": 100000 }' - QLEVER_INDEX_PARALLEL_PARSING: "true" - - # Server settings - QLEVER_SERVER_ACCESS_TOKEN: "default_7643543846_Zs6nw7yi3Z9m" - QLEVER_SERVER_HOST_NAME: "0.0.0.0" # Listen on all interfaces in k8s - QLEVER_SERVER_PORT: "7001" - QLEVER_SERVER_MEMORY_FOR_QUERIES: "5G" - QLEVER_SERVER_CACHE_MAX_SIZE: "2G" - QLEVER_SERVER_TIMEOUT: "30s" - - # Runtime - QLEVER_RUNTIME_SYSTEM: "native" - - # Entrypoint script behavior - SHOULD_INDEX: "false" - FORCE_INDEXING: "false" - SHOULD_DOWNLOAD: "true" - FORCE_DOWNLOAD: "false" - START_ADDITIONAL_ARGS: "" - STOP_ON_CALL_ENABLED: "false" - ---- -# PersistentVolumeClaim for QLever data storage -# This provides persistent storage for index data, Qleverfile, etc. -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: qlever-data-pvc - namespace: qlever -spec: - accessModes: - - ReadWriteOnce # Single pod read-write access - resources: - requests: - storage: 20Gi # Adjust based on your data size - # storageClassName: standard # Uncomment and set your storage class if needed - ---- -# ConfigMap for entrypoint script -# Mounts your custom entrypoint.sh -apiVersion: v1 -kind: ConfigMap -metadata: - name: qlever-scripts - namespace: qlever -data: - entrypoint.sh: | - #!/bin/bash - set -euo pipefail - - # Configuration from environment - SHOULD_INDEX="${SHOULD_INDEX:-false}" - FORCE_INDEXING="${FORCE_INDEXING:-false}" - SHOULD_DOWNLOAD="${SHOULD_DOWNLOAD:-true}" - FORCE_DOWNLOAD="${FORCE_DOWNLOAD:-false}" - START_ADDITIONAL_ARGS="${START_ADDITIONAL_ARGS:-}" - STOP_ON_CALL_ENABLED="${STOP_ON_CALL_ENABLED:-false}" - - echo "INFO: Indexing : should index = ${SHOULD_INDEX} ; force indexing = ${FORCE_INDEXING}" - echo "INFO: Data download : should download = ${SHOULD_DOWNLOAD} ; force download = ${FORCE_DOWNLOAD}" - echo "UID=$(id -u)" - echo "GID=$(id -g)" - - # Generate Qleverfile - /bin/sh /qlever/scripts/generate-qleverfile.sh - - cd /data - - QLEVER_FILE_PATH="${QLEVER_FILE_PATH:-/data/Qleverfile}" - - if [ ! -f "${QLEVER_FILE_PATH}" ]; then - echo "ERROR: Qleverfile not found at '${QLEVER_FILE_PATH}'" - exit 1 - fi - - echo "INFO: Qleverfile found at '${QLEVER_FILE_PATH}'" - cat "${QLEVER_FILE_PATH}" - - # Extract INPUT_FILES from Qleverfile - INPUT_FILES=$(grep "^INPUT_FILES[[:space:]]*=" "${QLEVER_FILE_PATH}" | head -n1 | sed 's/.*=[[:space:]]*//') - HAS_INPUT_FILES=$(echo "${INPUT_FILES}" | sed '/^[[:space:]]*$/d' | wc -l) - - if [ "${HAS_INPUT_FILES}" -ne 0 ]; then - echo "INFO: Found 'INPUT_FILES' in the Qleverfile" - - HAS_MISSING_INPUT_FILES="false" - for INPUT_FILE in ${INPUT_FILES}; do - if [ -f "${INPUT_FILE}" ]; then - echo "INFO: Input file found at '${INPUT_FILE}'" - else - echo "INFO: Input file not found at '${INPUT_FILE}'" - HAS_MISSING_INPUT_FILES="true" - fi - done - - if [ "${HAS_MISSING_INPUT_FILES}" = "false" ]; then - SHOULD_DOWNLOAD="false" - fi - - if [ "${SHOULD_DOWNLOAD}" = "true" ]; then - echo "INFO: Trigger download of input files…" - fi - fi - - if [ "${FORCE_DOWNLOAD}" = "true" ]; then - echo "INFO: Forcing download of input files…" - SHOULD_DOWNLOAD="true" - fi - - if grep -q "^GET_DATA_CMD" "${QLEVER_FILE_PATH}"; then - echo "INFO: Found 'GET_DATA_CMD' in the Qleverfile" - if [ "${SHOULD_DOWNLOAD}" = "true" ]; then - echo "INFO: Trigger download of data…" - qlever get-data - SHOULD_INDEX="true" - else - echo "INFO: Skipping download of data…" - fi - fi - - if [ "${SHOULD_INDEX}" = "true" ]; then - echo "INFO: Indexing is enabled" - qlever index --overwrite-existing - elif [ "${FORCE_INDEXING}" = "true" ]; then - echo "INFO: Forcing indexing" - qlever index --overwrite-existing - else - echo "INFO: Indexing is disabled" - fi - - echo "INFO: Starting QLever server..." - if [ "${STOP_ON_CALL_ENABLED}" = "true" ]; then - qlever start --run-in-foreground $START_ADDITIONAL_ARGS & - QLEVER_PID=$! - - stop_on_call & - STOP_ON_CALL_PID=$! - - wait -n $QLEVER_PID $STOP_ON_CALL_PID - - if ! kill -0 $QLEVER_PID 2>/dev/null; then - wait $QLEVER_PID - QLEVER_EXIT_CODE=$? - echo "qlever exited with code $QLEVER_EXIT_CODE" - kill $STOP_ON_CALL_PID 2>/dev/null - exit $QLEVER_EXIT_CODE - else - echo "[INFO] Stopped using stop_on_call" - qlever stop - kill $QLEVER_PID 2>/dev/null || true - wait $QLEVER_PID || true - exit 0 - fi - else - exec qlever start --run-in-foreground $START_ADDITIONAL_ARGS - fi - - generate-qleverfile.sh: | - #!/bin/sh - set -eu - - QLEVER_FILE_PATH="${QLEVER_FILE_PATH:-/data/Qleverfile}" - QLEVER_GENERATE_CONFIG_FILE="${QLEVER_GENERATE_CONFIG_FILE:-true}" - - if [ "${QLEVER_GENERATE_CONFIG_FILE}" = "auto" ]; then - if [ -f "${QLEVER_FILE_PATH}" ]; then - echo "INFO: Skipping Qleverfile generation, file already exists at '${QLEVER_FILE_PATH}'" - exit 0 - else - echo "INFO: Generating Qleverfile at '${QLEVER_FILE_PATH}'" - QLEVER_GENERATE_CONFIG_FILE="true" - fi - fi - - if [ "${QLEVER_GENERATE_CONFIG_FILE}" != "true" ]; then - echo "INFO: Skipping Qleverfile generation" - exit 0 - fi - - dirname "${QLEVER_FILE_PATH}" | xargs mkdir -p - - export QLEVER_DATA_NAME="${QLEVER_DATA_NAME:-default}" - export QLEVER_DATA_DESCRIPTION="${QLEVER_DATA_DESCRIPTION:-Default dataset}" - export QLEVER_INDEX_SETTINGS_JSON="${QLEVER_INDEX_SETTINGS_JSON:-{ \"ascii-prefixes-only\": false, \"num-triples-per-batch\": 100000 }}" - export QLEVER_INDEX_PARALLEL_PARSING="${QLEVER_INDEX_PARALLEL_PARSING:-true}" - export QLEVER_SERVER_ACCESS_TOKEN="${QLEVER_SERVER_ACCESS_TOKEN:-${QLEVER_DATA_NAME}_7643543846_Zs6nw7yi3Z9m}" - export QLEVER_SERVER_HOST_NAME="${QLEVER_SERVER_HOST_NAME:-127.0.0.1}" - export QLEVER_SERVER_PORT="${QLEVER_SERVER_PORT:-7001}" - export QLEVER_SERVER_MEMORY_FOR_QUERIES="${QLEVER_SERVER_MEMORY_FOR_QUERIES:-5G}" - export QLEVER_SERVER_CACHE_MAX_SIZE="${QLEVER_SERVER_CACHE_MAX_SIZE:-2G}" - export QLEVER_SERVER_TIMEOUT="${QLEVER_SERVER_TIMEOUT:-30s}" - export QLEVER_RUNTIME_SYSTEM="${QLEVER_RUNTIME_SYSTEM:-native}" - export QLEVER_UI_UI_CONFIG="${QLEVER_UI_UI_CONFIG:-default}" - export QLEVER_UI_UI_PORT="${QLEVER_UI_UI_PORT:-7002}" - - QLEVER_ENV_VARS="$(env | grep '^QLEVER_' | sort -u)" - - generate_config_section() { - section="$1" - section_upper="$(echo "${section}" | tr '[:lower:]' '[:upper:]')" - section_env_vars="$(echo "${QLEVER_ENV_VARS}" | grep "^QLEVER_${section_upper}_" | sed "s/^QLEVER_${section_upper}_//")" - - if [ -z "${section_env_vars}" ]; then - return - fi - - echo "[$section]" - echo "${section_env_vars}" - echo "" - } - - { - echo "# Qleverfile auto-generated at $(date)" - echo "" - generate_config_section "data" - generate_config_section "index" - generate_config_section "server" - generate_config_section "runtime" - generate_config_section "ui" - } > "${QLEVER_FILE_PATH}" - - echo "INFO: Generated Qleverfile at '${QLEVER_FILE_PATH}'" - exit 0 - ---- -# Deployment for QLever server -apiVersion: apps/v1 -kind: Deployment -metadata: - name: qlever-server - namespace: qlever - labels: - app: qlever-server -spec: - replicas: 1 # Single instance - QLever isn't designed for horizontal scaling - selector: - matchLabels: - app: qlever-server - template: - metadata: - labels: - app: qlever-server - spec: - # Init container to fix permissions (replaces qlever-init from docker-compose) - initContainers: - - name: fix-permissions - image: busybox:latest - command: - - sh - - -c - - | - echo "Fixing /data ownership to ${UID:-1000}:${GID:-1000}" - chown -R ${UID:-1000}:${GID:-1000} /data - chmod -R u+rwX,g+rwX,o+rX /data - envFrom: - - configMapRef: - name: qlever-config - volumeMounts: - - name: data - mountPath: /data - securityContext: - runAsUser: 0 # Init container runs as root to fix permissions - - containers: - - name: qlever-server - image: adfreiburg/qlever:latest - - # For Apple Silicon compatibility (uncomment if needed) - # This would need to be set via environment or overlay - # platform: linux/amd64 - - # Run as specified UID/GID - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - - # Working directory - workingDir: /data - - # Custom entrypoint - command: ["/bin/bash", "/qlever/scripts/entrypoint.sh"] - - # Environment variables from ConfigMap - envFrom: - - configMapRef: - name: qlever-config - - ports: - - name: qlever - containerPort: 7001 - protocol: TCP - - # Volume mounts - volumeMounts: - # Main data volume (persistent) - - name: data - mountPath: /data - - # Scripts from ConfigMap - - name: scripts - mountPath: /qlever/scripts - readOnly: true - - # Input data (you'll need to configure this based on your setup) - # Option 1: Mount from host path (for development) - # - name: input-data - # mountPath: /input - # readOnly: true - - # Option 2: Mount from another PVC or ConfigMap - # Uncomment and configure as needed - - # Resource requests and limits (adjust based on your needs) - resources: - requests: - memory: "4Gi" - cpu: "1000m" - limits: - memory: "8Gi" - cpu: "2000m" - - # Liveness probe - checks if server is responding - livenessProbe: - httpGet: - path: / # Adjust based on QLever's health endpoint - port: 7001 - initialDelaySeconds: 60 # Wait for indexing to complete - periodSeconds: 30 - timeoutSeconds: 5 - failureThreshold: 3 - - # Readiness probe - checks if server is ready to accept traffic - readinessProbe: - httpGet: - path: / # Adjust based on QLever's health endpoint - port: 7001 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - - volumes: - # Persistent data volume - - name: data - persistentVolumeClaim: - claimName: qlever-data-pvc - - # Scripts from ConfigMap - - name: scripts - configMap: - name: qlever-scripts - defaultMode: 0755 # Make scripts executable - - # Input data volume (configure based on your needs) - # Option 1: HostPath (for development/testing) - # - name: input-data - # hostPath: - # path: /path/to/your/data - # type: Directory - - # Option 2: Another PVC - # - name: input-data - # persistentVolumeClaim: - # claimName: qlever-input-data-pvc - - # Restart policy - restartPolicy: Always - ---- -# Service to expose QLever server -apiVersion: v1 -kind: Service -metadata: - name: qlever-server - namespace: qlever - labels: - app: qlever-server -spec: - type: ClusterIP # Change to LoadBalancer or NodePort if you need external access - selector: - app: qlever-server - ports: - - name: qlever - port: 7001 - targetPort: 7001 - protocol: TCP - # nodePort: 30701 # Uncomment if using NodePort - ---- -# Optional: Ingress for external access -# Uncomment and configure if you want to expose via ingress controller -# apiVersion: networking.k8s.io/v1 -# kind: Ingress -# metadata: -# name: qlever-ingress -# namespace: qlever -# annotations: -# # Add ingress controller specific annotations -# # nginx.ingress.kubernetes.io/rewrite-target: / -# spec: -# rules: -# - host: qlever.example.com # Replace with your domain -# http: -# paths: -# - path: / -# pathType: Prefix -# backend: -# service: -# name: qlever-server -# port: -# number: 7001 \ No newline at end of file