Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29162a150a | |||
| b4f2ec3ce4 | |||
| bf98e2055e | |||
| 2c538a1cf9 | |||
| 1b827c05a6 | |||
| 2649ac6117 | |||
| fe0974d162 | |||
| 45ad5f5901 | |||
| 4e127f663b | |||
| 7975430489 | |||
| a4952581ec | |||
| 469dfcd094 | |||
| abd4ee798b | |||
| b3f58e6e4a | |||
| ab7eaa0581 | |||
| 26f4608c93 | |||
| f600e46537 | |||
| 8e8655e917 | |||
| e29c03aee7 | |||
| 8eb2ade823 | |||
| 7a195de392 | |||
| ddc95882ee | |||
| d07892566f | |||
| 678218a33e | |||
| d2101345dc | |||
| 899b866142 | |||
| e5cae3dc52 |
@@ -132,3 +132,8 @@ dist
|
||||
|
||||
__pycache__/
|
||||
.venv/
|
||||
|
||||
*.xml
|
||||
temp.*
|
||||
bun.lock
|
||||
tmp/
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
My openWebUI/searxng configs, plugins, RAG server, as well as a custom program that runs the AI's code in isolated Docker containers
|
||||
|
||||
*Last updated: 2025-09-10*
|
||||
*Last updated: 2025-09-13*
|
||||
|
||||
> [!TIP]
|
||||
> Looking for the compose version of this? See the [compose branch](https://git.ion606.com/ION606/ollama-plus/src/branch/compose/)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: AppProject
|
||||
metadata:
|
||||
name: ai-stack
|
||||
namespace: argocd
|
||||
spec:
|
||||
destinations:
|
||||
- server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
- server: https://kubernetes.default.svc
|
||||
namespace: argo
|
||||
# # only add if need to deploy into argocd (ehhhhh)
|
||||
# - server: https://kubernetes.default.svc
|
||||
# namespace: argocd
|
||||
sourceRepos: ["*"]
|
||||
|
||||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: ai-stack
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||
targetRevision: argo-2
|
||||
path: apps/children
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: argo-templates
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argo
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||
targetRevision: argo-2
|
||||
path: apps/argo-templates
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: browser
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus.git
|
||||
targetRevision: argo-2
|
||||
path: manifests/browser
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: coderunner
|
||||
namespace: argocd
|
||||
labels:
|
||||
repo.ion606.com/ollama-plus: "true"
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||
targetRevision: argo-2
|
||||
path: manifests/coderunner
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: debug-netshoot
|
||||
namespace: argocd
|
||||
labels:
|
||||
repo.ion606.com/ollama-plus: "true"
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||
targetRevision: argo-2
|
||||
path: manifests/debug
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,39 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ollama-scheduler.yaml
|
||||
- coderunner.yaml
|
||||
- tools.yaml
|
||||
- rag-server.yaml
|
||||
- openwebui.yaml
|
||||
- postgresql.yaml
|
||||
- searxng.yaml
|
||||
- browser.yaml
|
||||
- debug.yaml
|
||||
- policy.yaml
|
||||
- policy-argo.yaml
|
||||
|
||||
generatorOptions:
|
||||
disableNameSuffixHash: true
|
||||
|
||||
# FINDME: The global branch for my repo
|
||||
configMapGenerator:
|
||||
- name: ollama-plus-revs
|
||||
literals:
|
||||
- targetRevision=argo
|
||||
|
||||
# Inject targetRevision from the ConfigMap into apps (kill me)
|
||||
replacements:
|
||||
- source:
|
||||
kind: ConfigMap
|
||||
name: ollama-plus-revs
|
||||
fieldPath: data.targetRevision
|
||||
targets:
|
||||
- select:
|
||||
kind: Application
|
||||
labelSelector: repo.ion606.com/ollama-plus=true
|
||||
fieldPaths:
|
||||
- spec.source.targetRevision
|
||||
options:
|
||||
create: true
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: ollama-scheduler
|
||||
namespace: argocd
|
||||
labels:
|
||||
repo.ion606.com/ollama-plus: "true"
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argo
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||
targetRevision: argo-2
|
||||
path: manifests/argo-schedules-api
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: openwebui
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "0"
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://helm.openwebui.com
|
||||
chart: open-webui
|
||||
targetRevision: "*"
|
||||
helm:
|
||||
valueFiles:
|
||||
- apps/values/openwebui.yaml
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: policy-argo
|
||||
namespace: argocd
|
||||
labels:
|
||||
repo.ion606.com/ollama-plus: "true"
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argo
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||
targetRevision: argo-2
|
||||
path: manifests/policy-argo
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: policy
|
||||
namespace: argocd
|
||||
labels:
|
||||
repo.ion606.com/ollama-plus: "true"
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||
targetRevision: argo-2
|
||||
path: manifests/policy
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: postgresql
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-10"
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://charts.bitnami.com/bitnami
|
||||
chart: postgresql
|
||||
targetRevision: "*"
|
||||
helm:
|
||||
valueFiles:
|
||||
- apps/values/postgresql.yaml
|
||||
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: rag-server
|
||||
namespace: argocd
|
||||
labels:
|
||||
repo.ion606.com/ollama-plus: "true"
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||
targetRevision: argo-2
|
||||
path: manifests/rag-server
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: searxng
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://charts.kubito.dev
|
||||
chart: searxng
|
||||
targetRevision: "*"
|
||||
helm:
|
||||
valueFiles:
|
||||
- apps/values/searxng.yaml
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: tools
|
||||
namespace: argocd
|
||||
labels:
|
||||
repo.ion606.com/ollama-plus: "true"
|
||||
spec:
|
||||
project: ai-stack
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
source:
|
||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||
targetRevision: argo-2
|
||||
path: manifests/tools
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -0,0 +1,40 @@
|
||||
image:
|
||||
repository: ghcr.io/open-webui/open-webui
|
||||
tag: "main"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
type: NodePort # use NodePort instead of ClusterIP so openwebui is accessible externally
|
||||
nodePort: 30400
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
|
||||
ingress:
|
||||
enabled: false # disable ingress since we'll use NodePort
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: openwebui.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: [] # no https for local/minikube
|
||||
|
||||
# NO SECRETS!!!
|
||||
extraEnvVars:
|
||||
- name: OLLAMA_BASE_URL
|
||||
value: "https://mlep.ion606.com"
|
||||
# postgres === primary db
|
||||
- name: DATABASE_URL
|
||||
value: "postgresql://postgres:mypassword@postgresql-primary.ai.svc.cluster.local:5432/openwebui"
|
||||
# store vectors in pgvector (on the same postgres)
|
||||
- name: VECTOR_DB
|
||||
value: "pgvector"
|
||||
- name: PGVECTOR_DB_URL
|
||||
value: "postgresql://postgres:mypassword@postgresql-primary.ai.svc.cluster.local:5432/openwebui"
|
||||
- name: PGVECTOR_CREATE_EXTENSION
|
||||
value: "true"
|
||||
# no bish
|
||||
# - name: WEBUI_URL
|
||||
# value: "http://openwebui.local"
|
||||
@@ -0,0 +1,24 @@
|
||||
# stupid mismatch fix
|
||||
fullnameOverride: postgresql
|
||||
|
||||
architecture: replication
|
||||
|
||||
auth:
|
||||
username: postgres
|
||||
password: mypassword
|
||||
database: openwebui
|
||||
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 8Gi
|
||||
service:
|
||||
type: ClusterIP
|
||||
|
||||
readReplicas:
|
||||
replicaCount: 1
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 8Gi
|
||||
service:
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,21 @@
|
||||
image:
|
||||
repository: searxng/searxng
|
||||
tag: "latest"
|
||||
|
||||
service:
|
||||
type: NodePort # expose service externally
|
||||
nodePort: 30081
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: searxng.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
|
||||
# env:
|
||||
# SEARXNG_SECRET: "please-change-me"
|
||||
# # helps with URL generation & results links
|
||||
# BASE_URL: "http://searxng.local/"
|
||||
+2
-2
@@ -30,8 +30,8 @@ RUN mkdir -p /opt/web-ui/tmp /data && chown -R appuser:appuser /opt/web-ui /data
|
||||
|
||||
USER appuser
|
||||
|
||||
# copy default env
|
||||
COPY .env .env
|
||||
# # copy default env
|
||||
# COPY .env .env
|
||||
|
||||
EXPOSE 7788
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=5 \
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^1.3.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="],
|
||||
|
||||
"@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="],
|
||||
|
||||
"@kubernetes/client-node": ["@kubernetes/client-node@1.3.0", "", { "dependencies": { "@types/js-yaml": "^4.0.1", "@types/node": "^22.0.0", "@types/node-fetch": "^2.6.9", "@types/stream-buffers": "^3.0.3", "form-data": "^4.0.0", "hpagent": "^1.2.0", "isomorphic-ws": "^5.0.0", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.3.0", "node-fetch": "^2.6.9", "openid-client": "^6.1.3", "rfc4648": "^1.3.0", "socks-proxy-agent": "^8.0.4", "stream-buffers": "^3.0.2", "tar-fs": "^3.0.8", "ws": "^8.18.2" } }, "sha512-IE0yrIpOT97YS5fg2QpzmPzm8Wmcdf4ueWMn+FiJSI3jgTTQT1u+LUhoYpdfhdHAVxdrNsaBg2C0UXSnOgMoCQ=="],
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
"@types/stream-buffers": ["@types/stream-buffers@3.0.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-azOCy05sXVXrO+qklf0c/B07H/oHaIuDDAiHPVwlk3A9Ek+ksHyTeMajLZl3r76FxpPpxem//4Te61G1iW3Giw=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"b4a": ["b4a@1.7.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-ZovbrBV0g6JxK5cGUF1Suby1vLfKjv4RWi8IxoaO/Mon8BDD9I21RxjHFtgQ+kskJqLAVyQZly3uMBui+vhc8Q=="],
|
||||
|
||||
"bare-events": ["bare-events@2.6.1", "", {}, "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g=="],
|
||||
|
||||
"bare-fs": ["bare-fs@4.4.4", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw=="],
|
||||
|
||||
"bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="],
|
||||
|
||||
"bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
|
||||
|
||||
"bare-stream": ["bare-stream@2.7.0", "", { "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A=="],
|
||||
|
||||
"bare-url": ["bare-url@2.2.2", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hpagent": ["hpagent@1.2.0", "", {}, "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA=="],
|
||||
|
||||
"ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
|
||||
|
||||
"isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
|
||||
|
||||
"jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="],
|
||||
|
||||
"jsonpath-plus": ["jsonpath-plus@10.3.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"oauth4webapi": ["oauth4webapi@3.8.1", "", {}, "sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"openid-client": ["openid-client@6.8.0", "", { "dependencies": { "jose": "^6.1.0", "oauth4webapi": "^3.8.1" } }, "sha512-oG1d1nAVhIIE+JSjLS+7E9wY1QOJpZltkzlJdbZ7kEn7Hp3hqur2TEeQ8gLOHoHkhbRAGZJKoOnEQcLOQJuIyg=="],
|
||||
|
||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||
|
||||
"rfc4648": ["rfc4648@1.5.4", "", {}, "sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg=="],
|
||||
|
||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||
|
||||
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
|
||||
|
||||
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
|
||||
|
||||
"stream-buffers": ["stream-buffers@3.0.3", "", {}, "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw=="],
|
||||
|
||||
"streamx": ["streamx@2.22.1", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA=="],
|
||||
|
||||
"tar-fs": ["tar-fs@3.1.0", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
|
||||
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"@kubernetes/client-node/@types/node": ["@types/node@22.18.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw=="],
|
||||
|
||||
"@kubernetes/client-node/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
}
|
||||
}
|
||||
+101
-47
@@ -1,5 +1,6 @@
|
||||
import http from "node:http";
|
||||
import { spawn } from "node:child_process";
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
|
||||
|
||||
const PORT = Number(process.env.PORT || 8787);
|
||||
const HOST = "0.0.0.0";
|
||||
@@ -41,8 +42,14 @@ type fileType = {
|
||||
content: string
|
||||
}
|
||||
|
||||
// docker binary (or set DOCKER_BIN=podman)
|
||||
const DOCKER_BIN = process.env.DOCKER_BIN || "docker";
|
||||
const NS = process.env.NAMESPACE || "ai";
|
||||
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromDefault(); // in-cluster uses serviceaccount
|
||||
|
||||
const batch = kc.makeApiClient(k8s.BatchV1Api),
|
||||
core = kc.makeApiClient(k8s.CoreV1Api);
|
||||
|
||||
|
||||
// basic openapi for open webui
|
||||
const OPENAPI = {
|
||||
@@ -123,7 +130,7 @@ const OPENAPI = {
|
||||
};
|
||||
|
||||
|
||||
function sendJson(res, status, obj) {
|
||||
function sendJson(res: any, status: number, obj: any) {
|
||||
const body = JSON.stringify(obj);
|
||||
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
||||
res.end(body);
|
||||
@@ -175,63 +182,110 @@ async function ensureImage(spec: langObj) {
|
||||
// }
|
||||
}
|
||||
|
||||
async function waitForJobPod(core: k8s.CoreV1Api, jobName: string): Promise<string> {
|
||||
const labelSelector = `job-name=${jobName}`;
|
||||
for (; ;) {
|
||||
const pods = await core.listNamespacedPod({ namespace: NS, labelSelector });
|
||||
const pod = pods.items.find((p) => p.status?.phase === "Running" || p.status?.phase === "Succeeded" || p.status?.phase === "Failed");
|
||||
if (pod?.metadata?.name) return pod.metadata.name;
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
}
|
||||
};
|
||||
|
||||
async function waitForCompletionAndLogs(core: k8s.CoreV1Api, podName: string): Promise<{ status: string; stdout: string; stderr: string; }> {
|
||||
for (; ;) {
|
||||
const readReq = {
|
||||
name: podName,
|
||||
namespace: NS
|
||||
},
|
||||
p = await core.readNamespacedPod(readReq),
|
||||
phase = p.status?.phase ?? "Pending";
|
||||
|
||||
if (phase === "Succeeded" || phase === "Failed") {
|
||||
const logs = await core.readNamespacedPodLog(readReq);
|
||||
// stderr is not separated by the api; you can split by stream if needed
|
||||
return { status: phase, stdout: logs, stderr: "" };
|
||||
};
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function runInContainer({ language, code, args = [], files = [] }: {
|
||||
language: string,
|
||||
code: string,
|
||||
args: string[],
|
||||
files: fileType[]
|
||||
language: string, code: string, args: string[], files: fileType[]
|
||||
}) {
|
||||
if (!LANGS[language]) throw new Error(`language not allowed: ${language}`);
|
||||
const spec = LANGS[language];
|
||||
if (!(language in LANGS)) throw new Error(`language not allowed: ${language}`);
|
||||
const spec = LANGS[language as keyof typeof LANGS];
|
||||
|
||||
await ensureImage(spec);
|
||||
|
||||
// build the Docker args
|
||||
const dockerArgs = [
|
||||
"run", "--rm",
|
||||
"--network=none", "--read-only",
|
||||
"--pids-limit=256",
|
||||
"--cpus=1", "--memory=512m",
|
||||
"--cap-drop=ALL", "--security-opt", "no-new-privileges",
|
||||
"--tmpfs", "/work:rw,exec,size=64m",
|
||||
"-w", "/work",
|
||||
"--pull=never",
|
||||
spec.image
|
||||
];
|
||||
|
||||
// inside the container, write the files and run code
|
||||
// build the same shell script you already use
|
||||
const script = [
|
||||
// write the main file using base64
|
||||
`echo ${JSON.stringify(Buffer.from(code, "utf8").toString("base64"))} | base64 -d > ${spec.filename}`,
|
||||
// write any extra files using base64
|
||||
...files.flatMap((f) => [
|
||||
`mkdir -p "$(dirname "${f.path}")"`,
|
||||
`echo ${JSON.stringify(Buffer.from(f.content, "utf8").toString("base64"))} | base64 -d > "${f.path}"`,
|
||||
]),
|
||||
// run it
|
||||
`${spec.run.join(' ')} ${args.map((a) => JSON.stringify(a)).join(' ')}`
|
||||
].join('\n');
|
||||
|
||||
dockerArgs.push("sh", "-lc", script);
|
||||
const uid = crypto.randomUUID().slice(0, 8);
|
||||
const jobName = `coderun-${uid}`;
|
||||
|
||||
const result = await new Promise((resolve) => {
|
||||
const child = spawn(DOCKER_BIN, dockerArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
||||
let stdout = "", stderr = "";
|
||||
// create a short-lived Job with tight security and resource caps
|
||||
const job: k8s.V1Job = {
|
||||
apiVersion: "batch/v1",
|
||||
kind: "Job",
|
||||
metadata: { name: jobName, namespace: NS },
|
||||
spec: {
|
||||
ttlSecondsAfterFinished: 300, // auto-clean once done
|
||||
backoffLimit: 0, // no retries
|
||||
activeDeadlineSeconds: 25, // mirrors your 25s timeout
|
||||
template: {
|
||||
metadata: { labels: { app: "coderunner-task" } },
|
||||
spec: {
|
||||
restartPolicy: "Never",
|
||||
securityContext: {
|
||||
runAsNonRoot: true,
|
||||
seccompProfile: { type: "RuntimeDefault" }
|
||||
},
|
||||
containers: [{
|
||||
name: "task",
|
||||
image: spec.image,
|
||||
command: ["sh", "-lc", script],
|
||||
resources: {
|
||||
requests: { cpu: "1", memory: "512Mi" },
|
||||
limits: { cpu: "1", memory: "512Mi" }
|
||||
},
|
||||
securityContext: {
|
||||
allowPrivilegeEscalation: false,
|
||||
readOnlyRootFilesystem: true,
|
||||
capabilities: { drop: ["ALL"] }
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
resolve({ stdout, stderr: stderr + "\n[killed: timeout]", exitCode: 137, timedOut: true });
|
||||
}, 25_000);
|
||||
|
||||
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
||||
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timer);
|
||||
resolve({ stdout, stderr, exitCode: code ?? 1, timedOut: false });
|
||||
});
|
||||
await batch.createNamespacedJob({
|
||||
namespace: NS,
|
||||
body: job
|
||||
});
|
||||
|
||||
return result;
|
||||
// wait for pod to complete, then get logs
|
||||
const podName = await waitForJobPod(core, jobName);
|
||||
const { status, stdout, stderr } = await waitForCompletionAndLogs(core, podName);
|
||||
|
||||
// delete job for hygiene (ttl also cleans it eventually)
|
||||
try {
|
||||
await batch.deleteNamespacedJob({ namespace: NS, propagationPolicy: "Background", name: jobName });
|
||||
} catch (err) { console.error(err); };
|
||||
|
||||
return {
|
||||
stdout,
|
||||
stderr,
|
||||
exitCode: status === "Succeeded" ? 0 : 1,
|
||||
timedOut: status === "Failed"
|
||||
};
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
@@ -257,7 +311,7 @@ const server = http.createServer(async (req, res) => {
|
||||
const payload = JSON.parse(body || "{}");
|
||||
const out = await runInContainer(payload);
|
||||
sendJson(res, 200, out);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
sendJson(res, 400, { error: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
@@ -266,7 +320,7 @@ const server = http.createServer(async (req, res) => {
|
||||
|
||||
res.writeHead(404);
|
||||
res.end("not found");
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
sendJson(res, 500, { error: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
Generated
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "coderunner",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
|
||||
"integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
services:
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
container_name: open-webui
|
||||
ports:
|
||||
- "4000:8080"
|
||||
volumes:
|
||||
- open-webui:/app/backend/data
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
restart: always
|
||||
depends_on:
|
||||
- postgres
|
||||
- tools
|
||||
networks:
|
||||
- internal
|
||||
|
||||
tools:
|
||||
container_name: openwebui_tools
|
||||
build:
|
||||
context: ./tools
|
||||
dockerfile: Dockerfile
|
||||
env_file: .env
|
||||
restart: on-failure
|
||||
|
||||
networks:
|
||||
- internal
|
||||
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
container_name: openwebui_postgres
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=mypassword
|
||||
- POSTGRES_DB=openwebui_db
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- internal
|
||||
|
||||
# 8080
|
||||
searxng:
|
||||
image: searxng/searxng:latest
|
||||
container_name: searxng
|
||||
volumes:
|
||||
- ./searxng.yml:/etc/searxng/settings.yml:ro,Z
|
||||
- searxng_data:/etc/searxng:rw
|
||||
restart: always
|
||||
|
||||
# DELETEME: for local testing only (extern port closed)
|
||||
ports:
|
||||
- "4001:8080"
|
||||
networks:
|
||||
- internal
|
||||
|
||||
coderunner:
|
||||
build:
|
||||
context: ./coderunner
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8787/openapi.json"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
user: "1000:1000"
|
||||
group_add:
|
||||
- "977"
|
||||
|
||||
# death
|
||||
environment:
|
||||
DOCKER_HOST: "unix:///var/run/docker.sock"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:Z
|
||||
# - ./tmp:/tmp
|
||||
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /run:rw,nosuid,nodev
|
||||
- /tmp:rw,exec,nosuid,nodev,size=64m
|
||||
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
- label=disable
|
||||
networks:
|
||||
- internal
|
||||
|
||||
browser:
|
||||
build:
|
||||
context: ./browser
|
||||
dockerfile: Dockerfile
|
||||
container_name: browser
|
||||
networks:
|
||||
- internal
|
||||
# playwright/chromium has larger /dev/shm :D
|
||||
shm_size: "1gb"
|
||||
user: "1000:1000"
|
||||
environment:
|
||||
WEBUI_IP: "0.0.0.0"
|
||||
WEBUI_PORT: "7788"
|
||||
ports:
|
||||
- "7788:7788"
|
||||
tmpfs:
|
||||
- /opt/web-ui/tmp:rw,exec,nosuid,nodev,mode=1777,size=64m
|
||||
volumes:
|
||||
- webui_data:/data
|
||||
# - webui_env:/opt/web-ui/.env
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:7788').read()"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
open-webui:
|
||||
pgdata:
|
||||
searxng_data:
|
||||
webui_data:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,59 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ollama-scheduler
|
||||
namespace: argo
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ollama-scheduler
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ollama-scheduler
|
||||
spec:
|
||||
serviceAccountName: ollama-scheduler
|
||||
containers:
|
||||
- name: ollama-scheduler
|
||||
image: docker.io/ion606/ollama-scheduler:0.1.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: PORT
|
||||
value: "12253"
|
||||
- name: NS
|
||||
value: "argo"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 12253
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 12253
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 12253
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
requests:
|
||||
cpu: "50m"
|
||||
memory: "64Mi"
|
||||
limits:
|
||||
cpu: "200m"
|
||||
memory: "256Mi"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ollama-scheduler
|
||||
namespace: argo
|
||||
spec:
|
||||
selector:
|
||||
app: ollama-scheduler
|
||||
ports:
|
||||
- name: http
|
||||
port: 12253
|
||||
targetPort: 12253
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ollama-scheduler
|
||||
namespace: argo
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: scheduler.local
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ollama-scheduler
|
||||
port:
|
||||
number: 12253
|
||||
@@ -0,0 +1,37 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: ollama-scheduler
|
||||
namespace: argo
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: ollama-scheduler
|
||||
namespace: argo
|
||||
rules:
|
||||
- apiGroups: ["argoproj.io"]
|
||||
resources: ["cronworkflows"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
- apiGroups: ["argoproj.io"]
|
||||
resources: ["workflows"]
|
||||
verbs: ["create", "get", "list"]
|
||||
- apiGroups: ["argoproj.io"]
|
||||
resources: ["workflowtemplates"]
|
||||
verbs: ["get", "list"]
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: ollama-scheduler
|
||||
namespace: argo
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: ollama-scheduler
|
||||
namespace: argo
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: ollama-scheduler
|
||||
@@ -0,0 +1,17 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: WorkflowTemplate
|
||||
metadata:
|
||||
name: hello-template
|
||||
namespace: argo
|
||||
spec:
|
||||
entrypoint: run
|
||||
arguments:
|
||||
parameters:
|
||||
- { name: message, value: "hello from argo" }
|
||||
templates:
|
||||
- name: run
|
||||
container:
|
||||
image: alpine:3.19
|
||||
command: ["/bin/sh","-lc"]
|
||||
args: ["echo \"{{workflow.parameters.message}}\""]
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: WorkflowTemplate
|
||||
metadata:
|
||||
name: ollama-job-template
|
||||
namespace: argo
|
||||
spec:
|
||||
entrypoint: run
|
||||
arguments:
|
||||
parameters:
|
||||
- { name: task, value: "quick-check" }
|
||||
- { name: prompt, value: "reindex embeddings" }
|
||||
templates:
|
||||
- name: run
|
||||
container:
|
||||
# Replace with an image that can reach your Ollama endpoint
|
||||
image: curlimages/curl:8.9.0
|
||||
command: ["/bin/sh","-lc"]
|
||||
args:
|
||||
- >-
|
||||
echo "task: {{workflow.parameters.task}}";
|
||||
echo "prompt: {{workflow.parameters.prompt}}";
|
||||
# Example call (adjust host/port or service DNS):
|
||||
# curl -s http://ollama.ai.svc.cluster.local:11434/api/generate \
|
||||
# -H 'content-type: application/json' \
|
||||
# -d '{"model":"llama3.1","prompt":"{{workflow.parameters.prompt}}"}' | tee /tmp/out.json;
|
||||
echo done.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: { name: browser, namespace: argocd }
|
||||
spec:
|
||||
replicas: 1
|
||||
selector: { matchLabels: { app: browser } }
|
||||
template:
|
||||
metadata: { labels: { app: browser } }
|
||||
spec:
|
||||
containers:
|
||||
- name: browser
|
||||
image: docker.io/ion606/browser:latest
|
||||
ports: [{ containerPort: 7788 }]
|
||||
env:
|
||||
- { name: WEBUI_IP, value: "0.0.0.0" }
|
||||
- { name: WEBUI_PORT, value: "7788" }
|
||||
resources:
|
||||
requests: { cpu: "250m", memory: "256Mi" }
|
||||
limits: { cpu: "1", memory: "1Gi" } # hard cap
|
||||
readinessProbe:
|
||||
httpGet: { path: "/", port: 7788 }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet: { path: "/", port: 7788 }
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata: { name: browser, namespace: argocd }
|
||||
spec:
|
||||
selector: { app: browser }
|
||||
ports:
|
||||
- name: http
|
||||
port: 7788
|
||||
targetPort: 7788
|
||||
nodePort: 30788
|
||||
type: NodePort
|
||||
@@ -0,0 +1,43 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: { name: coderunner, namespace: argocd }
|
||||
spec:
|
||||
replicas: 1
|
||||
selector: { matchLabels: { app: coderunner } }
|
||||
template:
|
||||
metadata: { labels: { app: coderunner } }
|
||||
spec:
|
||||
serviceAccountName: coderunner-sa
|
||||
containers:
|
||||
- name: coderunner
|
||||
image: docker.io/ion606/coderunner:latest
|
||||
ports: [{ containerPort: 8787 }]
|
||||
env:
|
||||
- { name: PORT, value: "8787" }
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
readinessProbe:
|
||||
httpGet: { path: "/openapi.json", port: 8787 }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet: { path: "/openapi.json", port: 8787 }
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
requests: { cpu: "100m", memory: "128Mi" }
|
||||
limits: { cpu: "500m", memory: "512Mi" }
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata: { name: coderunner, namespace: argocd }
|
||||
spec:
|
||||
selector: { app: coderunner }
|
||||
ports:
|
||||
- name: http
|
||||
port: 8787
|
||||
targetPort: 8787
|
||||
nodePort: 31787
|
||||
type: NodePort
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata: { name: coderunner-sa, namespace: argocd }
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata: { name: coderunner-job-role, namespace: argocd }
|
||||
rules:
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["create", "get", "list", "watch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "pods/log"]
|
||||
verbs: ["get", "list", "watch", "delete"]
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata: { name: coderunner-job-rb, namespace: argocd }
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: coderunner-sa
|
||||
namespace: argocd
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: coderunner-job-role
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- netshoot.yaml
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: netshoot
|
||||
namespace: argocd
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: netshoot
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: netshoot
|
||||
spec:
|
||||
containers:
|
||||
- name: netshoot
|
||||
image: nicolaka/netshoot:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: ["/bin/sh", "-c", "sleep infinity"]
|
||||
securityContext:
|
||||
capabilities:
|
||||
add: ["NET_ADMIN", "NET_RAW"]
|
||||
resources:
|
||||
requests: { cpu: "50m", memory: "64Mi" }
|
||||
limits: { cpu: "200m", memory: "256Mi" }
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../policy/allow-ollama-scheduler-ingress.yaml
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-browser-ingress
|
||||
namespace: argocd
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: browser
|
||||
policyTypes: ["Ingress"]
|
||||
ingress:
|
||||
- from:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
ports:
|
||||
- { protocol: TCP, port: 7788 }
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-common-egress
|
||||
namespace: argocd
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes: ["Egress"]
|
||||
egress:
|
||||
# Allow DNS to kube-dns/CoreDNS in kube-system
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- { protocol: UDP, port: 53 }
|
||||
- { protocol: TCP, port: 53 }
|
||||
|
||||
# Allow PostgreSQL to services/pods in namespace ai on 5432
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: ai
|
||||
ports:
|
||||
- { protocol: TCP, port: 5432 }
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-https-egress
|
||||
namespace: argocd
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes: ["Egress"]
|
||||
egress:
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
# exclude RFC1918/private ranges so this only permits Internet egress
|
||||
except:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
ports:
|
||||
- { protocol: TCP, port: 443 }
|
||||
- { protocol: TCP, port: 80 }
|
||||
@@ -0,0 +1,17 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ollama-scheduler-ingress
|
||||
namespace: argo
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: ollama-scheduler
|
||||
policyTypes: ["Ingress"]
|
||||
ingress:
|
||||
- from:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
ports:
|
||||
- { protocol: TCP, port: 12253 }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-openwebui-ingress
|
||||
namespace: argocd
|
||||
spec:
|
||||
# Select the Open WebUI pods deployed by the Helm release "openwebui"
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: openwebui
|
||||
policyTypes: ["Ingress"]
|
||||
ingress:
|
||||
- from:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
ports:
|
||||
# Open WebUI typically listens on 8080 (chart default), sometimes 80
|
||||
- { protocol: TCP, port: 8080 }
|
||||
- { protocol: TCP, port: 80 }
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: default-deny-all
|
||||
namespace: argocd
|
||||
spec:
|
||||
podSelector: {} # die
|
||||
policyTypes: ["Ingress", "Egress"]
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- default-deny.yaml
|
||||
- allow-openwebui-ingress.yaml
|
||||
- allow-browser-ingress.yaml
|
||||
- allow-common-egress.yaml
|
||||
- allow-https-egress.yaml
|
||||
@@ -0,0 +1,41 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: { name: rag-server, namespace: argocd }
|
||||
spec:
|
||||
replicas: 1
|
||||
selector: { matchLabels: { app: rag-server } }
|
||||
template:
|
||||
metadata: { labels: { app: rag-server } }
|
||||
spec:
|
||||
containers:
|
||||
- name: rag-server
|
||||
image: docker.io/ion606/rag-server:latest
|
||||
ports: [{ containerPort: 8788 }]
|
||||
env:
|
||||
- { name: PORT, value: "8788" }
|
||||
- {
|
||||
name: OLLAMA_BASE,
|
||||
value: "http://ollama.ai.svc.cluster.local:11434",
|
||||
}
|
||||
- { name: OLLAMA_CHAT_MODEL, value: "llama3.1" }
|
||||
- { name: OLLAMA_EMBED_MODEL, value: "nomic-embed-text" }
|
||||
readinessProbe:
|
||||
httpGet: { path: "/openapi.json", port: 8788 }
|
||||
livenessProbe:
|
||||
httpGet: { path: "/", port: 8788 }
|
||||
initialDelaySeconds: 10
|
||||
resources:
|
||||
requests: { cpu: "200m", memory: "256Mi" }
|
||||
limits: { cpu: "1", memory: "1Gi" }
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata: { name: rag-server, namespace: argocd }
|
||||
spec:
|
||||
selector: { app: rag-server }
|
||||
ports:
|
||||
- name: http
|
||||
port: 8788
|
||||
targetPort: 8788
|
||||
nodePort: 31788
|
||||
type: NodePort
|
||||
@@ -0,0 +1,37 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: { name: tools, namespace: argocd }
|
||||
spec:
|
||||
replicas: 1
|
||||
selector: { matchLabels: { app: tools } }
|
||||
template:
|
||||
metadata: { labels: { app: tools } }
|
||||
spec:
|
||||
containers:
|
||||
- name: tools
|
||||
image: docker.io/ion606/tools:latest
|
||||
ports: [{ containerPort: 1331 }]
|
||||
env:
|
||||
- { name: HOST, value: "0.0.0.0" }
|
||||
- { name: PORT, value: "1331" }
|
||||
- { name: ROKU_IP, value: "192.0.2.10" }
|
||||
readinessProbe:
|
||||
httpGet: { path: "/roku/openapi.json", port: 1331 }
|
||||
livenessProbe:
|
||||
httpGet: { path: "/roku/openapi.json", port: 1331 }
|
||||
initialDelaySeconds: 10
|
||||
resources:
|
||||
requests: { cpu: "100m", memory: "128Mi" }
|
||||
limits: { cpu: "500m", memory: "512Mi" }
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata: { name: tools, namespace: argocd }
|
||||
spec:
|
||||
selector: { app: tools }
|
||||
ports:
|
||||
- name: http
|
||||
port: 1331
|
||||
targetPort: 1331
|
||||
nodePort: 31331
|
||||
type: NodePort
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
npm-cache
|
||||
bun.lock
|
||||
bun.lockb
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM oven/bun:1 as base
|
||||
WORKDIR /app
|
||||
|
||||
# prod deps
|
||||
COPY package.json ./package.json
|
||||
RUN bun install --ci --production
|
||||
|
||||
COPY server.mjs ./server.mjs
|
||||
COPY public ./public
|
||||
|
||||
USER bun
|
||||
EXPOSE 12253
|
||||
ENV NODE_ENV=production
|
||||
CMD ["bun", "run", "server.mjs"]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "ollama-scheduler",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun run server.mjs",
|
||||
"dev": "bun run --hot server.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^0.22.1",
|
||||
"@types/node": "^24.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Schedules UI</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Manage Your Tasks and Follow-Ups!</h1>
|
||||
|
||||
<!-- login card -->
|
||||
<section class="card" id="auth">
|
||||
<h2>login</h2>
|
||||
<p class="muted">
|
||||
enter your open webui user id (uuid). this is sent as the
|
||||
<code class="inline">x-user-id</code> header on api requests.
|
||||
</p>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="userId">user id (uuid)</label>
|
||||
<input
|
||||
id="userId"
|
||||
name="userId"
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. 5a8d1d7e-..." />
|
||||
</div>
|
||||
<div>
|
||||
<label for="displayName">display name (optional)</label>
|
||||
<input
|
||||
id="displayName"
|
||||
name="displayName"
|
||||
type="text"
|
||||
placeholder="your name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" style="margin-top: 0.75rem">
|
||||
<button type="submit">save & set header</button>
|
||||
<button type="button" id="logoutBtn">logout</button>
|
||||
</div>
|
||||
<p id="authStatus" class="muted"></p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- schedules list -->
|
||||
<section class="card">
|
||||
<div
|
||||
class="actions"
|
||||
style="justify-content: space-between; align-items: center">
|
||||
<h2 style="margin: 0">your schedules</h2>
|
||||
<div class="actions">
|
||||
<button id="refreshBtn">refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="listStatus"
|
||||
class="muted"
|
||||
style="margin: 0.4rem 0 0.6rem"></div>
|
||||
<div style="overflow: auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>name</th>
|
||||
<th>schedules</th>
|
||||
<th>tz</th>
|
||||
<th>template</th>
|
||||
<th>entrypoint</th>
|
||||
<th>one-shot</th>
|
||||
<th>actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="schedulesTbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- create/update schedule -->
|
||||
<section class="card">
|
||||
<h2>create / update schedule</h2>
|
||||
<form id="createForm">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="name">name</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="daily-report" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="tz">timezone</label>
|
||||
<input
|
||||
id="tz"
|
||||
name="tz"
|
||||
type="text"
|
||||
value="America/New_York" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="iso"
|
||||
>run at (iso datetime, or leave empty if using
|
||||
cron)</label
|
||||
>
|
||||
<input id="iso" name="iso" type="datetime-local" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="cron"
|
||||
>cron (min hour day month *), if not using
|
||||
iso</label
|
||||
>
|
||||
<input
|
||||
id="cron"
|
||||
name="cron"
|
||||
type="text"
|
||||
placeholder="30 9 * * *" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="templateName">workflow template</label>
|
||||
<input
|
||||
id="templateName"
|
||||
name="templateName"
|
||||
type="text"
|
||||
required
|
||||
placeholder="report-template" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="entrypoint">entrypoint (optional)</label>
|
||||
<input
|
||||
id="entrypoint"
|
||||
name="entrypoint"
|
||||
type="text"
|
||||
placeholder="main" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<label
|
||||
><input id="clusterScope" type="checkbox" />
|
||||
template is cluster-scoped</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
><input id="oneShot" type="checkbox" /> stop after
|
||||
first success (one-shot)</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="params">parameters (json object)</label>
|
||||
<textarea
|
||||
id="params"
|
||||
name="params"
|
||||
placeholder='{"report_kind":"summary"}'></textarea>
|
||||
|
||||
<div class="actions" style="margin-top: 0.75rem">
|
||||
<button type="submit">upsert schedule</button>
|
||||
<button type="button" id="loadTemplatesBtn">
|
||||
load workflow templates
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="createStatus"
|
||||
class="muted"
|
||||
style="margin-top: 0.5rem"></div>
|
||||
</form>
|
||||
|
||||
<details style="margin-top: 0.75rem">
|
||||
<summary class="muted">available workflow templates</summary>
|
||||
<ul id="templatesUl" class="muted"></ul>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<!-- run now -->
|
||||
<section class="card">
|
||||
<h2>run now</h2>
|
||||
<form id="runNowForm">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="rnName">name (label only)</label>
|
||||
<input
|
||||
id="rnName"
|
||||
type="text"
|
||||
placeholder="ad-hoc-run" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="rnTemplateName">workflow template</label>
|
||||
<input
|
||||
id="rnTemplateName"
|
||||
type="text"
|
||||
placeholder="report-template"
|
||||
required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="rnEntrypoint">entrypoint (optional)</label>
|
||||
<input
|
||||
id="rnEntrypoint"
|
||||
type="text"
|
||||
placeholder="main" />
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
><input id="rnClusterScope" type="checkbox" />
|
||||
template is cluster-scoped</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<label for="rnParams">parameters (json object)</label>
|
||||
<textarea
|
||||
id="rnParams"
|
||||
placeholder='{"report_kind":"summary"}'></textarea>
|
||||
|
||||
<div class="actions" style="margin-top: 0.75rem">
|
||||
<button type="submit">run now</button>
|
||||
</div>
|
||||
<div
|
||||
id="runNowStatus"
|
||||
class="muted"
|
||||
style="margin-top: 0.5rem"></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script type="module" src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,267 @@
|
||||
const state = {
|
||||
userId: localStorage.getItem("userId") || "",
|
||||
displayName: localStorage.getItem("displayName") || "",
|
||||
};
|
||||
|
||||
const $ = (sel) => document.querySelector(sel),
|
||||
setText = (sel, v) => {
|
||||
const el = $(sel);
|
||||
if (el) el.textContent = v;
|
||||
};
|
||||
|
||||
const authStatusEl = $("#authStatus"),
|
||||
listStatusEl = $("#listStatus"),
|
||||
createStatusEl = $("#createStatus"),
|
||||
runNowStatusEl = $("#runNowStatus"),
|
||||
schedulesTbody = $("#schedulesTbody"),
|
||||
templatesUl = $("#templatesUl");
|
||||
|
||||
// update login ui from state
|
||||
function paintAuth() {
|
||||
$("#userId").value = state.userId || "";
|
||||
$("#displayName").value = state.displayName || "";
|
||||
if (state.userId) {
|
||||
authStatusEl.textContent = `logged in as ${state.displayName ? state.displayName + " · " : ""
|
||||
}${state.userId}`;
|
||||
} else {
|
||||
authStatusEl.textContent = "not logged in";
|
||||
}
|
||||
}
|
||||
|
||||
// wrap fetch to always attach x-user-id
|
||||
async function apiFetch(url, options = {}) {
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (!state.userId)
|
||||
throw new Error(
|
||||
"no user id set — use the login form first"
|
||||
);
|
||||
headers.set("x-user-id", state.userId); // custom header
|
||||
if (
|
||||
!headers.has("content-type") &&
|
||||
options.body &&
|
||||
!(options.body instanceof FormData)
|
||||
) {
|
||||
headers.set("content-type", "application/json");
|
||||
}
|
||||
|
||||
const resp = await fetch(url, { ...options, headers });
|
||||
if (!resp.ok) {
|
||||
// try to surface json error bodies
|
||||
let msg = `${resp.status} ${resp.statusText}`;
|
||||
try {
|
||||
const data = await resp.json();
|
||||
if (data && data.error) msg = data.error;
|
||||
} catch { }
|
||||
throw new Error(msg);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
// render list
|
||||
function renderSchedules(items = []) {
|
||||
schedulesTbody.innerHTML = "";
|
||||
items.forEach((it) => {
|
||||
const tr = document.createElement("tr");
|
||||
const tRef = it.templateRef
|
||||
? it.templateRef.clusterScope
|
||||
? `(cluster) ${it.templateRef.name}`
|
||||
: it.templateRef.name
|
||||
: "";
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(it.displayName || it.name || "")}</td>
|
||||
<td>${(it.schedules || []).map(escapeHtml).join("<br/>")}</td>
|
||||
<td>${escapeHtml(it.timezone || "")}</td>
|
||||
<td>${escapeHtml(tRef)}</td>
|
||||
<td>${escapeHtml(it.entrypoint || "")}</td>
|
||||
<td>${it.oneShot ? "yes" : "no"}</td>
|
||||
<td class="actions">
|
||||
<button data-del="${encodeURIComponent(it.name)}">delete</button>
|
||||
</td>
|
||||
`;
|
||||
schedulesTbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// tiny escape helper
|
||||
function escapeHtml(s = "") {
|
||||
return String(s)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
|
||||
// wire up events
|
||||
$("#loginForm").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const userId = $("#userId").value.trim(),
|
||||
displayName = $("#displayName").value.trim();
|
||||
if (!userId) {
|
||||
authStatusEl.textContent = "please enter a user id";
|
||||
return;
|
||||
}
|
||||
state.userId = userId;
|
||||
state.displayName = displayName;
|
||||
localStorage.setItem("userId", state.userId);
|
||||
localStorage.setItem("displayName", state.displayName);
|
||||
paintAuth();
|
||||
});
|
||||
|
||||
$("#logoutBtn").addEventListener("click", () => {
|
||||
localStorage.removeItem("userId");
|
||||
localStorage.removeItem("displayName");
|
||||
state.userId = "";
|
||||
state.displayName = "";
|
||||
paintAuth();
|
||||
});
|
||||
|
||||
$("#refreshBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
listStatusEl.textContent = "loading...";
|
||||
const res = await apiFetch("/api/schedules");
|
||||
const data = await res.json();
|
||||
renderSchedules(data.items || []);
|
||||
listStatusEl.textContent = `loaded ${Array.isArray(data.items) ? data.items.length : 0
|
||||
} schedule(s)`;
|
||||
} catch (e) {
|
||||
listStatusEl.textContent = `error: ${e.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
// delete handler (delegated)
|
||||
schedulesTbody.addEventListener("click", async (e) => {
|
||||
const target = e.target;
|
||||
if (!(target instanceof HTMLButtonElement)) return;
|
||||
|
||||
const name = target.getAttribute("data-del");
|
||||
if (!name) return;
|
||||
|
||||
try {
|
||||
target.disabled = true;
|
||||
const res = await apiFetch(`/schedules/${name}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (res.status === 204) {
|
||||
target.closest("tr")?.remove();
|
||||
listStatusEl.textContent = "deleted";
|
||||
} else {
|
||||
listStatusEl.textContent = "unexpected response";
|
||||
}
|
||||
} catch (err) {
|
||||
listStatusEl.textContent = `error: ${err.message}`;
|
||||
} finally {
|
||||
target.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// create/update schedule
|
||||
$("#createForm").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
createStatusEl.textContent = "saving...";
|
||||
const name = $("#name").value.trim(),
|
||||
tz = $("#tz").value.trim() || "America/New_York",
|
||||
iso = $("#iso").value
|
||||
? new Date($("#iso").value).toISOString()
|
||||
: "",
|
||||
cron = $("#cron").value.trim(),
|
||||
templateName = $("#templateName").value.trim(),
|
||||
entrypoint = $("#entrypoint").value.trim(),
|
||||
clusterScope = $("#clusterScope").checked,
|
||||
oneShot = $("#oneShot").checked,
|
||||
paramsRaw = $("#params").value.trim();
|
||||
|
||||
let parameters = {};
|
||||
if (paramsRaw) {
|
||||
try {
|
||||
parameters = JSON.parse(paramsRaw);
|
||||
} catch {
|
||||
throw new Error("parameters must be valid json");
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
when: cron ? { cron } : { iso },
|
||||
tz,
|
||||
oneShot,
|
||||
template: { name: templateName, clusterScope },
|
||||
parameters,
|
||||
entrypoint: entrypoint || undefined,
|
||||
};
|
||||
|
||||
await apiFetch("/schedules", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
createStatusEl.textContent = "saved ✅";
|
||||
$("#refreshBtn").click();
|
||||
} catch (err) {
|
||||
createStatusEl.textContent = `error: ${err.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
// run now
|
||||
$("#runNowForm").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
runNowStatusEl.textContent = "starting...";
|
||||
const name = $("#rnName").value.trim() || "ad-hoc",
|
||||
templateName = $("#rnTemplateName").value.trim(),
|
||||
entrypoint = $("#rnEntrypoint").value.trim(),
|
||||
clusterScope = $("#rnClusterScope").checked,
|
||||
paramsRaw = $("#rnParams").value.trim();
|
||||
|
||||
let parameters = {};
|
||||
if (paramsRaw) {
|
||||
try {
|
||||
parameters = JSON.parse(paramsRaw);
|
||||
} catch {
|
||||
throw new Error("parameters must be valid json");
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
template: { name: templateName, clusterScope },
|
||||
entrypoint: entrypoint || undefined,
|
||||
parameters,
|
||||
};
|
||||
|
||||
await apiFetch("/run-now", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
runNowStatusEl.textContent = "started ✅";
|
||||
} catch (err) {
|
||||
runNowStatusEl.textContent = `error: ${err.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
// load workflow templates for convenience
|
||||
$("#loadTemplatesBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
templatesUl.innerHTML = "";
|
||||
templatesUl.parentElement.open = true;
|
||||
|
||||
const res = await apiFetch("/api/workflowtemplates"),
|
||||
data = await res.json();
|
||||
|
||||
(data.items || []).forEach((t) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = t.name;
|
||||
templatesUl.appendChild(li);
|
||||
});
|
||||
} catch (e) {
|
||||
templatesUl.innerHTML = `<li class="danger">error: ${escapeHtml(
|
||||
e.message
|
||||
)}</li>`;
|
||||
}
|
||||
});
|
||||
|
||||
// boot
|
||||
paintAuth();
|
||||
|
||||
// auto-refresh if already logged in
|
||||
if (state.userId) $("#refreshBtn").click();
|
||||
@@ -0,0 +1,96 @@
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 2rem;
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
form,
|
||||
.card {
|
||||
border: 1px solid #ccc;
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0.25rem 0 0.15rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="datetime-local"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #bbb;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 96px;
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.55rem 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #888;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 0.6rem;
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.muted {
|
||||
opacity: 0.75;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #a30000;
|
||||
}
|
||||
|
||||
.ok {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
code.inline {
|
||||
padding: 0.15rem 0.3rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import http from 'http'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { KubeConfig, CustomObjectsApi } from '@kubernetes/client-node'
|
||||
|
||||
const GROUP = 'argoproj.io',
|
||||
VERSION = 'v1alpha1',
|
||||
CRON_PLURAL = 'cronworkflows',
|
||||
WF_PLURAL = 'workflows',
|
||||
NAMESPACE = process.env.NS || 'argo',
|
||||
|
||||
// k8s label/annotation keys (must be lowercase dns-labels)
|
||||
LABEL_USER_KEY = 'openwebui.user-id',
|
||||
ANNO_DISPLAY_NAME = 'openwebui/display-name';
|
||||
|
||||
// load cluster credentials
|
||||
const kc = new KubeConfig();
|
||||
try { kc.loadFromCluster() } catch { kc.loadFromDefault() }
|
||||
|
||||
const co = kc.makeApiClient(CustomObjectsApi);
|
||||
|
||||
// build cron string from an iso timestamp in a tz
|
||||
const cronFromISO = (iso, tz = 'America/New_York') => {
|
||||
const dt = new Date(iso),
|
||||
parts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: tz, year: 'numeric', month: 'numeric', day: 'numeric',
|
||||
hour: 'numeric', minute: '2-digit', hour12: false
|
||||
}).formatToParts(dt).reduce((a, p) => (a[p.type] = p.value, a), {}),
|
||||
|
||||
m = Number(parts.month), d = Number(parts.day), h = Number(parts.hour), min = Number(parts.minute);
|
||||
|
||||
return `${min} ${h} ${d} ${m} *`;
|
||||
}
|
||||
|
||||
// derive a k8s-safe, user-scoped name and preserve a human display name
|
||||
const scopedName = (name, userId) => {
|
||||
// keep to dns-1123 by trimming/normalizing a bit; add an 8-char user suffix for uniqueness
|
||||
const base = String(name).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40),
|
||||
suffix = String(userId).toLowerCase().replace(/[^a-z0-9]+/g, '').slice(0, 8) || 'anon';
|
||||
return `${base}--u-${suffix}`;
|
||||
}
|
||||
|
||||
// ensure we have a user id header
|
||||
const requireUserId = (req) => {
|
||||
const userId = String(req.headers['x-user-id'] || '').trim();
|
||||
if (!userId) throw Object.assign(new Error('missing x-user-id header'), { status: 401 });
|
||||
return userId;
|
||||
}
|
||||
|
||||
// normalize parameters and force-inject user_id
|
||||
const buildParams = (parameters = {}, userId) => {
|
||||
const merged = { ...parameters, user_id: userId },
|
||||
args = Object.entries(merged).map(([name, value]) => ({ name, value }));
|
||||
return args.length ? { parameters: args } : undefined;
|
||||
}
|
||||
|
||||
// create or update a cronworkflow that runs a workflowtemplate (scoped to user)
|
||||
async function upsertCronWorkflow({
|
||||
name, when, tz = 'America/New_York', oneShot = false,
|
||||
template = { name: '', clusterScope: false },
|
||||
parameters = {}, entrypoint, userId
|
||||
}) {
|
||||
const schedule = when.cron ?? cronFromISO(when.iso, tz),
|
||||
nameActual = scopedName(name, userId),
|
||||
|
||||
body = {
|
||||
apiVersion: `${GROUP}/${VERSION}`,
|
||||
kind: 'CronWorkflow',
|
||||
metadata: {
|
||||
name: nameActual,
|
||||
labels: { [LABEL_USER_KEY]: userId },
|
||||
annotations: { [ANNO_DISPLAY_NAME]: name },
|
||||
},
|
||||
spec: {
|
||||
timezone: tz,
|
||||
schedules: [schedule],
|
||||
concurrencyPolicy: 'Forbid',
|
||||
...(oneShot ? { stopStrategy: { expression: 'cronworkflow.succeeded >= 1' } } : {}),
|
||||
workflowSpec: {
|
||||
...(entrypoint ? { entrypoint } : {}),
|
||||
arguments: buildParams(parameters, userId),
|
||||
workflowTemplateRef: {
|
||||
name: template.name,
|
||||
...(template.clusterScope ? { clusterScope: true } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// try patch, else create
|
||||
try {
|
||||
await co.patchNamespacedCustomObject(
|
||||
GROUP, VERSION, NAMESPACE, CRON_PLURAL, nameActual, body,
|
||||
undefined, undefined, undefined,
|
||||
{ headers: { 'content-type': 'application/merge-patch+json' } }
|
||||
);
|
||||
} catch {
|
||||
await co.createNamespacedCustomObject(GROUP, VERSION, NAMESPACE, CRON_PLURAL, body);
|
||||
}
|
||||
}
|
||||
|
||||
// run immediately (no schedule) by creating a workflow from the same template (scoped to user)
|
||||
async function runNow({ name, template, parameters = {}, entrypoint, userId }) {
|
||||
const wf = {
|
||||
apiVersion: `${GROUP}/${VERSION}`,
|
||||
kind: 'Workflow',
|
||||
metadata: {
|
||||
generateName: `${scopedName(name, userId)}-`,
|
||||
labels: { [LABEL_USER_KEY]: userId },
|
||||
annotations: { [ANNO_DISPLAY_NAME]: name },
|
||||
},
|
||||
spec: {
|
||||
...(entrypoint ? { entrypoint } : {}),
|
||||
arguments: buildParams(parameters, userId),
|
||||
workflowTemplateRef: {
|
||||
name: template.name,
|
||||
...(template.clusterScope ? { clusterScope: true } : {})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await co.createNamespacedCustomObject(GROUP, VERSION, NAMESPACE, WF_PLURAL, wf);
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url),
|
||||
__dirname = path.dirname(__filename),
|
||||
publicDir = path.join(__dirname, 'public');
|
||||
|
||||
// tiny json helper
|
||||
const readJson = (req) => new Promise((resolve, reject) => {
|
||||
let d = ''; req.on('data', c => d += c);
|
||||
req.on('end', () => { try { resolve(JSON.parse(d || '{}')) } catch (e) { reject(e) } });
|
||||
req.on('error', reject);
|
||||
});
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
// death
|
||||
const origin = req.headers.origin || '*'
|
||||
res.setHeader('access-control-allow-origin', origin)
|
||||
res.setHeader('vary', 'origin')
|
||||
res.setHeader('access-control-allow-headers', 'content-type, x-user-id')
|
||||
res.setHeader('access-control-allow-methods', 'GET, POST, DELETE, OPTIONS')
|
||||
if (req.method === 'OPTIONS') return res.writeHead(204).end()
|
||||
|
||||
// minimal static ui
|
||||
if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
|
||||
try {
|
||||
const html = fs.readFileSync(path.join(publicDir, 'index.html'), 'utf8');
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }).end(html);
|
||||
} catch {
|
||||
res.writeHead(404).end('ui not found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// list CronWorkflows for the calling user
|
||||
if (req.method === 'GET' && req.url === '/api/schedules') {
|
||||
const userId = requireUserId(req),
|
||||
list = await co.listNamespacedCustomObject(
|
||||
GROUP, VERSION, NAMESPACE, CRON_PLURAL,
|
||||
undefined, undefined, undefined, `${LABEL_USER_KEY}=${userId}` // labelSelector
|
||||
),
|
||||
items = (list.body.items || []).map(it => ({
|
||||
name: it.metadata?.name,
|
||||
displayName: it.metadata?.annotations?.[ANNO_DISPLAY_NAME] || it.metadata?.name,
|
||||
userId: it.metadata?.labels?.[LABEL_USER_KEY],
|
||||
timezone: it.spec?.timezone,
|
||||
schedules: it.spec?.schedules,
|
||||
oneShot: Boolean(it.spec?.stopStrategy),
|
||||
templateRef: it.spec?.workflowSpec?.workflowTemplateRef,
|
||||
entrypoint: it.spec?.workflowSpec?.entrypoint,
|
||||
}));
|
||||
|
||||
return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, items }));
|
||||
}
|
||||
|
||||
// list WorkflowTemplates for UI (shared)
|
||||
if (req.method === 'GET' && req.url === '/api/workflowtemplates') {
|
||||
const list = await co.listNamespacedCustomObject(GROUP, VERSION, NAMESPACE, 'workflowtemplates'),
|
||||
items = (list.body.items || []).map(it => ({ name: it.metadata?.name }));
|
||||
|
||||
return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, items }));
|
||||
}
|
||||
|
||||
// create/update a user-scoped schedule
|
||||
if (req.method === 'POST' && req.url === '/schedules') {
|
||||
const userId = requireUserId(req),
|
||||
input = await readJson(req);
|
||||
|
||||
await upsertCronWorkflow({ ...input, userId });
|
||||
return res.writeHead(201, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }));
|
||||
}
|
||||
|
||||
// run a job now for the calling user
|
||||
if (req.method === 'POST' && req.url === '/run-now') {
|
||||
const userId = requireUserId(req),
|
||||
input = await readJson(req);
|
||||
|
||||
await runNow({ ...input, userId });
|
||||
return res.writeHead(201, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }));
|
||||
}
|
||||
|
||||
// delete a schedule owned by the calling user
|
||||
if (req.method === 'DELETE' && req.url?.startsWith('/schedules/')) {
|
||||
const userId = requireUserId(req),
|
||||
name = decodeURIComponent(req.url.split('/').pop());
|
||||
|
||||
// guard: verify ownership via label before deletion
|
||||
const obj = await co.getNamespacedCustomObject(GROUP, VERSION, NAMESPACE, CRON_PLURAL, name),
|
||||
owner = obj.body?.metadata?.labels?.[LABEL_USER_KEY];
|
||||
|
||||
if (owner !== userId) {
|
||||
res.writeHead(403, { 'content-type': 'application/json' })
|
||||
.end(JSON.stringify({ ok: false, error: 'forbidden: schedule not owned by this user' }));
|
||||
return;
|
||||
}
|
||||
|
||||
await co.deleteNamespacedCustomObject(GROUP, VERSION, NAMESPACE, CRON_PLURAL, name);
|
||||
return res.writeHead(204).end();
|
||||
}
|
||||
|
||||
res.writeHead(404).end('not found');
|
||||
} catch (e) {
|
||||
const code = Number(e.status) || 500;
|
||||
res.writeHead(code, { 'content-type': 'application/json' })
|
||||
.end(JSON.stringify({ ok: false, error: e.message || String(e) }));
|
||||
}
|
||||
});
|
||||
|
||||
const port = Number(process.env.PORT) || 12253;
|
||||
server.listen(port, () => console.log(`schedules api listening on :${port}`));
|
||||
@@ -0,0 +1,19 @@
|
||||
# browser
|
||||
docker build -t ion606/browser:latest ./browser;
|
||||
docker push ion606/browser:latest;
|
||||
|
||||
# coderunner
|
||||
docker build -t ion606/coderunner:latest ./coderunner;
|
||||
docker push ion606/coderunner:latest;
|
||||
|
||||
# rag-server
|
||||
docker build -t ion606/rag-server:latest ./rag-server;
|
||||
docker push ion606/rag-server:latest;
|
||||
|
||||
# tools
|
||||
docker build -t ion606/tools:latest ./tools;
|
||||
docker push ion606/tools:latest;
|
||||
|
||||
# scheduling
|
||||
docker build -t ion606/ollama-scheduler:latest ./scheduler;
|
||||
docker push ion606/ollama-scheduler:latest;
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail;
|
||||
|
||||
# cluster + ingress addons (nginx + ingress-dns)
|
||||
# https://kubernetes.io/docs/tasks/access-application-cluster/ingress-minikube/
|
||||
# https://minikube.sigs.k8s.io/docs/handbook/addons/ingress-dns/
|
||||
# NOTE: publish ports 80/443 from the node to the host when using Docker driver
|
||||
minikube start --driver=docker --cni=cilium --ports=80:80,443:443;
|
||||
minikube addons enable ingress;
|
||||
minikube addons enable ingress-dns;
|
||||
|
||||
# wait for Cilium (if present) and ingress controller to become Ready
|
||||
kubectl -n kube-system rollout status ds/cilium --timeout=180s || echo "WARN: cilium DaemonSet not found or not Ready yet";
|
||||
if kubectl -n ingress-nginx get ds/ingress-nginx-controller >/dev/null 2>&1; then
|
||||
kubectl -n ingress-nginx rollout status ds/ingress-nginx-controller --timeout=180s || true;
|
||||
else
|
||||
kubectl -n ingress-nginx rollout status deploy/ingress-nginx-controller --timeout=180s || true;
|
||||
fi
|
||||
|
||||
# namespaces
|
||||
kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f -;
|
||||
kubectl create namespace ai --dry-run=client -o yaml | kubectl apply -f -;
|
||||
# argo workflows namespace (for cronworkflows/workflows + templates)
|
||||
kubectl create namespace argo --dry-run=client -o yaml | kubectl apply -f -;
|
||||
|
||||
# install argo cd (stable manifest)
|
||||
# https://argo-cd.readthedocs.io/en/stable/operator-manual/installation/
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml;
|
||||
|
||||
# WAIT for argocd core components to be ready enough to accept apps (slow piece of-)
|
||||
kubectl rollout status deploy/argocd-server -n argocd --timeout=180s || true;
|
||||
kubectl rollout status deploy/argocd-repo-server -n argocd --timeout=180s || true;
|
||||
kubectl rollout status deploy/argocd-application-controller -n argocd --timeout=180s || true;
|
||||
|
||||
# bootstrap this repo
|
||||
# NOTE: creates the child Applications in apps/children/*
|
||||
kubectl apply -n argocd -f apps/0-project-and-root.yaml;
|
||||
|
||||
echo "DEBUG: writing pods to 'tmp/pods.txt'"
|
||||
mkdir -p tmp || ""
|
||||
kubectl get pod -o wide --all-namespaces > tmp/pods.txt
|
||||
|
||||
# quick ingress test hint
|
||||
MINIKUBE_IP=$(minikube ip || echo "<minikube-ip>")
|
||||
echo "";
|
||||
echo "To test ingress locally (without DNS), run:";
|
||||
echo " curl -H 'Host: openwebui.local' http://$MINIKUBE_IP/";
|
||||
echo "If name doesn't resolve on your host, add to /etc/hosts:";
|
||||
echo " sudo sh -c 'echo \"$MINIKUBE_IP openwebui.local\" >> /etc/hosts'";
|
||||
|
||||
# port-forward argocd ui
|
||||
echo "";
|
||||
echo "argocd initial admin password (username 'admin'):";
|
||||
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo "";
|
||||
echo "";
|
||||
echo "port-forwarding argocd ui to https://localhost:8443 (ctrl+c to stop) ...";
|
||||
|
||||
# kubectl -n argocd port-forward svc/scheduler-ui 12253:12253
|
||||
kubectl -n argocd port-forward svc/argocd-server 8443:443
|
||||
File diff suppressed because one or more lines are too long
+3
-10
@@ -7,7 +7,6 @@ import traceback
|
||||
|
||||
from pathlib import Path
|
||||
from rokuHandler import RokuWrapper, ROKU_IP
|
||||
from privatebinHandler import PrivateBinWrapper
|
||||
|
||||
HOST = os.environ.get("HOST", "0.0.0.0")
|
||||
PORT = int(os.environ.get("PORT", "1331"))
|
||||
@@ -20,7 +19,6 @@ class HTTPHandler(BaseHTTPRequestHandler):
|
||||
|
||||
super().setup()
|
||||
self.rapp = RokuWrapper(self)
|
||||
self.pbin = PrivateBinWrapper(self)
|
||||
|
||||
def _cors(self):
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
@@ -55,16 +53,11 @@ class HTTPHandler(BaseHTTPRequestHandler):
|
||||
|
||||
try:
|
||||
# specs
|
||||
if path == "/roku/openapi.json":
|
||||
return self._send_file(ROOT / "spec" / "roku.openapi.json", "application/json")
|
||||
|
||||
if path.startswith("/roku"):
|
||||
if path == "/roku/openapi.json":
|
||||
return self._send_file(ROOT / "spec" / "roku.openapi.json", "application/json")
|
||||
|
||||
return self.rapp.run_command(path.replace("/roku/", ''))
|
||||
|
||||
if path.startswith("/privatebin"):
|
||||
if path == "/privatebin/openapi.json":
|
||||
return self._send_file(ROOT / "spec" / "privatebin.openapi.json", "application/json")
|
||||
return self.pbin.run_command(path.replace("/privatebin/", ''))
|
||||
|
||||
# catch-all
|
||||
self._send(404, json.dumps({"error": "unknown endpoint"}).encode())
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict, Any
|
||||
import json
|
||||
import urllib.parse as urlparse
|
||||
import privatebinapi
|
||||
|
||||
class PrivateBinWrapper():
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
|
||||
# read query params from the current request
|
||||
def _qs(self) -> Dict[str, Any]:
|
||||
parsed = urlparse.urlparse(self.parent.path)
|
||||
|
||||
# parse_qs returns lists and flatten singletons
|
||||
raw = urlparse.parse_qs(parsed.query, keep_blank_values=True)
|
||||
flat: Dict[str, Any] = {}
|
||||
|
||||
for k, v in raw.items():
|
||||
flat[k] = v[0] if len(v) == 1 else v
|
||||
return flat
|
||||
|
||||
def _ok(self, payload: Dict[str, Any], code: int = 200):
|
||||
body = json.dumps(payload).encode()
|
||||
self.parent._send(code, body)
|
||||
|
||||
def _err(self, msg: str, code: int = 400):
|
||||
self._ok({"error": msg}, code)
|
||||
|
||||
def _create(self, qs: Dict[str, Any]):
|
||||
base_url = qs.get("base_url")
|
||||
text = qs.get("text")
|
||||
password = qs.get("password")
|
||||
expiration = qs.get("expiration", "1day")
|
||||
formatting = qs.get("formatting", "plaintext")
|
||||
burn_after_reading = qs.get("burn_after_reading", "false").lower() == "true"
|
||||
discussion = qs.get("discussion", "false").lower() == "true"
|
||||
|
||||
if not base_url:
|
||||
return self._err("missing required 'base_url'")
|
||||
if not text:
|
||||
return self._err("missing required 'text'")
|
||||
|
||||
try:
|
||||
resp = privatebinapi.send(
|
||||
base_url,
|
||||
text=text,
|
||||
password=password,
|
||||
expiration=expiration,
|
||||
formatting=formatting,
|
||||
burn_after_reading=burn_after_reading,
|
||||
discussion=discussion,
|
||||
)
|
||||
# resp already contains: status, id, url, full_url, deletetoken, passcode?
|
||||
return self._ok(resp, 200)
|
||||
except Exception as e:
|
||||
return self._err(f"privatebin send failed: {e}", 502)
|
||||
|
||||
def _read(self, qs: Dict[str, Any]):
|
||||
full_url = qs.get("full_url")
|
||||
password = qs.get("password")
|
||||
if not full_url:
|
||||
return self._err("missing required 'full_url'")
|
||||
try:
|
||||
resp = privatebinapi.get(full_url, password=password)
|
||||
# has status, id, url, v, text, meta, attachment
|
||||
return self._ok(resp, 200)
|
||||
except Exception as e:
|
||||
return self._err(f"privatebin get failed: {e}", 502)
|
||||
|
||||
def _delete(self, qs: Dict[str, Any]):
|
||||
full_url = qs.get("full_url")
|
||||
deletetoken = qs.get("deletetoken")
|
||||
if not full_url or not deletetoken:
|
||||
return self._err("missing required 'full_url' or 'deletetoken'")
|
||||
try:
|
||||
resp = privatebinapi.delete(full_url, deletetoken)
|
||||
if resp == None:
|
||||
return self._err("response is null!")
|
||||
else:
|
||||
return self._ok(resp, 200)
|
||||
except Exception as e:
|
||||
return self._err(f"privatebin delete failed: {e}", 502)
|
||||
|
||||
def run_command(self, cmd_path: str):
|
||||
# expected forms:
|
||||
# /privatebin/create?base_url=...&text=...&password=...&expiration=...
|
||||
# /privatebin/read?full_url=...&password=...
|
||||
# /privatebin/delete?full_url=...&deletetoken=...
|
||||
cmd = (cmd_path or "").strip("/").lower()
|
||||
qs = self._qs()
|
||||
|
||||
if cmd == "create":
|
||||
return self._create(qs)
|
||||
if cmd == "read":
|
||||
return self._read(qs)
|
||||
if cmd == "delete":
|
||||
return self._delete(qs)
|
||||
|
||||
# unknown subpath
|
||||
return self._err("unknown privatebin command", 404)
|
||||
@@ -1,18 +1,6 @@
|
||||
anyio==4.10.0
|
||||
argcomplete==3.6.2
|
||||
base58==2.1.1
|
||||
certifi==2025.8.3
|
||||
charset-normalizer==3.4.3
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
PBinCLI==0.3.7
|
||||
PrivateBinAPI==1.0.0
|
||||
pycryptodome==3.23.0
|
||||
PySocks==1.7.1
|
||||
requests==2.32.5
|
||||
roku==4.1.0
|
||||
sjcl==0.2.1
|
||||
sniffio==1.3.1
|
||||
urllib3==2.5.0
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "PrivateBin HTTP API",
|
||||
"description": "wrapper server for privatebin API (create/read/delete) via query parameters",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://{host}:{port}",
|
||||
"variables": {
|
||||
"host": {
|
||||
"default": "0.0.0.0"
|
||||
},
|
||||
"port": {
|
||||
"default": "1331"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/privatebin/create": {
|
||||
"get": {
|
||||
"operationId": "privatebinCreate",
|
||||
"summary": "create a privatebin paste",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "base_url",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"description": "root url of your privatebin instance, e.g. https://privatebin.example"
|
||||
},
|
||||
{
|
||||
"name": "text",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "plain text of the paste"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "optional password to protect the paste"
|
||||
},
|
||||
{
|
||||
"name": "expiration",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"5min",
|
||||
"10min",
|
||||
"1hour",
|
||||
"1day",
|
||||
"1week",
|
||||
"1month",
|
||||
"1year",
|
||||
"never"
|
||||
],
|
||||
"default": "1day"
|
||||
},
|
||||
"description": "expire time"
|
||||
},
|
||||
{
|
||||
"name": "formatting",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"plaintext",
|
||||
"syntaxhighlighting",
|
||||
"markdown"
|
||||
],
|
||||
"default": "plaintext"
|
||||
},
|
||||
"description": "one of plaintext, syntaxhighlighting, markdown"
|
||||
},
|
||||
{
|
||||
"name": "burn_after_reading",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"description": "delete after first read"
|
||||
},
|
||||
{
|
||||
"name": "discussion",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"description": "enable comments/discussion"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "paste created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePasteResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid request parameters",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "upstream privatebin send failed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/privatebin/read": {
|
||||
"get": {
|
||||
"operationId": "privatebinRead",
|
||||
"summary": "read a privatebin paste",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "full_url",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"description": "full url of the paste (including key fragment)"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "password if the paste is protected"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "paste fetched successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ReadPasteResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "missing full_url parameter",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "upstream privatebin get failed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/privatebin/delete": {
|
||||
"get": {
|
||||
"operationId": "privatebinDelete",
|
||||
"summary": "delete a privatebin paste",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "full_url",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"description": "full url of the paste (including key fragment)"
|
||||
},
|
||||
{
|
||||
"name": "deletetoken",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "deletetoken returned upon create"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "paste deleted successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeletePasteResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "missing full_url or deletetoken",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "upstream privatebin delete failed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"CreatePasteResponse": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"full_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"deletetoken": {
|
||||
"type": "string"
|
||||
},
|
||||
"passcode": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"id",
|
||||
"url",
|
||||
"full_url",
|
||||
"deletetoken"
|
||||
]
|
||||
},
|
||||
"ReadPasteResponse": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"attachment": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "attachment object: includes filename and content (base64 or bytes)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"id",
|
||||
"url",
|
||||
"v"
|
||||
]
|
||||
},
|
||||
"DeletePasteResponse": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"id",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"Error": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user