mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Merge pull request #684 from semantica-agi/issue-681-knowledge-explorer-deploy-templates
Add Knowledge Explorer deployment templates
This commit is contained in:
@@ -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
@@ -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/**
|
||||||
@@ -32,7 +32,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
MSDO:
|
MSDO:
|
||||||
# currently only windows latest is supported
|
# currently only windows-latest is supported
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -46,8 +46,43 @@ jobs:
|
|||||||
uses: microsoft/security-devops-action@v1.12.0
|
uses: microsoft/security-devops-action@v1.12.0
|
||||||
id: msdo
|
id: msdo
|
||||||
with:
|
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
|
- name: Upload results to Security tab
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
uses: github/codeql-action/upload-sarif@v4
|
||||||
with:
|
with:
|
||||||
sarif_file: ${{ steps.msdo.outputs.sarifFile }}
|
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
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ on:
|
|||||||
- 'requirements-docs.txt'
|
- 'requirements-docs.txt'
|
||||||
- '**/*.md'
|
- '**/*.md'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security-scan:
|
security-scan:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+27
-16
@@ -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
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
RUN npm install
|
PYTHONUNBUFFERED=1 \
|
||||||
|
FALKORDB_HOST=falkordb \
|
||||||
|
FALKORDB_PORT=6379 \
|
||||||
COPY semantica-explorer/ ./
|
ALLOWED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.14-slim AS runtime
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY pyproject.toml ./
|
RUN groupadd --system semantica \
|
||||||
COPY semantica/ ./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
|
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
|
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"]
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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: .
|
||||||
@@ -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}'
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
@@ -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: {}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: semantica
|
||||||
|
labels:
|
||||||
|
owner: semantica
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[build]
|
||||||
|
builder = "DOCKERFILE"
|
||||||
|
dockerfilePath = "Dockerfile"
|
||||||
|
|
||||||
|
[deploy]
|
||||||
|
healthcheckPath = "/api/health"
|
||||||
|
healthcheckTimeout = 300
|
||||||
|
restartPolicyType = "ON_FAILURE"
|
||||||
|
restartPolicyMaxRetries = 10
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
@@ -1,11 +1,35 @@
|
|||||||
services:
|
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:
|
falkordb:
|
||||||
image: falkordb/falkordb:latest
|
image: falkordb/falkordb:latest
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- falkordb_data:/data
|
- falkordb_data:/data
|
||||||
restart: always
|
networks:
|
||||||
|
- semantica
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
semantica:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
falkordb_data:
|
falkordb_data:
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ python -c "import semantica; print(semantica.__version__)"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:8000/health
|
curl http://localhost:8000/health
|
||||||
# {"status": "healthy"}
|
# {"status": "ok"}
|
||||||
|
|
||||||
curl http://localhost:8000/api/info
|
curl http://localhost:8000/api/info
|
||||||
# {"name": "Semantica API", "version": "...", "status": "active"}
|
# {"name": "Semantica API", "version": "...", "status": "active"}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ The browser opens at `http://127.0.0.1:8000`. The health endpoint confirms the s
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:8000/api/health
|
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` | Interactive dashboard |
|
||||||
| `http://127.0.0.1:8000/docs` | Swagger UI: every REST endpoint, interactive |
|
| `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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ Full interactive docs at `http://localhost:8000/docs`. All endpoints accept and
|
|||||||
|
|
||||||
| Endpoint | Method | Description |
|
| Endpoint | Method | Description |
|
||||||
| :-------- | :------ | :----------- |
|
| :-------- | :------ | :----------- |
|
||||||
| `/api/health` | `GET` | Returns `{"status": "healthy"}` |
|
| `/api/health` | `GET` | Returns `{"status": "ok"}` |
|
||||||
| `/api/info` | `GET` | Server name, version, status |
|
| `/api/info` | `GET` | Server name, version, status |
|
||||||
| `/docs` | `GET` | Interactive Swagger UI: all endpoints |
|
| `/docs` | `GET` | Interactive Swagger UI: all endpoints |
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { defineConfig } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import path from 'path'
|
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/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -57,11 +60,11 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:8000',
|
target: apiTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: 'ws://127.0.0.1:8000',
|
target: wsTarget,
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+43
-10
@@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Semantica Explorer FastAPI application factory.
|
Semantica Explorer FastAPI application factory.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -14,11 +14,44 @@ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
|
from ..context.context_graph import ContextGraph
|
||||||
from .session import GraphSession
|
from .session import GraphSession
|
||||||
from .ws import ConnectionManager
|
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:
|
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)
|
previous_callback = getattr(session.graph, "mutation_callback", None)
|
||||||
|
|
||||||
def on_mutation(event_type: str, entity_id: str, payload: dict) -> 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:
|
def create_app(session: Optional[GraphSession] = None) -> FastAPI:
|
||||||
|
active_session = session or GraphSession(ContextGraph(advanced_analytics=False))
|
||||||
|
settings = _read_explorer_settings()
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
app.state.event_loop = asyncio.get_running_loop()
|
app.state.event_loop = asyncio.get_running_loop()
|
||||||
app.state.ws_manager = ConnectionManager()
|
app.state.ws_manager = ConnectionManager()
|
||||||
if session is not None:
|
app.state.session = active_session
|
||||||
app.state.session = session
|
_install_mutation_bridge(app, active_session)
|
||||||
_install_mutation_bridge(app, session)
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@@ -59,10 +94,8 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
|
|||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
_raw_origins = os.environ.get(
|
app.state.explorer_settings = settings
|
||||||
"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()]
|
|
||||||
# allow_credentials lets browsers send cookies/auth headers cross-origin.
|
# allow_credentials lets browsers send cookies/auth headers cross-origin.
|
||||||
# The Explorer has no authentication, so credentials serve no purpose and
|
# The Explorer has no authentication, so credentials serve no purpose and
|
||||||
# enabling them when origins are broadened creates cross-site request risk.
|
# 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"
|
_allow_credentials = os.environ.get("EXPLORER_CORS_CREDENTIALS", "false").lower() == "true"
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=_cors_origins,
|
allow_origins=settings["allowed_origins"],
|
||||||
allow_credentials=_allow_credentials,
|
allow_credentials=_allow_credentials,
|
||||||
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
|
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["Content-Type", "Authorization"],
|
allow_headers=["Content-Type", "Authorization"],
|
||||||
@@ -169,7 +202,7 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
|
|||||||
|
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
async def health():
|
async def health():
|
||||||
return {"status": "healthy"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@app.get("/api/info")
|
@app.get("/api/info")
|
||||||
async def info():
|
async def info():
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Integration tests for the explorer API."""
|
"""Integration tests for the explorer API."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -151,7 +151,7 @@ class TestHealthInfo:
|
|||||||
def test_health(self, client):
|
def test_health(self, client):
|
||||||
response = client.get("/api/health")
|
response = client.get("/api/health")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["status"] == "healthy"
|
assert response.json() == {"status": "ok"}
|
||||||
|
|
||||||
def test_info(self, client):
|
def test_info(self, client):
|
||||||
response = client.get("/api/info")
|
response = client.get("/api/info")
|
||||||
@@ -161,6 +161,40 @@ class TestHealthInfo:
|
|||||||
assert payload["status"] == "active"
|
assert payload["status"] == "active"
|
||||||
assert payload["version"]
|
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:
|
class TestGraphNodes:
|
||||||
def test_list_nodes(self, client):
|
def test_list_nodes(self, client):
|
||||||
|
|||||||
Reference in New Issue
Block a user