Compare commits
15 Commits
8e8655e917
..
argo
| Author | SHA1 | Date | |
|---|---|---|---|
| bf98e2055e | |||
| 2c538a1cf9 | |||
| 1b827c05a6 | |||
| 2649ac6117 | |||
| fe0974d162 | |||
| 45ad5f5901 | |||
| 4e127f663b | |||
| 7975430489 | |||
| a4952581ec | |||
| 469dfcd094 | |||
| abd4ee798b | |||
| b3f58e6e4a | |||
| ab7eaa0581 | |||
| 26f4608c93 | |||
| f600e46537 |
@@ -134,3 +134,6 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
|
|
||||||
*.xml
|
*.xml
|
||||||
|
temp.*
|
||||||
|
bun.lock
|
||||||
|
tmp/
|
||||||
|
|||||||
@@ -2,10 +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
|
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]
|
> [!TIP]
|
||||||
> Looking for the compose version of this? See the [compose]()
|
> Looking for the compose version of this? See the [compose branch](https://git.ion606.com/ION606/ollama-plus/src/branch/compose/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: AppProject
|
kind: AppProject
|
||||||
metadata: { name: ai-stack, namespace: argocd }
|
metadata:
|
||||||
|
name: ai-stack
|
||||||
|
namespace: argocd
|
||||||
spec:
|
spec:
|
||||||
destinations:
|
destinations:
|
||||||
- server: https://kubernetes.default.svc
|
- server: https://kubernetes.default.svc
|
||||||
namespace: ai
|
namespace: ai
|
||||||
|
- server: https://kubernetes.default.svc
|
||||||
|
namespace: argo
|
||||||
# # only add if need to deploy into argocd (ehhhhh)
|
# # only add if need to deploy into argocd (ehhhhh)
|
||||||
# - server: https://kubernetes.default.svc
|
# - server: https://kubernetes.default.svc
|
||||||
# namespace: argocd
|
# namespace: argocd
|
||||||
@@ -13,14 +17,21 @@ spec:
|
|||||||
---
|
---
|
||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata: { name: ai-stack, namespace: argocd }
|
metadata:
|
||||||
|
name: ai-stack
|
||||||
|
namespace: argocd
|
||||||
spec:
|
spec:
|
||||||
project: ai-stack
|
project: ai-stack
|
||||||
destination: { server: https://kubernetes.default.svc, namespace: ai }
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
source:
|
source:
|
||||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||||
targetRevision: argo
|
targetRevision: argo
|
||||||
path: apps/children
|
path: apps/children
|
||||||
directory: { recurse: true }
|
|
||||||
syncPolicy:
|
syncPolicy:
|
||||||
automated: { prune: true, selfHeal: true }
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: argo-templates
|
||||||
|
namespace: ai
|
||||||
|
spec:
|
||||||
|
project: ai-stack
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: argo
|
||||||
|
source:
|
||||||
|
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||||
|
targetRevision: main
|
||||||
|
path: apps/argo-templates
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata: { name: airflow, namespace: ai }
|
|
||||||
spec:
|
|
||||||
project: ai-stack
|
|
||||||
destination: { server: https://kubernetes.default.svc, namespace: ai }
|
|
||||||
sources:
|
|
||||||
- repoURL: https://airflow.apache.org
|
|
||||||
chart: airflow
|
|
||||||
targetRevision: "*"
|
|
||||||
helm:
|
|
||||||
valueFiles:
|
|
||||||
- $values/values/airflow.yaml
|
|
||||||
|
|
||||||
- repoURL: https://git.ion606.com/ion606/ollama-plus
|
|
||||||
targetRevision: argo
|
|
||||||
ref: values
|
|
||||||
syncPolicy: { automated: { prune: true, selfHeal: true } }
|
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata: { name: browser, namespace: ai }
|
metadata:
|
||||||
|
name: browser
|
||||||
|
namespace: ai
|
||||||
spec:
|
spec:
|
||||||
project: ai-stack
|
project: ai-stack
|
||||||
destination: { server: https://kubernetes.default.svc, namespace: ai }
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
source:
|
source:
|
||||||
repoURL: https://your.git/repo.git
|
repoURL: https://git.ion606.com/ion606/ollama-plus.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: manifests/browser
|
path: manifests/browser
|
||||||
syncPolicy: { automated: { prune: true, selfHeal: true } }
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata: { name: coderunner, namespace: ai }
|
metadata:
|
||||||
|
name: coderunner
|
||||||
|
namespace: ai
|
||||||
|
labels:
|
||||||
|
repo.ion606.com/ollama-plus: "true"
|
||||||
spec:
|
spec:
|
||||||
project: ai-stack
|
project: ai-stack
|
||||||
destination: { server: https://kubernetes.default.svc, namespace: ai }
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
source:
|
source:
|
||||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: manifests/coderunner
|
path: manifests/coderunner
|
||||||
syncPolicy: { automated: { prune: true, selfHeal: true } }
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: debug-netshoot
|
||||||
|
namespace: ai
|
||||||
|
labels:
|
||||||
|
repo.ion606.com/ollama-plus: "true"
|
||||||
|
spec:
|
||||||
|
project: ai-stack
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
|
source:
|
||||||
|
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||||
|
targetRevision: main
|
||||||
|
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: ai
|
||||||
|
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: main
|
||||||
|
path: manifests/argo-schedules-api
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata: { name: openwebui, namespace: ai }
|
metadata:
|
||||||
|
name: openwebui
|
||||||
|
namespace: ai
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
spec:
|
spec:
|
||||||
project: ai-stack
|
project: ai-stack
|
||||||
destination: { server: https://kubernetes.default.svc, namespace: ai }
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
source:
|
source:
|
||||||
repoURL: https://helm.openwebui.com
|
repoURL: https://helm.openwebui.com
|
||||||
chart: open-webui
|
chart: open-webui
|
||||||
@@ -11,4 +17,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- apps/values/openwebui.yaml
|
- apps/values/openwebui.yaml
|
||||||
syncPolicy: { automated: { prune: true, selfHeal: true } }
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: policy-argo
|
||||||
|
namespace: ai
|
||||||
|
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: main
|
||||||
|
path: manifests/policy-argo
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: policy
|
||||||
|
namespace: ai
|
||||||
|
labels:
|
||||||
|
repo.ion606.com/ollama-plus: "true"
|
||||||
|
spec:
|
||||||
|
project: ai-stack
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
|
source:
|
||||||
|
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||||
|
targetRevision: main
|
||||||
|
path: manifests/policy
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata: { name: postgresql, namespace: ai }
|
metadata:
|
||||||
|
name: postgresql
|
||||||
|
namespace: ai
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "-10"
|
||||||
spec:
|
spec:
|
||||||
project: ai-stack
|
project: ai-stack
|
||||||
destination: { server: https://kubernetes.default.svc, namespace: ai }
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
source:
|
source:
|
||||||
repoURL: https://charts.bitnami.com/bitnami
|
repoURL: https://charts.bitnami.com/bitnami
|
||||||
chart: postgresql
|
chart: postgresql
|
||||||
@@ -12,4 +18,7 @@ spec:
|
|||||||
valueFiles:
|
valueFiles:
|
||||||
- apps/values/postgresql.yaml
|
- apps/values/postgresql.yaml
|
||||||
|
|
||||||
syncPolicy: { automated: { prune: true, selfHeal: true } }
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata: { name: rag-server, namespace: ai }
|
metadata:
|
||||||
|
name: rag-server
|
||||||
|
namespace: ai
|
||||||
|
labels:
|
||||||
|
repo.ion606.com/ollama-plus: "true"
|
||||||
spec:
|
spec:
|
||||||
project: ai-stack
|
project: ai-stack
|
||||||
destination: { server: https://kubernetes.default.svc, namespace: ai }
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
source:
|
source:
|
||||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: manifests/rag-server
|
path: manifests/rag-server
|
||||||
syncPolicy: { automated: { prune: true, selfHeal: true } }
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata: { name: searxng, namespace: ai }
|
metadata:
|
||||||
|
name: searxng
|
||||||
|
namespace: ai
|
||||||
spec:
|
spec:
|
||||||
project: ai-stack
|
project: ai-stack
|
||||||
destination: { server: https://kubernetes.default.svc, namespace: ai }
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
source:
|
source:
|
||||||
repoURL: https://charts.kubito.dev
|
repoURL: https://charts.kubito.dev
|
||||||
chart: searxng
|
chart: searxng
|
||||||
@@ -11,4 +15,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- apps/values/searxng.yaml
|
- apps/values/searxng.yaml
|
||||||
syncPolicy: { automated: { prune: true, selfHeal: true } }
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata: { name: tools, namespace: ai }
|
metadata:
|
||||||
|
name: tools
|
||||||
|
namespace: ai
|
||||||
|
labels:
|
||||||
|
repo.ion606.com/ollama-plus: "true"
|
||||||
spec:
|
spec:
|
||||||
project: ai-stack
|
project: ai-stack
|
||||||
destination: { server: https://kubernetes.default.svc, namespace: ai }
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ai
|
||||||
source:
|
source:
|
||||||
repoURL: https://git.ion606.com/ion606/ollama-plus
|
repoURL: https://git.ion606.com/ion606/ollama-plus
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: manifests/tools
|
path: manifests/tools
|
||||||
syncPolicy: { automated: { prune: true, selfHeal: true } }
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
useStandardNaming: true
|
|
||||||
executor: KubernetesExecutor
|
|
||||||
|
|
||||||
airflow:
|
|
||||||
extraPipPackages:
|
|
||||||
- "apache-airflow-providers-cncf-kubernetes>=7.4.0"
|
|
||||||
|
|
||||||
# metastore (postgres)
|
|
||||||
env:
|
|
||||||
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
|
|
||||||
value: "postgresql+psycopg2://postgres:mypassword@postgresql-primary.ai.svc.cluster.local:5432/openwebui"
|
|
||||||
|
|
||||||
pgbouncer:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
logs:
|
|
||||||
persistence:
|
|
||||||
enabled: true
|
|
||||||
size: 2Gi
|
|
||||||
|
|
||||||
allowPodLaunching: true
|
|
||||||
|
|
||||||
scheduler:
|
|
||||||
resources:
|
|
||||||
requests: { cpu: "200m", memory: "512Mi" }
|
|
||||||
limits: { cpu: "1", memory: "1Gi" }
|
|
||||||
|
|
||||||
webserver:
|
|
||||||
secretKeySecretName: airflow-webserver-secret
|
|
||||||
service:
|
|
||||||
type: NodePort
|
|
||||||
nodePort: 30082 # 30000–32767
|
|
||||||
resources:
|
|
||||||
requests: { cpu: "100m", memory: "256Mi" }
|
|
||||||
limits: { cpu: "500m", memory: "512Mi" }
|
|
||||||
|
|
||||||
triggerer:
|
|
||||||
resources:
|
|
||||||
requests: { cpu: "50m", memory: "128Mi" }
|
|
||||||
limits: { cpu: "200m", memory: "256Mi" }
|
|
||||||
|
|
||||||
# bc using nodeport
|
|
||||||
ingress:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# naur helm hooks for these jobs
|
|
||||||
createUserJob:
|
|
||||||
useHelmHooks: false
|
|
||||||
migrateDatabaseJob:
|
|
||||||
useHelmHooks: false
|
|
||||||
+30
-30
@@ -1,40 +1,40 @@
|
|||||||
image:
|
image:
|
||||||
repository: ghcr.io/open-webui/open-webui
|
repository: ghcr.io/open-webui/open-webui
|
||||||
tag: "main"
|
tag: "main"
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
service:
|
service:
|
||||||
type: NodePort # or ClusterIP if actually using ingress below
|
type: NodePort # use NodePort instead of ClusterIP so openwebui is accessible externally
|
||||||
nodePort: 4000
|
nodePort: 30400
|
||||||
|
|
||||||
persistence:
|
persistence:
|
||||||
enabled: true
|
enabled: true
|
||||||
size: 5Gi
|
size: 5Gi
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: false # set true for http://openwebui.local via nginx
|
enabled: false # disable ingress since we'll use NodePort
|
||||||
className: nginx
|
className: nginx
|
||||||
hosts:
|
hosts:
|
||||||
- host: openwebui.local
|
- host: openwebui.local
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
tls: [] # no https bc I lazy
|
tls: [] # no https for local/minikube
|
||||||
|
|
||||||
# NO SECRETS!!!
|
# NO SECRETS!!!
|
||||||
extraEnvVars:
|
extraEnvVars:
|
||||||
- name: OLLAMA_BASE_URL
|
- name: OLLAMA_BASE_URL
|
||||||
value: "https://mlep.ion606.com"
|
value: "https://mlep.ion606.com"
|
||||||
# postgres === primary db
|
# postgres === primary db
|
||||||
- name: DATABASE_URL
|
- name: DATABASE_URL
|
||||||
value: "postgresql://postgres:mypassword@postgresql-primary.ai.svc.cluster.local:5432/openwebui"
|
value: "postgresql://postgres:mypassword@postgresql-primary.ai.svc.cluster.local:5432/openwebui"
|
||||||
# store vectors in pgvector (on the same postgres)
|
# store vectors in pgvector (on the same postgres)
|
||||||
- name: VECTOR_DB
|
- name: VECTOR_DB
|
||||||
value: "pgvector"
|
value: "pgvector"
|
||||||
- name: PGVECTOR_DB_URL
|
- name: PGVECTOR_DB_URL
|
||||||
value: "postgresql://postgres:mypassword@postgresql-primary.ai.svc.cluster.local:5432/openwebui"
|
value: "postgresql://postgres:mypassword@postgresql-primary.ai.svc.cluster.local:5432/openwebui"
|
||||||
- name: PGVECTOR_CREATE_EXTENSION
|
- name: PGVECTOR_CREATE_EXTENSION
|
||||||
value: "true"
|
value: "true"
|
||||||
# no bish
|
# no bish
|
||||||
# - name: WEBUI_URL
|
# - name: WEBUI_URL
|
||||||
# value: "http://openwebui.local"
|
# value: "http://openwebui.local"
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# stupid mismatch fix
|
||||||
|
fullnameOverride: postgresql
|
||||||
|
|
||||||
architecture: replication
|
architecture: replication
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
|
|||||||
+15
-14
@@ -1,20 +1,21 @@
|
|||||||
image:
|
image:
|
||||||
repository: searxng/searxng
|
repository: searxng/searxng
|
||||||
tag: "latest"
|
tag: "latest"
|
||||||
|
|
||||||
service:
|
service:
|
||||||
type: NodePort # or ClusterIP if using ingress
|
type: NodePort # expose service externally
|
||||||
# nodePort: 30081
|
nodePort: 30081
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: false
|
enabled: false
|
||||||
className: nginx
|
className: nginx
|
||||||
hosts:
|
hosts:
|
||||||
- host: searxng.local
|
- host: searxng.local
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
|
|
||||||
# env:
|
# env:
|
||||||
# SEARXNG_SECRET: "please-change-me"
|
# SEARXNG_SECRET: "please-change-me"
|
||||||
# # helps with URL generation & results links
|
# # helps with URL generation & results links
|
||||||
# BASE_URL: "http://searxng.local/"
|
# BASE_URL: "http://searxng.local/"
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -2,39 +2,38 @@ apiVersion: apps/v1
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata: { name: browser, namespace: ai }
|
metadata: { name: browser, namespace: ai }
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector: { matchLabels: { app: browser } }
|
selector: { matchLabels: { app: browser } }
|
||||||
template:
|
template:
|
||||||
metadata: { labels: { app: browser } }
|
metadata: { labels: { app: browser } }
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: browser
|
- name: browser
|
||||||
image: docker.io/ion606/browser:latest
|
image: docker.io/ion606/browser:latest
|
||||||
ports: [{ containerPort: 7788 }]
|
ports: [{ containerPort: 7788 }]
|
||||||
env:
|
env:
|
||||||
- { name: WEBUI_IP, value: "0.0.0.0" }
|
- { name: WEBUI_IP, value: "0.0.0.0" }
|
||||||
- { name: WEBUI_PORT, value: "7788" }
|
- { name: WEBUI_PORT, value: "7788" }
|
||||||
resources:
|
resources:
|
||||||
requests: { cpu: "250m", memory: "256Mi" }
|
requests: { cpu: "250m", memory: "256Mi" }
|
||||||
limits: { cpu: "1", memory: "1Gi" } # hard cap
|
limits: { cpu: "1", memory: "1Gi" } # hard cap
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
{
|
httpGet: { path: "/", port: 7788 }
|
||||||
httpGet: { path: "/", port: 7788 },
|
initialDelaySeconds: 5
|
||||||
initialDelaySeconds: 5,
|
periodSeconds: 10
|
||||||
periodSeconds: 10,
|
livenessProbe:
|
||||||
}
|
httpGet: { path: "/", port: 7788 }
|
||||||
livenessProbe:
|
initialDelaySeconds: 15
|
||||||
{
|
periodSeconds: 20
|
||||||
httpGet: { path: "/", port: 7788 },
|
|
||||||
initialDelaySeconds: 15,
|
|
||||||
periodSeconds: 20,
|
|
||||||
}
|
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata: { name: browser, namespace: ai }
|
metadata: { name: browser, namespace: ai }
|
||||||
spec:
|
spec:
|
||||||
selector: { app: browser }
|
selector: { app: browser }
|
||||||
ports: [{ name: http, port: 7788, targetPort: 7788 }]
|
ports:
|
||||||
type: ClusterIP
|
- name: http
|
||||||
|
port: 7788
|
||||||
|
targetPort: 7788
|
||||||
|
nodePort: 30788
|
||||||
|
type: NodePort
|
||||||
@@ -14,23 +14,18 @@ spec:
|
|||||||
ports: [{ containerPort: 8787 }]
|
ports: [{ containerPort: 8787 }]
|
||||||
env:
|
env:
|
||||||
- { name: PORT, value: "8787" }
|
- { name: PORT, value: "8787" }
|
||||||
- {
|
- name: NAMESPACE
|
||||||
name: NAMESPACE,
|
valueFrom:
|
||||||
valueFrom:
|
fieldRef:
|
||||||
{ fieldRef: { fieldPath: metadata.namespace } },
|
fieldPath: metadata.namespace
|
||||||
}
|
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
{
|
httpGet: { path: "/openapi.json", port: 8787 }
|
||||||
httpGet: { path: "/openapi.json", port: 8787 },
|
initialDelaySeconds: 5
|
||||||
initialDelaySeconds: 5,
|
periodSeconds: 10
|
||||||
periodSeconds: 10,
|
|
||||||
}
|
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
{
|
httpGet: { path: "/openapi.json", port: 8787 }
|
||||||
httpGet: { path: "/openapi.json", port: 8787 },
|
initialDelaySeconds: 15
|
||||||
initialDelaySeconds: 15,
|
periodSeconds: 20
|
||||||
periodSeconds: 20,
|
|
||||||
}
|
|
||||||
resources:
|
resources:
|
||||||
requests: { cpu: "100m", memory: "128Mi" }
|
requests: { cpu: "100m", memory: "128Mi" }
|
||||||
limits: { cpu: "500m", memory: "512Mi" }
|
limits: { cpu: "500m", memory: "512Mi" }
|
||||||
@@ -40,5 +35,9 @@ kind: Service
|
|||||||
metadata: { name: coderunner, namespace: ai }
|
metadata: { name: coderunner, namespace: ai }
|
||||||
spec:
|
spec:
|
||||||
selector: { app: coderunner }
|
selector: { app: coderunner }
|
||||||
ports: [{ name: http, port: 8787, targetPort: 8787 }]
|
ports:
|
||||||
type: ClusterIP
|
- name: http
|
||||||
|
port: 8787
|
||||||
|
targetPort: 8787
|
||||||
|
nodePort: 31787
|
||||||
|
type: NodePort
|
||||||
|
|||||||
@@ -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: ai
|
||||||
|
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,17 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-browser-ingress
|
||||||
|
namespace: ai
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: browser
|
||||||
|
policyTypes: ["Ingress"]
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- ipBlock:
|
||||||
|
cidr: 0.0.0.0/0
|
||||||
|
ports:
|
||||||
|
- { protocol: TCP, port: 7788 }
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-common-egress
|
||||||
|
namespace: ai
|
||||||
|
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: ai
|
||||||
|
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,20 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-openwebui-ingress
|
||||||
|
namespace: ai
|
||||||
|
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 }
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata: { name: default-deny-all, namespace: ai }
|
metadata:
|
||||||
|
name: default-deny-all
|
||||||
|
namespace: ai
|
||||||
spec:
|
spec:
|
||||||
podSelector: {}
|
podSelector: {} # die
|
||||||
policyTypes: ["Ingress", "Egress"]
|
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
|
||||||
@@ -20,12 +20,10 @@ spec:
|
|||||||
- { name: OLLAMA_CHAT_MODEL, value: "llama3.1" }
|
- { name: OLLAMA_CHAT_MODEL, value: "llama3.1" }
|
||||||
- { name: OLLAMA_EMBED_MODEL, value: "nomic-embed-text" }
|
- { name: OLLAMA_EMBED_MODEL, value: "nomic-embed-text" }
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
{ httpGet: { path: "/openapi.json", port: 8788 } }
|
httpGet: { path: "/openapi.json", port: 8788 }
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
{
|
httpGet: { path: "/", port: 8788 }
|
||||||
httpGet: { path: "/", port: 8788 },
|
initialDelaySeconds: 10
|
||||||
initialDelaySeconds: 10,
|
|
||||||
}
|
|
||||||
resources:
|
resources:
|
||||||
requests: { cpu: "200m", memory: "256Mi" }
|
requests: { cpu: "200m", memory: "256Mi" }
|
||||||
limits: { cpu: "1", memory: "1Gi" }
|
limits: { cpu: "1", memory: "1Gi" }
|
||||||
@@ -35,5 +33,9 @@ kind: Service
|
|||||||
metadata: { name: rag-server, namespace: ai }
|
metadata: { name: rag-server, namespace: ai }
|
||||||
spec:
|
spec:
|
||||||
selector: { app: rag-server }
|
selector: { app: rag-server }
|
||||||
ports: [{ name: http, port: 8788, targetPort: 8788 }]
|
ports:
|
||||||
type: ClusterIP
|
- name: http
|
||||||
|
port: 8788
|
||||||
|
targetPort: 8788
|
||||||
|
nodePort: 31788
|
||||||
|
type: NodePort
|
||||||
|
|||||||
@@ -2,34 +2,36 @@ apiVersion: apps/v1
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata: { name: tools, namespace: ai }
|
metadata: { name: tools, namespace: ai }
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector: { matchLabels: { app: tools } }
|
selector: { matchLabels: { app: tools } }
|
||||||
template:
|
template:
|
||||||
metadata: { labels: { app: tools } }
|
metadata: { labels: { app: tools } }
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: tools
|
- name: tools
|
||||||
image: docker.io/ion606/tools:latest
|
image: docker.io/ion606/tools:latest
|
||||||
ports: [{ containerPort: 1331 }]
|
ports: [{ containerPort: 1331 }]
|
||||||
env:
|
env:
|
||||||
- { name: HOST, value: "0.0.0.0" }
|
- { name: HOST, value: "0.0.0.0" }
|
||||||
- { name: PORT, value: "1331" }
|
- { name: PORT, value: "1331" }
|
||||||
- { name: ROKU_IP, value: "192.0.2.10" }
|
- { name: ROKU_IP, value: "192.0.2.10" }
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
{ httpGet: { path: "/roku/openapi.json", port: 1331 } }
|
httpGet: { path: "/roku/openapi.json", port: 1331 }
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
{
|
httpGet: { path: "/roku/openapi.json", port: 1331 }
|
||||||
httpGet: { path: "/roku/openapi.json", port: 1331 },
|
initialDelaySeconds: 10
|
||||||
initialDelaySeconds: 10,
|
resources:
|
||||||
}
|
requests: { cpu: "100m", memory: "128Mi" }
|
||||||
resources:
|
limits: { cpu: "500m", memory: "512Mi" }
|
||||||
requests: { cpu: "100m", memory: "128Mi" }
|
|
||||||
limits: { cpu: "500m", memory: "512Mi" }
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata: { name: tools, namespace: ai }
|
metadata: { name: tools, namespace: ai }
|
||||||
spec:
|
spec:
|
||||||
selector: { app: tools }
|
selector: { app: tools }
|
||||||
ports: [{ name: http, port: 1331, targetPort: 1331 }]
|
ports:
|
||||||
type: ClusterIP
|
- 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}`));
|
||||||
@@ -13,3 +13,7 @@ docker push ion606/rag-server:latest;
|
|||||||
# tools
|
# tools
|
||||||
docker build -t ion606/tools:latest ./tools;
|
docker build -t ion606/tools:latest ./tools;
|
||||||
docker push ion606/tools:latest;
|
docker push ion606/tools:latest;
|
||||||
|
|
||||||
|
# scheduling
|
||||||
|
docker build -t ion606/ollama-scheduler:latest ./scheduler;
|
||||||
|
docker push ion606/ollama-scheduler:latest;
|
||||||
|
|||||||
+26
-9
@@ -5,13 +5,24 @@ set -euo pipefail;
|
|||||||
# cluster + ingress addons (nginx + ingress-dns)
|
# cluster + ingress addons (nginx + ingress-dns)
|
||||||
# https://kubernetes.io/docs/tasks/access-application-cluster/ingress-minikube/
|
# https://kubernetes.io/docs/tasks/access-application-cluster/ingress-minikube/
|
||||||
# https://minikube.sigs.k8s.io/docs/handbook/addons/ingress-dns/
|
# https://minikube.sigs.k8s.io/docs/handbook/addons/ingress-dns/
|
||||||
minikube start --driver=docker || true;
|
# 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;
|
||||||
minikube addons enable ingress-dns;
|
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
|
# namespaces
|
||||||
kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f -;
|
kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f -;
|
||||||
kubectl create namespace ai --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)
|
# install argo cd (stable manifest)
|
||||||
# https://argo-cd.readthedocs.io/en/stable/operator-manual/installation/
|
# https://argo-cd.readthedocs.io/en/stable/operator-manual/installation/
|
||||||
@@ -26,18 +37,24 @@ kubectl rollout status deploy/argocd-application-controller -n argocd --timeout=
|
|||||||
# NOTE: creates the child Applications in apps/children/*
|
# NOTE: creates the child Applications in apps/children/*
|
||||||
kubectl apply -n argocd -f apps/0-project-and-root.yaml;
|
kubectl apply -n argocd -f apps/0-project-and-root.yaml;
|
||||||
|
|
||||||
# service!
|
echo "DEBUG: writing pods to 'tmp/pods.txt'"
|
||||||
# SEE???? I CAN USE DASHES AND NOT JUST CAMELCASE!!!
|
mkdir -p tmp || ""
|
||||||
kubectl -n ai create secret generic airflow-fernet-key-secret --from-literal=fernet-key=$(python3 -c 'import secrets;print(secrets.token_urlsafe(32))')
|
kubectl get pod -o wide --all-namespaces > tmp/pods.txt
|
||||||
kubectl -n ai create secret generic airflow-webserver-secret --from-literal=webserver-secret-key=$(python3 -c 'import secrets;print(secrets.token_hex(16))')
|
|
||||||
|
|
||||||
minikube service -n ai airflow-webserver --url
|
# 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
|
# port-forward argocd ui
|
||||||
echo "";
|
echo "";
|
||||||
echo "argocd initial admin password (username 'admin'):";
|
echo "argocd initial admin password (username 'admin'):";
|
||||||
kubectl -n argocd get secret argocd-initial-admin-secret \
|
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo "";
|
||||||
-o jsonpath='{.data.password}' | base64 -d; echo "";
|
|
||||||
echo "";
|
echo "";
|
||||||
echo "port-forwarding argocd ui to https://localhost:8443 (ctrl+c to stop) ...";
|
echo "port-forwarding argocd ui to https://localhost:8443 (ctrl+c to stop) ...";
|
||||||
kubectl -n argocd port-forward svc/argocd-server 8443:443;
|
|
||||||
|
# kubectl -n argocd port-forward svc/scheduler-ui 12253:12253
|
||||||
|
kubectl -n argocd port-forward svc/argocd-server 8443:443
|
||||||
|
|||||||
Reference in New Issue
Block a user