Merge pull request #684 from semantica-agi/issue-681-knowledge-explorer-deploy-templates

Add Knowledge Explorer deployment templates
This commit is contained in:
Mohd Kaif
2026-06-24 23:21:54 +05:30
committed by GitHub
47 changed files with 1416 additions and 37 deletions
+5
View File
@@ -0,0 +1,5 @@
# Checkov configuration.
# Cloud Run false-positives (CKV_K8S_21/28/30) are suppressed via per-file
# inline checkov:skip comments in deploy/gcp/cloudrun-service.yaml rather than
# globally here, so future real Kubernetes manifests are not silently exempted.
skip-check: []
+103
View File
@@ -0,0 +1,103 @@
# Start with a tiny Docker context and opt in only files used by Dockerfile.
*
!Dockerfile
!.dockerignore
!pyproject.toml
!README.md
!LICENSE
!MANIFEST.in
!semantica/
!semantica/**
!integrations/
!integrations/**
!explorer/
!explorer/**
# VCS, local config, and secrets.
.git
.git/**
.github
.github/**
.claude
.claude/**
.codex
.codex/**
.agents
.agents/**
.env
.env.*
*.env
# Python build/test/cache artifacts.
__pycache__
**/__pycache__
*.py[cod]
.pytest_cache
.pytest_cache/**
.mypy_cache
.mypy_cache/**
.ruff_cache
.ruff_cache/**
.tox
.tox/**
.venv
.venv/**
venv
venv/**
coverage
coverage/**
htmlcov
htmlcov/**
*.egg-info
*.egg-info/**
build
build/**
dist
dist/**
# Frontend dependency/build artifacts.
node_modules
node_modules/**
explorer/node_modules
explorer/node_modules/**
explorer/dist
explorer/dist/**
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Local outputs and large generated samples.
logs
logs/**
*.log
*.tmp
*.bak
*.backup
tests
tests/**
explorer/tests
explorer/tests/**
docs
docs/**
site
site/**
.mkdocs_cache
.mkdocs_cache/**
cookbook
cookbook/**
examples
examples/**
demo_assets
demo_assets/**
demo_out
demo_out/**
demo_out_*
demo_out_*/**
outputs
outputs/**
pytest-cache-files-*
pytest-cache-files-*/**
test_data
test_data/**
sample_data
sample_data/**
+37 -2
View File
@@ -32,7 +32,7 @@ permissions:
jobs:
MSDO:
# currently only windows latest is supported
# currently only windows-latest is supported
runs-on: windows-latest
steps:
@@ -46,8 +46,43 @@ jobs:
uses: microsoft/security-devops-action@v1.12.0
id: msdo
with:
tools: checkov,eslint,templateanalyzer,terrascan
# checkov is intentionally excluded from this MSDO step.
# MSDO 0.215.0's guardian.cmd wrapper treats checkov's exit code 1
# (emitted whenever any violation is found, even below the active severity
# threshold) as a fatal "tool error" and breaks the build even when
# "Active results: 0" and "Found no breaking results." The .checkov.yaml
# soft-fail setting is never read by the guardian wrapper.
# IaC security scanning continues below in this same MSDO job identity.
# That preserves the existing GitHub code-scanning configuration while
# avoiding the guardian.cmd/checkov exit-code bug in the MSDO wrapper.
tools: eslint,templateanalyzer,terrascan
- name: Upload results to Security tab
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ steps.msdo.outputs.sarifFile }}
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Checkov
run: python -m pip install checkov==3.3.1
- name: Run Checkov
shell: pwsh
env:
PYTHONUTF8: "1"
run: |
New-Item -ItemType Directory -Force reports | Out-Null
checkov --directory . --framework kubernetes helm dockerfile github_actions secrets bicep arm --soft-fail --output sarif --output-file-path reports/checkov.sarif
if (-not (Test-Path reports/checkov.sarif)) {
$sarif = Get-ChildItem -Path reports -Recurse -Filter *.sarif | Select-Object -First 1
if ($null -eq $sarif) { throw "Checkov did not produce a SARIF file" }
Copy-Item $sarif.FullName reports/checkov.sarif
}
- name: Upload Checkov results to Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: reports/checkov.sarif
+3
View File
@@ -18,6 +18,9 @@ on:
- 'requirements-docs.txt'
- '**/*.md'
permissions:
contents: read
jobs:
security-scan:
runs-on: ubuntu-latest
+27 -16
View File
@@ -1,29 +1,40 @@
FROM node:26-alpine AS frontend-builder
# syntax=docker/dockerfile:1
FROM node:22-alpine AS frontend-builder
WORKDIR /app/semantica-explorer
WORKDIR /app
COPY explorer/package*.json ./explorer/
WORKDIR /app/explorer
RUN npm ci
COPY explorer/ ./
RUN mkdir -p /app/semantica && npm run build
COPY semantica-explorer/package.json semantica-explorer/package-lock.json* ./
FROM python:3.12-slim AS runtime
RUN npm install
COPY semantica-explorer/ ./
RUN npm run build
FROM python:3.14-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
FALKORDB_HOST=falkordb \
FALKORDB_PORT=6379 \
ALLOWED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
WORKDIR /app
COPY pyproject.toml ./
COPY semantica/ ./semantica/
RUN groupadd --system semantica \
&& useradd --system --gid semantica --home-dir /app --shell /usr/sbin/nologin semantica
COPY pyproject.toml README.md LICENSE MANIFEST.in ./
COPY semantica/ ./semantica/
COPY integrations/ ./integrations/
COPY --from=frontend-builder /app/semantica/static ./semantica/static
RUN pip install --no-cache-dir ".[explorer]"
RUN pip install --no-cache-dir ".[explorer]" \
&& chown -R semantica:semantica /app
USER semantica
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "semantica.explorer.app:app", "--host", "0.0.0.0", "--port", "8000"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD python -c "import json, urllib.request; data=json.load(urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3)); raise SystemExit(0 if data.get('status') == 'ok' else 1)"
CMD ["python", "-m", "uvicorn", "semantica.explorer.app:app", "--host", "0.0.0.0", "--port", "8000"]
+26
View File
@@ -0,0 +1,26 @@
# Azure Container Apps
Deploy with Azure Developer CLI from this template directory:
```bash
cd deploy/azure
azd auth login
azd init --environment semantica-ke
azd env set AZURE_LOCATION eastus
# The template defaults to an internal (private) Container Apps environment.
# Provide the resource ID of an existing subnet (delegated to Microsoft.App/environments):
azd env set AZURE_INFRASTRUCTURE_SUBNET_ID /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Network/virtualNetworks/<vnet>/subnets/<subnet>
# For a quick public dev/test deployment without a VNet, override the default:
# azd env set AZURE_INFRASTRUCTURE_SUBNET_ID "" and set vnetInternal=false in main.parameters.json
azd up
```
After the first deploy, set `allowedOrigins` in `main.parameters.json` to the Container App URL printed by `azd up` (e.g. `https://<app-name>.<unique>.eastus.azurecontainerapps.io`), then re-run `azd up` to apply the CORS restriction.
The Bicep template provisions:
- A Container Apps managed environment with an internal load balancer (private VNet, no public IP) and a system-assigned managed identity on the Container App (AZR-000363 / AZR-000361 compliant).
- HTTP ingress, scale-to-zero, max 10 replicas, and a `/api/health` liveness probe.
+14
View File
@@ -0,0 +1,14 @@
name: semantica-knowledge-explorer
metadata:
template: semantica-knowledge-explorer@0.1.0
services:
explorer:
project: ../..
host: containerapp
language: docker
docker:
path: ./Dockerfile
context: .
infra:
provider: bicep
path: .
+132
View File
@@ -0,0 +1,132 @@
targetScope = 'resourceGroup'
param environmentName string = 'semantica-ke'
param location string = resourceGroup().location
param imageName string
param containerPort int = 8000
param allowedOrigins string = '*'
param falkordbHost string = 'falkordb'
param falkordbPort string = '6379'
@description('Deploy the managed environment with an internal load balancer (no public IP). Recommended for production. Set false only for quick dev/test deployments.')
param vnetInternal bool = true
@description('Resource ID of an existing subnet for the Container Apps environment. Required when vnetInternal is true. E.g. /subscriptions/.../subnets/aca-subnet')
param infrastructureSubnetId string = ''
var appName = '${environmentName}-explorer'
var logAnalyticsName = '${environmentName}-logs'
var managedEnvironmentName = '${environmentName}-env'
// Two concrete objects avoids a `null` ternary branch, which crashes checkov's Bicep parser.
var vnetConfigInternal = {
internal: true
infrastructureSubnetId: infrastructureSubnetId
}
var vnetConfigExternal = {
internal: false
}
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: logAnalyticsName
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
resource managedEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' = {
name: managedEnvironmentName
location: location
properties: {
vnetConfiguration: vnetInternal ? vnetConfigInternal : vnetConfigExternal
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalytics.properties.customerId
sharedKey: logAnalytics.listKeys().primarySharedKey
}
}
}
}
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
name: appName
location: location
identity: {
type: 'SystemAssigned'
}
tags: {
'azd-service-name': 'explorer'
}
properties: {
managedEnvironmentId: managedEnvironment.id
configuration: {
activeRevisionsMode: 'Single'
ingress: {
external: true
targetPort: containerPort
transport: 'auto'
allowInsecure: false
}
}
template: {
containers: [
{
name: 'explorer'
image: imageName
env: [
{
name: 'ALLOWED_ORIGINS'
value: allowedOrigins
}
{
name: 'FALKORDB_HOST'
value: falkordbHost
}
{
name: 'FALKORDB_PORT'
value: falkordbPort
}
]
probes: [
{
type: 'Liveness'
httpGet: {
path: '/api/health'
port: containerPort
}
initialDelaySeconds: 20
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
}
]
resources: {
cpu: json('0.5')
memory: '1Gi'
}
}
]
scale: {
minReplicas: 0
maxReplicas: 10
rules: [
{
name: 'http-concurrency'
http: {
metadata: {
concurrentRequests: '100'
}
}
}
]
}
}
}
}
output endpoint string = 'https://${containerApp.properties.configuration.ingress.fqdn}'
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": {
"value": "${AZURE_ENV_NAME}"
},
"location": {
"value": "${AZURE_LOCATION}"
},
"imageName": {
"value": "${SERVICE_EXPLORER_IMAGE_NAME}"
},
"allowedOrigins": {
"value": "https://REPLACE_ME.azurecontainerapps.io"
},
"falkordbHost": {
"value": "falkordb"
},
"falkordbPort": {
"value": "6379"
},
"vnetInternal": {
"value": true
},
"infrastructureSubnetId": {
"value": "${AZURE_INFRASTRUCTURE_SUBNET_ID}"
}
}
}
+15
View File
@@ -0,0 +1,15 @@
# Fly.io
Deploy from a clean checkout using the root Dockerfile:
```bash
flyctl auth login
flyctl launch --copy-config --config deploy/fly/fly.toml --no-deploy
# Replace <falkordb-app-name> with your FalkorDB Fly app name.
# Fly.io private networking uses .internal hostnames — do not use localhost
# unless FalkorDB is a co-located process inside the same Machine.
flyctl secrets set FALKORDB_HOST=<falkordb-app-name>.internal FALKORDB_PORT=6379
flyctl deploy --config deploy/fly/fly.toml
```
Change `app` in `fly.toml` before launch if the default app name is already taken.
+32
View File
@@ -0,0 +1,32 @@
app = "semantica-knowledge-explorer"
primary_region = "iad"
kill_signal = "SIGTERM"
kill_timeout = "30s"
[build]
dockerfile = "Dockerfile"
[env]
ALLOWED_ORIGINS = "https://semantica-knowledge-explorer.fly.dev"
# Set via: flyctl secrets set FALKORDB_HOST=<your-falkordb-app-name>.internal FALKORDB_PORT=6379
# Do not use localhost unless FalkorDB runs as a co-located process in the same Machine.
FALKORDB_HOST = "falkordb-REPLACE_ME.internal"
FALKORDB_PORT = "6379"
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[[http_service.checks]]
grace_period = "20s"
interval = "30s"
method = "GET"
timeout = "5s"
path = "/api/health"
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
+19
View File
@@ -0,0 +1,19 @@
# GCP Cloud Run
Create the Secret Manager entries, then submit the Cloud Build pipeline:
```bash
gcloud services enable cloudbuild.googleapis.com run.googleapis.com secretmanager.googleapis.com
printf "falkordb-host.example.internal" | gcloud secrets create falkordb-host --data-file=-
printf "6379" | gcloud secrets create falkordb-port --data-file=-
# Set _ALLOWED_ORIGINS to your actual service URL after the first deploy.
gcloud builds submit --config deploy/gcp/cloudbuild.yaml \
--substitutions _REGION=us-central1,_SERVICE_NAME=knowledge-explorer,_ALLOWED_ORIGINS=https://knowledge-explorer-REPLACE_ME.a.run.app
```
For declarative deploys, substitute your project ID and deploy in one step:
```bash
sed "s/PROJECT_ID/$(gcloud config get-value project)/g" deploy/gcp/cloudrun-service.yaml | \
gcloud run services replace - --region us-central1
```
+61
View File
@@ -0,0 +1,61 @@
substitutions:
_REGION: us-central1
_SERVICE_NAME: knowledge-explorer
_IMAGE: gcr.io/$PROJECT_ID/knowledge-explorer
# Set to your actual service URL — do not use '*' in production.
_ALLOWED_ORIGINS: https://knowledge-explorer-REPLACE_ME.a.run.app
steps:
- name: gcr.io/cloud-builders/docker
args:
- build
- -t
- ${_IMAGE}:$SHORT_SHA
- -t
- ${_IMAGE}:latest
- .
- name: gcr.io/cloud-builders/docker
args:
- push
- ${_IMAGE}:$SHORT_SHA
- name: gcr.io/cloud-builders/docker
args:
- push
- ${_IMAGE}:latest
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
entrypoint: gcloud
args:
- run
- deploy
- ${_SERVICE_NAME}
- --image
- ${_IMAGE}:$SHORT_SHA
- --region
- ${_REGION}
- --platform
- managed
# SECURITY: Remove --allow-unauthenticated and restrict ingress for
# production; add IAP or a load balancer with auth before enabling
# unauthenticated access. See: cloud.google.com/run/docs/authenticating
- --no-allow-unauthenticated
- --ingress
- internal-and-cloud-load-balancing
- --port
- "8000"
- --min-instances
- "0"
- --max-instances
- "10"
- --set-env-vars
# Replace with your actual Cloud Run service URL after first deploy,
# e.g. ALLOWED_ORIGINS=https://knowledge-explorer-abc123-uc.a.run.app
- ALLOWED_ORIGINS=${_ALLOWED_ORIGINS}
- --set-secrets
- FALKORDB_HOST=falkordb-host:latest,FALKORDB_PORT=falkordb-port:latest
images:
- ${_IMAGE}:$SHORT_SHA
- ${_IMAGE}:latest
+58
View File
@@ -0,0 +1,58 @@
# checkov:skip=CKV_K8S_21:Cloud Run has no namespace concept; Knative services are project-scoped not namespace-scoped
# checkov:skip=CKV_K8S_28:Cloud Run enforces seccomp at the platform level; this Knative YAML is not a K8s deployment
# checkov:skip=CKV_K8S_30:Cloud Run enforces AppArmor at the platform level; this Knative YAML is not a K8s deployment
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: knowledge-explorer
annotations:
# Use 'internal-and-cloud-load-balancing' or 'internal' in production.
# 'all' permits direct unauthenticated public internet access.
run.googleapis.com/ingress: internal-and-cloud-load-balancing
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "10"
spec:
containerConcurrency: 80
timeoutSeconds: 300
containers:
- name: explorer
# Replace PROJECT_ID with your GCP project ID before deploying.
# See the README for the sed one-liner that does this automatically.
image: gcr.io/PROJECT_ID/knowledge-explorer:latest
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
ports:
- name: http1
containerPort: 8000
env:
- name: ALLOWED_ORIGINS
# Replace with your actual service URL — do not use '*' in production.
value: "https://knowledge-explorer-REPLACE_ME.a.run.app"
- name: FALKORDB_HOST
valueFrom:
secretKeyRef:
name: falkordb-host
key: latest
- name: FALKORDB_PORT
valueFrom:
secretKeyRef:
name: falkordb-port
key: latest
resources:
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 20
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
+9
View File
@@ -0,0 +1,9 @@
# Helm
Deploy the Knowledge Explorer chart:
```bash
helm lint deploy/helm/knowledge-explorer
helm upgrade --install knowledge-explorer deploy/helm/knowledge-explorer --namespace semantica --create-namespace
helm upgrade --install knowledge-explorer deploy/helm/knowledge-explorer --namespace semantica --create-namespace -f deploy/helm/knowledge-explorer/values.prod.yaml
```
@@ -0,0 +1,6 @@
apiVersion: v2
name: knowledge-explorer
description: Semantica Knowledge Explorer deployment chart
type: application
version: 0.1.0
appVersion: "0.5.0"
+9
View File
@@ -0,0 +1,9 @@
# Knowledge Explorer Helm Chart
```bash
helm lint deploy/helm/knowledge-explorer
helm upgrade --install knowledge-explorer deploy/helm/knowledge-explorer --namespace semantica --create-namespace
helm upgrade --install knowledge-explorer deploy/helm/knowledge-explorer --namespace semantica --create-namespace -f deploy/helm/knowledge-explorer/values.prod.yaml
```
Set `autoscaling.enabled=true` to render the HPA. Put sensitive values in Kubernetes Secrets and reference them outside this chart, or pass non-secret env values through `env`.
@@ -0,0 +1,29 @@
{{- define "knowledge-explorer.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "knowledge-explorer.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 -}}
{{- define "knowledge-explorer.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ include "knowledge-explorer.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "knowledge-explorer.selectorLabels" -}}
app.kubernetes.io/name: {{ include "knowledge-explorer.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
data:
{{- range $key, $value := .Values.env }}
{{ $key }}: {{ $value | quote }}
{{- end }}
@@ -0,0 +1,101 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
{{- include "knowledge-explorer.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "knowledge-explorer.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
automountServiceAccountToken: {{ .Values.automountServiceAccountToken }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: explorer
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
{{- if .Values.image.digest }}
image: "{{ .Values.image.repository }}@{{ .Values.image.digest }}"
{{- else }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "knowledge-explorer.fullname" . }}
{{- if .Values.secretEnv }}
env:
{{- range $key, $value := .Values.secretEnv }}
- name: {{ $key }}
valueFrom:
secretKeyRef:
name: {{ $value.secretName }}
key: {{ $value.secretKey }}
{{- end }}
{{- end }}
livenessProbe:
httpGet:
path: {{ .Values.livenessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
readinessProbe:
httpGet:
path: {{ .Values.readinessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
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 }}
@@ -0,0 +1,23 @@
{{- if .Values.autoscaling.enabled -}}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "knowledge-explorer.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
@@ -0,0 +1,42 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- 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 "knowledge-explorer.fullname" $ }}
port:
name: http
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,43 @@
{{- if .Values.networkPolicy.enabled -}}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "knowledge-explorer.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
# Allow traffic from the ingress controller namespace.
# Override networkPolicy.ingressNamespace in values if your controller uses a different namespace.
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ .Values.networkPolicy.ingressNamespace }}
ports:
- protocol: TCP
port: {{ .Values.service.targetPort }}
# Allow traffic from pods within the same namespace (e.g. monitoring sidecars).
- from:
- podSelector: {}
ports:
- protocol: TCP
port: {{ .Values.service.targetPort }}
egress:
# FalkorDB
- ports:
- protocol: TCP
port: {{ .Values.networkPolicy.falkordbPort | default 6379 }}
# DNS resolution
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- end }}
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "knowledge-explorer.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "knowledge-explorer.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "knowledge-explorer.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,29 @@
image:
repository: ghcr.io/semantica-agi/semantica-knowledge-explorer
# Replace this placeholder digest with the digest of the image you publish.
digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"
tag: "0.5.0"
pullPolicy: Always
ingress:
enabled: true
hosts:
- host: knowledge-explorer.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: knowledge-explorer-tls
hosts:
- knowledge-explorer.example.com
env:
ALLOWED_ORIGINS: https://knowledge-explorer.example.com
FALKORDB_HOST: falkordb.semantic-data.svc.cluster.local
FALKORDB_PORT: "6379"
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 75
+103
View File
@@ -0,0 +1,103 @@
replicaCount: 2
image:
repository: semantica-knowledge-explorer
pullPolicy: Always
# Replace this placeholder digest with the digest of the image you publish.
digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"
# Used only when image.digest is empty.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
automountServiceAccountToken: false
podAnnotations:
# AppArmor — must match the container name defined in the Deployment template ("explorer").
container.apparmor.security.beta.kubernetes.io/explorer: runtime/default
podLabels: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALL
service:
type: ClusterIP
port: 80
targetPort: 8000
ingress:
enabled: false
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: knowledge-explorer.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: knowledge-explorer-tls
hosts:
- knowledge-explorer.example.com
env:
ALLOWED_ORIGINS: https://knowledge-explorer.example.com
FALKORDB_HOST: falkordb
FALKORDB_PORT: "6379"
secretEnv: {}
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
path: /api/health
initialDelaySeconds: 20
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
path: /api/health
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
networkPolicy:
enabled: true
# Namespace label of your ingress controller. Ingress is only admitted from this namespace
# and from pods within the same namespace as the Explorer.
ingressNamespace: ingress-nginx
# FalkorDB port allowed for egress. Must match FALKORDB_PORT.
falkordbPort: 6379
+12
View File
@@ -0,0 +1,12 @@
# Kubernetes
Apply the raw manifests with Kustomize:
```bash
cp deploy/kubernetes/secret.yaml.example deploy/kubernetes/secret.yaml
kubectl apply -f deploy/kubernetes/secret.yaml
kubectl apply -k deploy/kubernetes
kubectl -n semantica rollout status deployment/knowledge-explorer
```
Update the placeholder image digest and ingress host before deploying to production. `secret.yaml` is intentionally ignored from the kustomization; keep only `secret.yaml.example` in git.
+9
View File
@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: knowledge-explorer-config
namespace: semantica
data:
ALLOWED_ORIGINS: "https://knowledge-explorer.example.com"
FALKORDB_HOST: "falkordb"
FALKORDB_PORT: "6379"
+84
View File
@@ -0,0 +1,84 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: knowledge-explorer
namespace: semantica
labels:
app.kubernetes.io/name: knowledge-explorer
app.kubernetes.io/part-of: semantica
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app.kubernetes.io/name: knowledge-explorer
template:
metadata:
annotations:
container.apparmor.security.beta.kubernetes.io/explorer: runtime/default
seccomp.security.alpha.kubernetes.io/pod: runtime/default
labels:
app.kubernetes.io/name: knowledge-explorer
app.kubernetes.io/part-of: semantica
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: explorer
image: semantica-knowledge-explorer@sha256:0000000000000000000000000000000000000000000000000000000000000000
imagePullPolicy: Always
ports:
- name: http
containerPort: 8000
envFrom:
- configMapRef:
name: knowledge-explorer-config
- secretRef:
name: knowledge-explorer-secrets
optional: true
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 20
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
+25
View File
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: knowledge-explorer
namespace: semantica
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- knowledge-explorer.example.com
secretName: knowledge-explorer-tls
rules:
- host: knowledge-explorer.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: knowledge-explorer
port:
name: http
+9
View File
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
- networkpolicy.yaml
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: semantica
labels:
owner: semantica
+42
View File
@@ -0,0 +1,42 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: knowledge-explorer
namespace: semantica
labels:
app.kubernetes.io/name: knowledge-explorer
app.kubernetes.io/part-of: semantica
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: knowledge-explorer
policyTypes:
- Ingress
- Egress
ingress:
# Allow traffic from the ingress controller namespace.
# Adjust the namespace label if your ingress controller uses a different namespace.
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 8000
# Allow traffic from pods within the same namespace (e.g. monitoring sidecars).
- from:
- podSelector: {}
ports:
- protocol: TCP
port: 8000
egress:
# FalkorDB
- ports:
- protocol: TCP
port: 6379
# DNS resolution
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: knowledge-explorer-secrets
namespace: semantica
type: Opaque
stringData:
FALKORDB_PASSWORD: "replace-me-if-your-falkordb-requires-auth"
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: knowledge-explorer
namespace: semantica
labels:
app.kubernetes.io/name: knowledge-explorer
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: knowledge-explorer
ports:
- name: http
port: 80
targetPort: http
+15
View File
@@ -0,0 +1,15 @@
# Railway
Deploys the Knowledge Explorer from the root `Dockerfile` and checks `/api/health`.
```bash
railway login
railway init
railway add --database redis
railway variable --set "FALKORDB_HOST=${{Redis.REDISHOST}}"
railway variable --set "FALKORDB_PORT=${{Redis.REDISPORT}}"
railway variable --set "ALLOWED_ORIGINS=https://${{RAILWAY_PUBLIC_DOMAIN}}"
railway up
```
The Redis plugin variables are wired to the requested FalkorDB env names for deployment compatibility. The Explorer currently reads these settings but does not persist graph state to FalkorDB.
+9
View File
@@ -0,0 +1,9 @@
[build]
builder = "DOCKERFILE"
dockerfilePath = "Dockerfile"
[deploy]
healthcheckPath = "/api/health"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
+11
View File
@@ -0,0 +1,11 @@
# Render
This Blueprint provisions a Docker web service plus a Render Key Value instance and wires the datastore host/port into the Explorer env vars.
```bash
render login
render blueprints validate deploy/render/render.yaml
render blueprint apply deploy/render/render.yaml
```
After creation, update `ALLOWED_ORIGINS` in the Render dashboard if you attach a custom domain.
+29
View File
@@ -0,0 +1,29 @@
services:
- type: web
name: semantica-knowledge-explorer
runtime: docker
plan: starter
region: oregon
dockerfilePath: ./Dockerfile
dockerContext: .
healthCheckPath: /api/health
envVars:
- key: ALLOWED_ORIGINS
value: https://semantica-knowledge-explorer.onrender.com
- key: FALKORDB_HOST
fromService:
type: keyvalue
name: semantica-explorer-redis
property: host
- key: FALKORDB_PORT
fromService:
type: keyvalue
name: semantica-explorer-redis
property: port
- type: keyvalue
name: semantica-explorer-redis
plan: starter
ipAllowList: []
maxmemoryPolicy: noeviction
persistenceMode: snapshot
+46
View File
@@ -0,0 +1,46 @@
services:
explorer:
command:
- python
- -m
- uvicorn
- semantica.explorer.app:app
- --host
- 0.0.0.0
- --port
- "8000"
- --reload
- --reload-dir
- /app/semantica
environment:
ALLOWED_ORIGINS: http://localhost:5173,http://127.0.0.1:5173,http://localhost:8000,http://127.0.0.1:8000
FALKORDB_HOST: falkordb
FALKORDB_PORT: "6379"
volumes:
- ./semantica:/app/semantica
- ./pyproject.toml:/app/pyproject.toml:ro
frontend:
image: node:22-alpine
working_dir: /app/explorer
command: sh -c "npm ci && npm run dev -- --host 0.0.0.0"
environment:
VITE_EXPLORER_API_TARGET: http://explorer:8000
VITE_EXPLORER_WS_TARGET: ws://explorer:8000
ports:
- "5173:5173"
volumes:
- ./explorer:/app/explorer
- explorer_node_modules:/app/explorer/node_modules
depends_on:
explorer:
condition: service_started
networks:
- semantica
networks:
semantica:
driver: bridge
volumes:
explorer_node_modules:
+25 -1
View File
@@ -1,11 +1,35 @@
services:
explorer:
build:
context: .
dockerfile: Dockerfile
image: semantica-knowledge-explorer:latest
environment:
FALKORDB_HOST: falkordb
FALKORDB_PORT: "6379"
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:8000,http://127.0.0.1:8000}
depends_on:
falkordb:
condition: service_started
ports:
- "8000:8000"
networks:
- semantica
restart: unless-stopped
falkordb:
image: falkordb/falkordb:latest
ports:
- "6379:6379"
volumes:
- falkordb_data:/data
restart: always
networks:
- semantica
restart: unless-stopped
networks:
semantica:
driver: bridge
volumes:
falkordb_data:
+1 -1
View File
@@ -69,7 +69,7 @@ python -c "import semantica; print(semantica.__version__)"
```bash
curl http://localhost:8000/health
# {"status": "healthy"}
# {"status": "ok"}
curl http://localhost:8000/api/info
# {"name": "Semantica API", "version": "...", "status": "active"}
+2 -2
View File
@@ -56,7 +56,7 @@ The browser opens at `http://127.0.0.1:8000`. The health endpoint confirms the s
```bash
curl http://127.0.0.1:8000/api/health
# {"status": "healthy"}
# {"status": "ok"}
```
@@ -174,7 +174,7 @@ Once the server is running:
| :--- | :------------ |
| `http://127.0.0.1:8000` | Interactive dashboard |
| `http://127.0.0.1:8000/docs` | Swagger UI: every REST endpoint, interactive |
| `http://127.0.0.1:8000/api/health` | Health check: `{"status": "healthy"}` |
| `http://127.0.0.1:8000/api/health` | Health check: `{"status": "ok"}` |
The browser tab opens shortly after startup. If it does not open, navigate to the URL manually or pass `--no-browser` and open it yourself.
+1 -1
View File
@@ -306,7 +306,7 @@ Full interactive docs at `http://localhost:8000/docs`. All endpoints accept and
| Endpoint | Method | Description |
| :-------- | :------ | :----------- |
| `/api/health` | `GET` | Returns `{"status": "healthy"}` |
| `/api/health` | `GET` | Returns `{"status": "ok"}` |
| `/api/info` | `GET` | Server name, version, status |
| `/docs` | `GET` | Interactive Swagger UI: all endpoints |
+5 -2
View File
@@ -2,6 +2,9 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
const apiTarget = process.env.VITE_EXPLORER_API_TARGET ?? 'http://127.0.0.1:8000'
const wsTarget = process.env.VITE_EXPLORER_WS_TARGET ?? apiTarget.replace(/^http/, 'ws')
// https://vite.dev/config/
export default defineConfig({
plugins: [
@@ -57,11 +60,11 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
target: apiTarget,
changeOrigin: true,
},
'/ws': {
target: 'ws://127.0.0.1:8000',
target: wsTarget,
ws: true,
},
},
+43 -10
View File
@@ -1,4 +1,4 @@
"""
"""
Semantica Explorer FastAPI application factory.
"""
@@ -14,11 +14,44 @@ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from .. import __version__
from ..context.context_graph import ContextGraph
from .session import GraphSession
from .ws import ConnectionManager
def _read_int_env(name: str, default: int) -> int:
raw_value = os.environ.get(name)
if raw_value is None or raw_value.strip() == "":
return default
try:
return int(raw_value)
except ValueError:
return default
def _read_explorer_settings() -> dict:
if "ALLOWED_ORIGINS" in os.environ:
raw_origins = os.environ["ALLOWED_ORIGINS"]
elif "EXPLORER_CORS_ORIGINS" in os.environ:
raw_origins = os.environ["EXPLORER_CORS_ORIGINS"]
else:
raw_origins = "http://localhost:5173,http://127.0.0.1:5173"
return {
"allowed_origins": [
origin.strip() for origin in raw_origins.split(",") if origin.strip()
],
# These are read and stored for future use when direct FalkorDB connection
# support is added to the Explorer. Currently GraphSession uses an in-memory
# ContextGraph and does not open a network connection to FalkorDB.
"falkordb_host": os.environ.get("FALKORDB_HOST", "localhost"),
"falkordb_port": _read_int_env("FALKORDB_PORT", 6379),
}
def _install_mutation_bridge(app: FastAPI, session: GraphSession) -> None:
if getattr(session.graph, "_mutation_bridge_installed", False):
return
session.graph._mutation_bridge_installed = True
previous_callback = getattr(session.graph, "mutation_callback", None)
def on_mutation(event_type: str, entity_id: str, payload: dict) -> None:
@@ -43,13 +76,15 @@ def _install_mutation_bridge(app: FastAPI, session: GraphSession) -> None:
def create_app(session: Optional[GraphSession] = None) -> FastAPI:
active_session = session or GraphSession(ContextGraph(advanced_analytics=False))
settings = _read_explorer_settings()
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.event_loop = asyncio.get_running_loop()
app.state.ws_manager = ConnectionManager()
if session is not None:
app.state.session = session
_install_mutation_bridge(app, session)
app.state.session = active_session
_install_mutation_bridge(app, active_session)
yield
app = FastAPI(
@@ -59,10 +94,8 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
lifespan=lifespan,
)
_raw_origins = os.environ.get(
"EXPLORER_CORS_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173"
)
_cors_origins = [o.strip() for o in _raw_origins.split(",") if o.strip()]
app.state.explorer_settings = settings
# allow_credentials lets browsers send cookies/auth headers cross-origin.
# The Explorer has no authentication, so credentials serve no purpose and
# enabling them when origins are broadened creates cross-site request risk.
@@ -71,7 +104,7 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
_allow_credentials = os.environ.get("EXPLORER_CORS_CREDENTIALS", "false").lower() == "true"
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins,
allow_origins=settings["allowed_origins"],
allow_credentials=_allow_credentials,
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"],
@@ -169,7 +202,7 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
@app.get("/api/health")
async def health():
return {"status": "healthy"}
return {"status": "ok"}
@app.get("/api/info")
async def info():
+36 -2
View File
@@ -1,4 +1,4 @@
"""Integration tests for the explorer API."""
"""Integration tests for the explorer API."""
import json
from pathlib import Path
@@ -151,7 +151,7 @@ class TestHealthInfo:
def test_health(self, client):
response = client.get("/api/health")
assert response.status_code == 200
assert response.json()["status"] == "healthy"
assert response.json() == {"status": "ok"}
def test_info(self, client):
response = client.get("/api/info")
@@ -161,6 +161,40 @@ class TestHealthInfo:
assert payload["status"] == "active"
assert payload["version"]
def test_env_settings_are_read_from_supported_names(self, monkeypatch):
monkeypatch.setenv(
"ALLOWED_ORIGINS",
"https://app.example.com, https://team.example.com",
)
monkeypatch.setenv("FALKORDB_HOST", "falkordb.internal")
monkeypatch.setenv("FALKORDB_PORT", "6380")
app = create_app()
assert app.state.explorer_settings["allowed_origins"] == [
"https://app.example.com",
"https://team.example.com",
]
assert app.state.explorer_settings["falkordb_host"] == "falkordb.internal"
assert app.state.explorer_settings["falkordb_port"] == 6380
def test_env_settings_fall_back_to_legacy_cors_name(self, monkeypatch):
monkeypatch.delenv("ALLOWED_ORIGINS", raising=False)
monkeypatch.setenv("EXPLORER_CORS_ORIGINS", "https://legacy.example.com")
app = create_app()
assert app.state.explorer_settings["allowed_origins"] == ["https://legacy.example.com"]
def test_default_app_initializes_empty_graph_session(self):
with TestClient(create_app()) as test_client:
response = test_client.get("/api/graph/nodes")
assert response.status_code == 200
payload = response.json()
assert payload["nodes"] == []
assert payload["total"] == 0
class TestGraphNodes:
def test_list_nodes(self, client):