diff --git a/.gitignore b/.gitignore index c935f0e..9041e60 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,5 @@ dist __pycache__/ .venv/ + +*.xml diff --git a/apps/0-project-and-root.yaml b/apps/0-project-and-root.yaml new file mode 100644 index 0000000..cd12bea --- /dev/null +++ b/apps/0-project-and-root.yaml @@ -0,0 +1,21 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: { name: ai-stack, namespace: argocd } +spec: + destinations: [{ namespace: ai, server: https://kubernetes.default.svc }] + sourceRepos: ["*"] + +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: { name: ai-stack, namespace: argocd } +spec: + project: ai-stack + destination: { server: https://kubernetes.default.svc, namespace: ai } + source: + repoURL: https://git.ion606.com/ion606/ollama-plus + targetRevision: main + path: apps/children + directory: { recurse: true } + syncPolicy: + automated: { prune: true, selfHeal: true } diff --git a/apps/children/browser.yaml b/apps/children/browser.yaml new file mode 100644 index 0000000..5c26817 --- /dev/null +++ b/apps/children/browser.yaml @@ -0,0 +1,11 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: { name: browser, namespace: argocd } +spec: + project: ai-stack + destination: { server: https://kubernetes.default.svc, namespace: ai } + source: + repoURL: https://your.git/repo.git + targetRevision: main + path: manifests/browser + syncPolicy: { automated: { prune: true, selfHeal: true } } diff --git a/apps/children/coderunner.yaml b/apps/children/coderunner.yaml new file mode 100644 index 0000000..89eb72c --- /dev/null +++ b/apps/children/coderunner.yaml @@ -0,0 +1,11 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: { name: coderunner, namespace: argocd } +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/coderunner + syncPolicy: { automated: { prune: true, selfHeal: true } } diff --git a/apps/children/ollama.yaml b/apps/children/ollama.yaml new file mode 100644 index 0000000..f44a9a5 --- /dev/null +++ b/apps/children/ollama.yaml @@ -0,0 +1,15 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: { name: ollama, namespace: argocd } +spec: + project: ai-stack + destination: { server: https://kubernetes.default.svc, namespace: ai } + source: + repoURL: https://helm.otwld.com + chart: ollama + targetRevision: "*" + helm: + values: | + service: { type: ClusterIP } + # add gpu values later if your node has one + syncPolicy: { automated: { prune: true, selfHeal: true } } diff --git a/apps/children/openwebui.yaml b/apps/children/openwebui.yaml new file mode 100644 index 0000000..b500dcb --- /dev/null +++ b/apps/children/openwebui.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: { name: open-webui, namespace: argocd } +spec: + project: ai-stack + destination: { server: https://kubernetes.default.svc, namespace: ai } + source: + repoURL: https://helm.openwebui.com + chart: open-webui + targetRevision: "*" + helm: + values: | + persistence: + enabled: true + size: 5Gi + service: { type: ClusterIP } + ingress: + enabled: true + className: nginx + hosts: + - host: openwebui.local + paths: [{ path: "/", pathType: Prefix }] + syncPolicy: { automated: { prune: true, selfHeal: true } } diff --git a/apps/children/postgresql.yaml b/apps/children/postgresql.yaml new file mode 100644 index 0000000..1ca26c8 --- /dev/null +++ b/apps/children/postgresql.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: { name: postgresql, namespace: argocd } +spec: + project: ai-stack + destination: { server: https://kubernetes.default.svc, namespace: ai } + source: + repoURL: https://charts.bitnami.com/bitnami + chart: postgresql + targetRevision: "*" + helm: + values: | + architecture: replication + auth: + username: openwebui + password: openwebui-pass + database: openwebui_db + primary: + persistence: { enabled: true, size: 5Gi } + readReplicas: + replicaCount: 1 + syncPolicy: { automated: { prune: true, selfHeal: true } } diff --git a/apps/children/rag-server.yaml b/apps/children/rag-server.yaml new file mode 100644 index 0000000..2fbc7c6 --- /dev/null +++ b/apps/children/rag-server.yaml @@ -0,0 +1,11 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: { name: rag-server, namespace: argocd } +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/rag-server + syncPolicy: { automated: { prune: true, selfHeal: true } } diff --git a/apps/children/searxng.yaml b/apps/children/searxng.yaml new file mode 100644 index 0000000..c40f9cc --- /dev/null +++ b/apps/children/searxng.yaml @@ -0,0 +1,14 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: { name: searxng, namespace: argocd } +spec: + project: ai-stack + destination: { server: https://kubernetes.default.svc, namespace: ai } + source: + repoURL: https://charts.kubito.dev + chart: searxng + targetRevision: "*" + helm: + values: | + service: { type: ClusterIP } + syncPolicy: { automated: { prune: true, selfHeal: true } } diff --git a/apps/children/tools.yaml b/apps/children/tools.yaml new file mode 100644 index 0000000..ab675b0 --- /dev/null +++ b/apps/children/tools.yaml @@ -0,0 +1,11 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: { name: tools, namespace: argocd } +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/tools + syncPolicy: { automated: { prune: true, selfHeal: true } } diff --git a/browser/Dockerfile b/browser/Dockerfile index 610f23a..21bda19 100644 --- a/browser/Dockerfile +++ b/browser/Dockerfile @@ -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 \ diff --git a/coderunner/bun.lock b/coderunner/bun.lock new file mode 100644 index 0000000..6dabd95 --- /dev/null +++ b/coderunner/bun.lock @@ -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=="], + } +} diff --git a/coderunner/index.ts b/coderunner/index.ts index 4450430..7107efb 100644 --- a/coderunner/index.ts +++ b/coderunner/index.ts @@ -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 { + 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) }); } }); diff --git a/coderunner/package-lock.json b/coderunner/package-lock.json deleted file mode 100644 index ff32074..0000000 --- a/coderunner/package-lock.json +++ /dev/null @@ -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" - } - } -} diff --git a/coderunner/package.json b/coderunner/package.json index 1220716..d36730c 100644 --- a/coderunner/package.json +++ b/coderunner/package.json @@ -1,5 +1,8 @@ { "devDependencies": { "@types/node": "^24.3.1" + }, + "dependencies": { + "@kubernetes/client-node": "^1.3.0" } } diff --git a/docker-compose.yml b/docker-compose.yml index a637037..c3d1e61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: - POSTGRES_PASSWORD=mypassword - POSTGRES_DB=openwebui_db volumes: - - pgdata:/var/lib/postgresql/data + - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s diff --git a/manifests/browser/deployment.yaml b/manifests/browser/deployment.yaml new file mode 100644 index 0000000..98aea45 --- /dev/null +++ b/manifests/browser/deployment.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: { name: browser, namespace: ai } +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: ai } +spec: + selector: { app: browser } + ports: [{ name: http, port: 7788, targetPort: 7788 }] + type: ClusterIP diff --git a/manifests/coderunner/deployment.yaml b/manifests/coderunner/deployment.yaml new file mode 100644 index 0000000..dfe5bb3 --- /dev/null +++ b/manifests/coderunner/deployment.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: { name: coderunner, namespace: ai } +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: ai } +spec: + selector: { app: coderunner } + ports: [{ name: http, port: 8787, targetPort: 8787 }] + type: ClusterIP diff --git a/manifests/coderunner/rbac.yaml b/manifests/coderunner/rbac.yaml new file mode 100644 index 0000000..c23cf82 --- /dev/null +++ b/manifests/coderunner/rbac.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: { name: coderunner-sa, namespace: ai } +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: { name: coderunner-job-role, namespace: ai } +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: ai } +subjects: + - kind: ServiceAccount + name: coderunner-sa + namespace: ai +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coderunner-job-role diff --git a/manifests/policy/default-deny.yaml b/manifests/policy/default-deny.yaml new file mode 100644 index 0000000..04f19a9 --- /dev/null +++ b/manifests/policy/default-deny.yaml @@ -0,0 +1,6 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: { name: default-deny-all, namespace: ai } +spec: + podSelector: {} + policyTypes: ["Ingress", "Egress"] diff --git a/manifests/rag-server/deployment.yaml b/manifests/rag-server/deployment.yaml new file mode 100644 index 0000000..11bc233 --- /dev/null +++ b/manifests/rag-server/deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: { name: rag-server, namespace: ai } +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: ai } +spec: + selector: { app: rag-server } + ports: [{ name: http, port: 8788, targetPort: 8788 }] + type: ClusterIP diff --git a/manifests/tools/deployment.yaml b/manifests/tools/deployment.yaml new file mode 100644 index 0000000..a3a84f9 --- /dev/null +++ b/manifests/tools/deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: { name: tools, namespace: ai } +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: ai } +spec: + selector: { app: tools } + ports: [{ name: http, port: 1331, targetPort: 1331 }] + type: ClusterIP diff --git a/scripts/rebuild_and_push_imgs.sh b/scripts/rebuild_and_push_imgs.sh new file mode 100644 index 0000000..ff23e0a --- /dev/null +++ b/scripts/rebuild_and_push_imgs.sh @@ -0,0 +1,15 @@ +# 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; diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..1f6c7e8 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,36 @@ +#!/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/ +minikube start --driver=docker || true; +minikube addons enable ingress; +minikube addons enable ingress-dns; + +# 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 -; + +# 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; + +# 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/argocd-server 8443:443;