adding kustomization

This commit is contained in:
2025-09-13 11:44:13 -04:00
parent 469dfcd094
commit a4952581ec
17 changed files with 381 additions and 2 deletions
+2 -2
View File
@@ -7,6 +7,8 @@ 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
@@ -27,8 +29,6 @@ spec:
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: automated:
prune: true prune: true
+18
View File
@@ -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
+20
View File
@@ -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-ollama-scheduler
syncPolicy:
automated:
prune: true
selfHeal: true
+2
View File
@@ -3,6 +3,8 @@ kind: Application
metadata: metadata:
name: coderunner name: coderunner
namespace: ai namespace: ai
labels:
repo.ion606.com/ollama-plus: "true"
spec: spec:
project: ai-stack project: ai-stack
destination: destination:
+35
View File
@@ -0,0 +1,35 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- argo-ollama-scheduler.yaml
- coderunner.yaml
- tools.yaml
- rag-server.yaml
- openwebui.yaml
- postgresql.yaml
- searxng.yaml
- browser.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: ollama-plus-revs
literals:
- targetRevision=main
# 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
+2
View File
@@ -3,6 +3,8 @@ kind: Application
metadata: metadata:
name: rag-server name: rag-server
namespace: ai namespace: ai
labels:
repo.ion606.com/ollama-plus: "true"
spec: spec:
project: ai-stack project: ai-stack
destination: destination:
+2
View File
@@ -3,6 +3,8 @@ kind: Application
metadata: metadata:
name: tools name: tools
namespace: ai namespace: ai
labels:
repo.ion606.com/ollama-plus: "true"
spec: spec:
project: ai-stack project: ai-stack
destination: destination:
@@ -0,0 +1,46 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: schedules-api
namespace: argo
spec:
replicas: 1
selector:
matchLabels: { app: schedules-api }
template:
metadata:
labels: { app: schedules-api }
spec:
serviceAccountName: schedules-api
containers:
- name: schedules-api
# TODO: build & push your image, then update below
image: ghcr.io/your-org/schedules-api:0.1.0
imagePullPolicy: IfNotPresent
env:
- { name: PORT, value: "3000" }
- { name: NS, value: "argo" }
ports:
- { name: http, containerPort: 3000 }
readinessProbe:
tcpSocket: { port: 3000 }
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
tcpSocket: { port: 3000 }
initialDelaySeconds: 10
periodSeconds: 20
resources:
requests: { cpu: "50m", memory: "64Mi" }
limits: { cpu: "200m", memory: "256Mi" }
---
apiVersion: v1
kind: Service
metadata:
name: schedules-api
namespace: argo
spec:
selector: { app: schedules-api }
ports:
- { name: http, port: 3000, targetPort: 3000 }
type: ClusterIP
+47
View File
@@ -0,0 +1,47 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: schedules-api
namespace: argo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: schedules-api
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"]
---
# If you need ClusterWorkflowTemplate support, create this ClusterRole and a ClusterRoleBinding
# with subject serviceAccountName: schedules-api, namespace: argo
# apiVersion: rbac.authorization.k8s.io/v1
# kind: ClusterRole
# metadata:
# name: schedules-api-cwft-read
# rules:
# - apiGroups: ["argoproj.io"]
# resources: ["clusterworkflowtemplates"]
# verbs: ["get","list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: schedules-api
namespace: argo
subjects:
- kind: ServiceAccount
name: schedules-api
namespace: argo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: schedules-api
@@ -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.
+6
View File
@@ -0,0 +1,6 @@
node_modules
npm-cache
bun.lockb
.DS_Store
*.log
+14
View File
@@ -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
USER bun
EXPOSE 3000
ENV NODE_ENV=production
CMD ["bun", "run", "server.mjs"]
+14
View File
@@ -0,0 +1,14 @@
{
"name": "schedules-api",
"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"
}
}
+123
View File
@@ -0,0 +1,123 @@
// bun run server.mjs
// tiny schedules api to manage argo cronworkflows/workflows via k8s CRDs
// comments intentionally lowercase per original style
import http from 'http'
import { KubeConfig, CustomObjectsApi } from '@kubernetes/client-node'
const GROUP = 'argoproj.io'
const VERSION = 'v1alpha1'
const CRON_PLURAL = 'cronworkflows'
const WF_PLURAL = 'workflows'
const NAMESPACE = process.env.NS || 'argo'
// load cluster credentials (or fallback to local kubeconfig for dev)
const kc = new KubeConfig()
try { kc.loadFromCluster() } catch { kc.loadFromDefault() }
const co = kc.makeApiClient(CustomObjectsApi)
// helper: build cron string from an iso timestamp in a tz
const cronFromISO = (iso, tz = 'America/New_York') => {
const dt = new Date(iso)
const 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), {})
const m = Number(parts.month), d = Number(parts.day), h = Number(parts.hour), min = Number(parts.minute)
return `${min} ${h} ${d} ${m} *`
}
// create or update a cronworkflow that runs a workflowtemplate
async function upsertCronWorkflow({
name, when, tz = 'America/New_York', oneShot = false,
template = { name: '', clusterScope: false },
parameters = {}, entrypoint
}) {
const schedule = when.cron ?? cronFromISO(when.iso, tz)
const args = Object.entries(parameters).map(([name, value]) => ({ name, value }))
const body = {
apiVersion: `${GROUP}/${VERSION}`,
kind: 'CronWorkflow',
metadata: { name },
spec: {
timezone: tz,
schedules: [schedule],
concurrencyPolicy: 'Forbid',
...(oneShot ? { stopStrategy: { expression: 'cronworkflow.succeeded >= 1' } } : {}),
workflowSpec: {
...(entrypoint ? { entrypoint } : {}),
arguments: args.length ? { parameters: args } : undefined,
workflowTemplateRef: {
name: template.name,
...(template.clusterScope ? { clusterScope: true } : {})
}
}
}
}
// try patch, else create
try {
await co.patchNamespacedCustomObject(
GROUP, VERSION, NAMESPACE, CRON_PLURAL, name, 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
async function runNow({ name, template, parameters = {}, entrypoint }) {
const args = Object.entries(parameters).map(([name, value]) => ({ name, value }))
const wf = {
apiVersion: `${GROUP}/${VERSION}`,
kind: 'Workflow',
metadata: { generateName: `${name}-` },
spec: {
...(entrypoint ? { entrypoint } : {}),
arguments: args.length ? { parameters: args } : undefined,
workflowTemplateRef: {
name: template.name,
...(template.clusterScope ? { clusterScope: true } : {})
}
}
}
await co.createNamespacedCustomObject(GROUP, VERSION, NAMESPACE, WF_PLURAL, wf)
}
// tiny http api
const server = http.createServer(async (req, res) => {
try {
if (req.method === 'POST' && req.url === '/schedules') {
const input = JSON.parse(await new Promise(r => {
let d = ''; req.on('data', c => d += c); req.on('end', () => r(d))
}))
await upsertCronWorkflow(input)
res.writeHead(201).end(JSON.stringify({ ok: true }))
return
}
if (req.method === 'POST' && req.url === '/run-now') {
const input = JSON.parse(await new Promise(r => {
let d = ''; req.on('data', c => d += c); req.on('end', () => r(d))
}))
await runNow(input)
res.writeHead(201).end(JSON.stringify({ ok: true }))
return
}
if (req.method === 'DELETE' && req.url?.startsWith('/schedules/')) {
const name = decodeURIComponent(req.url.split('/').pop())
await co.deleteNamespacedCustomObject(GROUP, VERSION, NAMESPACE, CRON_PLURAL, name)
res.writeHead(204).end()
return
}
res.writeHead(404).end('not found')
} catch (e) {
res.writeHead(500).end(JSON.stringify({ ok: false, error: e.message }))
}
})
const port = Number(process.env.PORT) || 3000
server.listen(port, () => console.log(`schedules api listening on :${port}`))
+4
View File
@@ -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;
+2
View File
@@ -12,6 +12,8 @@ minikube addons enable ingress-dns;
# 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/