From 24328dfcfcad40de504af5d1ee94ab04d3fe1e9f Mon Sep 17 00:00:00 2001 From: jskorlol Date: Sat, 17 Oct 2026 00:18:05 +0900 Subject: [PATCH] feat: publish Magpie Helm chart --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 5 +- README.md | 3 +- charts/magpie/.helmignore | 2 + charts/magpie/Chart.yaml | 20 ++ charts/magpie/README.md | 237 ++++++++++++++++++++++++ charts/magpie/scripts/start.sh | 17 ++ charts/magpie/templates/NOTES.txt | 24 +++ charts/magpie/templates/_helpers.tpl | 46 +++++ charts/magpie/templates/bootstrap.yaml | 13 ++ charts/magpie/templates/deployment.yaml | 124 +++++++++++++ charts/magpie/templates/ingress.yaml | 30 +++ charts/magpie/templates/pvc.yaml | 22 +++ charts/magpie/templates/secret.yaml | 40 ++++ charts/magpie/templates/service.yaml | 15 ++ charts/magpie/templates/webui.yaml | 50 +++++ charts/magpie/values.schema.json | 219 ++++++++++++++++++++++ charts/magpie/values.yaml | 59 ++++++ 18 files changed, 924 insertions(+), 3 deletions(-) create mode 100644 charts/magpie/.helmignore create mode 100644 charts/magpie/Chart.yaml create mode 100644 charts/magpie/README.md create mode 100644 charts/magpie/scripts/start.sh create mode 100644 charts/magpie/templates/NOTES.txt create mode 100644 charts/magpie/templates/_helpers.tpl create mode 100644 charts/magpie/templates/bootstrap.yaml create mode 100644 charts/magpie/templates/deployment.yaml create mode 100644 charts/magpie/templates/ingress.yaml create mode 100644 charts/magpie/templates/pvc.yaml create mode 100644 charts/magpie/templates/secret.yaml create mode 100644 charts/magpie/templates/service.yaml create mode 100644 charts/magpie/templates/webui.yaml create mode 100644 charts/magpie/values.schema.json create mode 100644 charts/magpie/values.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8056a46..c3ae0a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,3 +26,4 @@ jobs: --set controller.storage.size=4Gi \ --set controller.storage.maxBytes=1073741824 \ --set 'nfs.trustedNodeCIDRs[0]=192.0.2.10/32' + helm lint charts/magpie --strict diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 415e5aa..eea174b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,12 +33,13 @@ jobs: --set controller.storage.size=4Gi \ --set controller.storage.maxBytes=1073741824 \ --set 'nfs.trustedNodeCIDRs[0]=192.0.2.10/32' + helm lint charts/magpie --strict - name: Configure Git run: | git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - name: Package chart - run: helm package charts/multica-runtime-controller --destination .cr-release-packages + - name: Package charts + run: helm package charts/multica-runtime-controller charts/magpie --destination .cr-release-packages - name: Install chart-releaser shell: bash run: | diff --git a/README.md b/README.md index f752360..a2cf0c5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Korio Helm Charts -Helm charts for running Korio services on Kubernetes. +Helm charts maintained by Korio for running services on Kubernetes. ## Install @@ -13,6 +13,7 @@ helm repo update | Chart | Description | | --- | --- | +| [magpie](https://github.com/korioinc/helm/tree/main/charts/magpie) | Magpie model gateway with a local management UI and persistent credentials. Defaults to `latest`; set `image.tag` in `values.yaml` (e.g. `"0.1.1275"`) to pin the application version. | | [multica-runtime-controller](https://github.com/korioinc/helm/tree/main/charts/multica-runtime-controller) | Run Multica agent tasks in dedicated Kubernetes Pods with shared workspace storage. | Follow the chart's README for prerequisites, installation, provider configuration, and operations. Each chart includes its defaults in `values.yaml` and validation rules in `values.schema.json`. diff --git a/charts/magpie/.helmignore b/charts/magpie/.helmignore new file mode 100644 index 0000000..478d0a4 --- /dev/null +++ b/charts/magpie/.helmignore @@ -0,0 +1,2 @@ +.DS_Store +*.tgz diff --git a/charts/magpie/Chart.yaml b/charts/magpie/Chart.yaml new file mode 100644 index 0000000..fbe4ca4 --- /dev/null +++ b/charts/magpie/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: magpie +description: >- + Magpie model gateway with a local management UI and persistent credentials. + Set the application version directly with image.tag in values.yaml (for example, 0.1.1275); defaults to latest. +type: application +version: 0.1.0 +home: https://github.com/yetone/magpie +sources: + - https://github.com/yetone/magpie + - https://github.com/korioinc/helm +maintainers: + - name: jskorlol + email: jskorlol@gmail.com +keywords: + - magpie + - ai + - gateway +annotations: + artifacthub.io/category: ai-machine-learning diff --git a/charts/magpie/README.md b/charts/magpie/README.md new file mode 100644 index 0000000..ababe1b --- /dev/null +++ b/charts/magpie/README.md @@ -0,0 +1,237 @@ +# Magpie Helm chart + +This chart runs a persistent Magpie gateway with a local management UI. +It uses the official `ghcr.io/yetone/magpie` image and defaults to `image.tag: "latest"`. +Magpie releases frequently. Set the application version directly in your `values.yaml` with `image.tag` +(for example, `"0.1.1275"`), independently of the chart version. +The supported architectures are Linux amd64 and arm64. + +```mermaid +flowchart LR + Admin[LAN administrator] -->|HTTPS admin hostname| Gate[LAN IP allowlist] + Gate --> UI[Web UI Service] + Client[API client] -->|HTTPS and gateway key| Ingress[API Ingress] + Ingress --> Service[ClusterIP API Service] + Service --> API[Magpie gateway] + UI --> PVC[PVC mounted at /config] + API --> PVC + API -->|Explicit provider/model| Upstream[Registered upstream] +``` + +The default Web UI binds to `127.0.0.1:3430` inside the Pod and uses a local tunnel. +Enabling `webui.ingress` binds it to `0.0.0.0:3430` behind a separate Service and protected HTTPS hostname. +The diagram shows that optional LAN Ingress mode. +The API uses port `3425` and requires enabled gateway keys for remote requests. +The chart uses Magpie's standard listener ports throughout its templates and first-run settings. + +## Install + +The chart creates a Web UI Secret with a random 48-character key during installation. +If the default Secret already exists, the chart reuses its key instead of generating a new one. + +```sh +helm repo add korioinc https://korioinc.github.io/helm +helm repo update +helm upgrade --install magpie korioinc/magpie --namespace tools-magpie --create-namespace --wait +``` + +Repository installation requires a published chart release. +Before publication, install from the repository root with `./charts/magpie` instead of `korioinc/magpie`. + +To pin the application version, create your own `values.yaml`: + +```yaml +image: + tag: "0.1.1275" +``` + +Pass it with `helm upgrade --install magpie korioinc/magpie --namespace tools-magpie -f values.yaml --wait`. +Only change `image.tag` for application updates; no chart release is needed. +A configured `image.digest` overrides the tag, so clear or replace it when changing versions. + +| Setting | Purpose | +| --- | --- | +| `auth.existingSecret` | Empty by default: create or reuse `-webui`. Set a name to reference an externally managed Secret without changing it | +| `auth.webKeySecretKey` | Secret field containing the Web UI key | +| `image.repository`, `image.tag`, `image.digest` | Image selection; defaults to `latest`. Set `image.tag` in `values.yaml` (e.g. `"0.1.1275"`) to pin a version; digest takes precedence | +| `persistence.storageClass` | Storage class; empty uses the cluster default, `-` disables dynamic provisioning | +| `persistence.size` | Requested volume size | +| `persistence.existingClaim` | Reuse an existing writable claim instead of creating one | +| `ingress.enabled` | Enable HTTPS access to the model API | +| `ingress.host` | Public hostname; also sets Magpie's advertised API URL | +| `ingress.className`, `ingress.annotations` | Ingress controller configuration | +| `ingress.tlsSecretName` | Existing TLS Secret in the release namespace | +| `webui.ingress.enabled` | Enable LAN administration through Traefik | +| `webui.ingress.host` | Admin hostname, separate from the API hostname | +| `webui.ingress.className`, `webui.ingress.annotations` | Admin Ingress configuration | +| `webui.ingress.tlsSecretName` | Existing TLS Secret for the admin hostname | + +The chart defaults to the `latest` image tag and no Ingress. An image tag or digest must remain set. +An enabled Ingress requires a hostname and TLS Secret. +Ingress routes all paths to the API listener, including `/v1`, `/models`, `/muse-code/models`, and `/v1beta`. +Management endpoints stay on the separate Web UI listener. +The optional admin Ingress requires a distinct hostname and an existing Traefik access middleware. +Set `webui.ingress.annotations.traefik.ingress.kubernetes.io/router.middlewares` to the appropriate private-access rule. +The middleware must restrict access to trusted networks; the chart cannot verify its policy. +The chart preserves this annotation and creates no additional middleware or Traefik CRD. +Use LAN DNS pointing to the private Ingress entrypoint, so LAN browsers reach it directly. +API Ingress annotations and paths remain independent of administration. + +## Web UI Secret lifecycle + +By default, the Secret is `-webui` in the release namespace (`magpie-webui` for the commands above). +The key is stored in `auth.webKeySecretKey`, which defaults to `web-key`. +A generated key has 48 random alphanumeric characters. Existing keys must have at least 16 characters +and use only letters, digits, or `-._~`. A missing or invalid key fails installation or upgrade; it is never silently replaced. + +The chart reads the existing Secret through Helm `lookup` and preserves its data during upgrades. +A Secret managed by this release is retained on uninstall with `helm.sh/resource-policy: keep`. +Reinstalling with the same release name, namespace, and fullname reuses that key. +A pre-existing unmanaged Secret is only referenced, not adopted or changed. +A Secret owned by another Helm release requires explicit `auth.existingSecret` to avoid accidental sharing. + +To use an external Secret, set: + +```yaml +auth: + existingSecret: my-magpie-webui + webKeySecretKey: web-key +``` + +This path does not create, read, validate, or modify the Secret; provide a valid key before the Pod starts. +For older installations that used the fixed `magpie-webui` default with a different release/fullname, +set `auth.existingSecret: magpie-webui` before upgrading to preserve the existing login key. + +Automatic reuse requires Helm to access the cluster API and read Secrets in the release namespace. +`helm template` and client-side dry runs cannot look up existing keys and generate a fresh preview key each time. +For offline-rendered GitOps deployments, manage the Secret externally and set `auth.existingSecret`. +Use `--dry-run=server` for a cluster-aware preview; rendered manifests contain credentials, so keep output private. +Helm release history also contains generated credentials. Restrict access to Secrets and release records. +Helm rollback restores stored manifests without `lookup` and can restore an older key after manual rotation. +Use an externally managed Secret when key rotation must remain independent of Helm history. + +## Configure providers and clients + +With LAN Ingress enabled, open `https:///?k=` from an allowed source network. +With the default loopback mode, start a tunnel on the administrator's computer: + +```sh +kubectl --namespace tools-magpie port-forward deployment/magpie 3430:3430 --address 127.0.0.1 +``` + +Retrieve the Web UI key (use the Secret name and field shown in Helm NOTES if overridden): + +```sh +kubectl --namespace tools-magpie get secret magpie-webui \ + -o jsonpath='{.data.web-key}' | base64 --decode +``` + +For loopback mode, open `http://127.0.0.1:3430/?k=`. +Use the release's actual deployment name when it differs. + +1. Add providers or supported subscription accounts in the Web UI. +2. Register vLLM or CLIProxyAPI as custom OpenAI-compatible providers when needed. +3. Use each upstream's reachable URL, such as its Kubernetes Service DNS name. +4. Refresh the model list and select the models to expose. +5. Create a named gateway key for each API client under **Gateway → Gateway keys**. +6. Request the full `provider/model` ID returned by `/v1/models`. + +The chart creates no gateway keys. A new installation rejects all remote API requests until a key exists. +Web UI keys and API gateway keys are independent. + +New installations disable automatic routing groups and automatic image description or image-model selection. +The chart seeds `settings.json` and `providers.json` only when each file is absent. +It preserves subsequent Web UI changes, account credentials, provider configuration, and named gateway keys. +Do not enable provider fallback lists, routing groups, model remapping, or vendor `Auto` models for fixed model selection. +An administrator can change these policies in the Web UI; the chart does not lock application settings. +An existing or restored volume keeps its policies. Review them before reconnecting clients. + +For Codex OAuth, copy the final `http://localhost:1455/auth/callback?...` URL into the Web UI's callback field. +The browser's failed localhost redirect is expected when the callback listener runs inside Kubernetes. +No public OAuth callback route is required. Other provider login flows require their own verification. + +The official image does not contain Claude Code. Claude subscription forwarding requires an image that includes it. +An Anthropic API-key provider or a suitable external upstream does not require that local executable. +When using a custom image, set its repository and tag and clear or replace `image.digest`. +The image must retain `/magpie`, `/bin/sh`, and writable HOME/XDG directories under `/config`. + +## Verify after deployment + +Use the public endpoint for authentication checks. API port-forwarding reaches loopback and bypasses remote authentication. +With sharing enabled, the first request below should return `401`: + +```sh +curl -i https://magpie.example.com/v1/models +curl --fail-with-body https://magpie.example.com/v1/models \ + -H "Authorization: Bearer $MAGPIE_GATEWAY_KEY" +curl --fail-with-body https://magpie.example.com/v1/chat/completions \ + -H "Authorization: Bearer $MAGPIE_GATEWAY_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"model":"/","messages":[{"role":"user","content":"Hello"}],"stream":true}' +``` + +Use `https:///v1` for OpenAI-compatible clients, including Codex Responses requests. +Use `https://` for Anthropic-compatible clients, including Claude Code. +Client model catalogs and protocol support still need verification with the actual client and upstream. +Test provider selection, upstream errors without model substitution, streaming, tool calls, and restart persistence. + +## Storage, updates, and access + +The Pod runs as UID/GID `65532`, with `fsGroup: 65532` for the claim. +`/config` holds accounts, API credentials, plugins, catalogs, and application state. +Back up that volume and restrict access to it. Claim annotations retain newly created storage on Helm uninstall. +An existing claim must be writable by the container user. Local storage remains tied to its Kubernetes node. + +The deployment uses one replica and `Recreate` updates to avoid concurrent file writers. +Updates cause downtime and can interrupt active streams; Magpie's Web mode does not guarantee stream draining. +Do not run two releases against the same claim. Retained claims require explicit adoption or `existingClaim` on reinstall. +First-run settings disable application auto-update, and the read-only root filesystem protects the installed binary. +Change `image.tag` in your `values.yaml` when updating the application (for example, `"0.1.1275"`). +A configured digest takes precedence over the tag. +`Chart.yaml` `version` identifies the chart package; it does not select the application image. +The chart omits `appVersion` because application versions are selected independently. + +The default `image.pullPolicy: Always` resolves `latest` whenever a Pod starts. +It does not update running Pods, and a Helm upgrade with unchanged values does not trigger a restart. +Use `kubectl --namespace tools-magpie rollout restart deployment/magpie` to fetch a newer `latest` image. +A mutable tag can change on any Pod restart; pin a release tag or digest for reproducible deployments. +Restart the deployment after rotating the Web UI Secret. + +The chart explicitly clears the image's `MAGPIE_ADDR` override. +Sharing then controls the API listener and its remote authentication. +Disabling sharing closes external API access. +With admin Ingress enabled, readiness checks the gateway's loopback health, keeping the admin Service available. +With loopback-only administration, API TCP readiness still removes unreachable public endpoints. +Keep the Gateway port in Settings at its default `3425`; changing it in the Web UI disconnects the Service. +For a restored volume with another Gateway port, set it back through the Web UI. + +Ingress must forward to the Pod IP through the API Service. +A proxy in the same Pod that forwards through loopback would bypass native gateway authentication. +Magpie prints a Web UI login link during startup, so treat application logs as sensitive. +Its printed network link may use `MAGPIE_PUBLIC_URL`, which belongs to the API host. +Use the admin hostname from Helm NOTES or your `values.yaml` for browser login. +The chart does not enable request-body logging or telemetry. + +Configure streaming timeouts in the chosen Ingress controller. Avoid response-buffering middleware. +This chart does not change shared controller settings. + +## Verification commands + +```sh +# Run from the korioinc/helm repository root. +helm lint ./charts/magpie --strict +helm template magpie ./charts/magpie --namespace tools-magpie +helm package ./charts/magpie --destination /tmp +``` + +Add `-f values.yaml` to lint or render your installation overrides. + +## Upstream evidence + +The original chart implementation was checked against Magpie `0.1.1024`, commit `528d6c12dc11558f55aa00a39c7ba988c83520eb`. +The installed image is selected by installation values. The links below record the original runtime review. + +- [Docker image and filesystem layout](https://github.com/yetone/magpie/blob/528d6c12dc11558f55aa00a39c7ba988c83520eb/Dockerfile) +- [Listener selection and remote authentication](https://github.com/yetone/magpie/blob/528d6c12dc11558f55aa00a39c7ba988c83520eb/internal/gateway/lan.go) +- [Automatic routing groups](https://github.com/yetone/magpie/blob/528d6c12dc11558f55aa00a39c7ba988c83520eb/internal/provider/group.go) +- [Headless Web UI and OAuth callback instructions](https://github.com/yetone/magpie/blob/528d6c12dc11558f55aa00a39c7ba988c83520eb/docs/reference.md#docker) diff --git a/charts/magpie/scripts/start.sh b/charts/magpie/scripts/start.sh new file mode 100644 index 0000000..87be7f3 --- /dev/null +++ b/charts/magpie/scripts/start.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu +umask 077 + +mkdir -p /config/magpie +# Seed new installations only. Keep Web UI changes and restored credentials intact. +for name in settings.json providers.json; do + target="/config/magpie/$name" + if [ ! -e "$target" ]; then + temporary=$(mktemp "$target.XXXXXX") + cat "/bootstrap/$name" > "$temporary" + chmod 600 "$temporary" + mv "$temporary" "$target" + fi +done + +exec /magpie "$@" diff --git a/charts/magpie/templates/NOTES.txt b/charts/magpie/templates/NOTES.txt new file mode 100644 index 0000000..9696a47 --- /dev/null +++ b/charts/magpie/templates/NOTES.txt @@ -0,0 +1,24 @@ +{{- if .Values.webui.ingress.enabled }} +LAN Web UI: https://{{ .Values.webui.ingress.host }}/?k= +Access middleware: {{ index .Values.webui.ingress.annotations "traefik.ingress.kubernetes.io/router.middlewares" }} +LAN DNS must resolve the admin hostname to the private Ingress entrypoint. +{{- else }} +The Web UI listens on Pod loopback only. Open a local tunnel: + + kubectl --namespace {{ .Release.Namespace }} port-forward deployment/{{ include "magpie.fullname" . }} {{ include "magpie.webuiPort" . }}:{{ include "magpie.webuiPort" . }} --address 127.0.0.1 + +Open http://127.0.0.1:{{ include "magpie.webuiPort" . }}/?k=. +{{- end }} + +Retrieve the Web UI key from Secret {{ include "magpie.webuiSecretName" . }}, field {{ .Values.auth.webKeySecretKey }}: + + kubectl --namespace {{ .Release.Namespace }} get secret {{ include "magpie.webuiSecretName" . }} -o go-template='{{ "{{ index .data " }}{{ .Values.auth.webKeySecretKey | quote }}{{ " | base64decode }}" }}' + +Keep this key private. It is separate from API gateway keys. + +Create named gateway keys in the Web UI before connecting external API clients. +Remote API requests without an enabled gateway key are denied. +{{- if .Values.ingress.enabled }} +OpenAI-compatible API base URL: https://{{ .Values.ingress.host }}/v1 +Anthropic-compatible API base URL: https://{{ .Values.ingress.host }} +{{- end }} diff --git a/charts/magpie/templates/_helpers.tpl b/charts/magpie/templates/_helpers.tpl new file mode 100644 index 0000000..ac2bfb7 --- /dev/null +++ b/charts/magpie/templates/_helpers.tpl @@ -0,0 +1,46 @@ +{{- define "magpie.apiPort" -}}3425{{- end -}} +{{- define "magpie.webuiPort" -}}3430{{- end -}} + +{{- define "magpie.webuiName" -}} +{{- printf "%s-webui" (include "magpie.fullname" . | trunc 57 | trimSuffix "-") -}} +{{- end -}} + +{{- define "magpie.webuiSecretName" -}} +{{- default (include "magpie.webuiName" .) .Values.auth.existingSecret -}} +{{- end -}} + +{{- define "magpie.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "magpie.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else if contains (include "magpie.name" .) .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name (include "magpie.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "magpie.selectorLabels" -}} +app.kubernetes.io/name: {{ include "magpie.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "magpie.labels" -}} +{{ include "magpie.selectorLabels" . }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | quote }} +{{- if .Values.image.tag }} +app.kubernetes.io/version: {{ .Values.image.tag | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "magpie.image" -}} +{{- if .Values.image.digest -}} +{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}} +{{- else -}} +{{- printf "%s:%s" .Values.image.repository (required "Set image.tag or image.digest in the installation values" .Values.image.tag) -}} +{{- end -}} +{{- end -}} diff --git a/charts/magpie/templates/bootstrap.yaml b/charts/magpie/templates/bootstrap.yaml new file mode 100644 index 0000000..a98f22a --- /dev/null +++ b/charts/magpie/templates/bootstrap.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "magpie.fullname" . }}-bootstrap + labels: + {{- include "magpie.labels" . | nindent 4 }} +data: + settings.json: | + {{- dict "lan" true "port" (include "magpie.apiPort" . | int) "noAutoUpdate" true "vision" "off" "imageGen" "off" | toPrettyJson | nindent 4 }} + providers.json: | + {"providers": [], "noAutoGroups": true} + start.sh: | + {{- .Files.Get "scripts/start.sh" | nindent 4 }} diff --git a/charts/magpie/templates/deployment.yaml b/charts/magpie/templates/deployment.yaml new file mode 100644 index 0000000..5793ff1 --- /dev/null +++ b/charts/magpie/templates/deployment.yaml @@ -0,0 +1,124 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "magpie.fullname" . }} + labels: + {{- include "magpie.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "magpie.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "magpie.selectorLabels" . | nindent 8 }} + annotations: + checksum/bootstrap: {{ include (print $.Template.BasePath "/bootstrap.yaml") . | sha256sum }} + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 60 + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: magpie + image: {{ include "magpie.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/bin/sh", "/bootstrap/start.sh"] + args: + - web + - --addr + - {{ printf "%s:%s" (ternary "0.0.0.0" "127.0.0.1" .Values.webui.ingress.enabled) (include "magpie.webuiPort" .) | quote }} + - --no-open + - --gateway + env: + # Clear the image's override. Settings then close external access when LAN sharing is disabled. + - name: MAGPIE_ADDR + value: "" + - name: MAGPIE_WEB_KEY + valueFrom: + secretKeyRef: + name: {{ include "magpie.webuiSecretName" . | quote }} + key: {{ .Values.auth.webKeySecretKey | quote }} + {{- if .Values.ingress.enabled }} + - name: MAGPIE_PUBLIC_URL + value: {{ printf "https://%s" .Values.ingress.host | quote }} + {{- end }} + - name: MAGPIE_NO_STATS + value: "1" + ports: + - name: api + containerPort: {{ include "magpie.apiPort" . }} + - name: webui + containerPort: {{ include "magpie.webuiPort" . }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + startupProbe: + exec: + command: ["/magpie", "healthcheck"] + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 30 + livenessProbe: + exec: + command: ["/magpie", "healthcheck"] + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + {{- if .Values.webui.ingress.enabled }} + # Keep administration available when the administrator disables API sharing. + exec: + command: ["/magpie", "healthcheck"] + {{- else }} + tcpSocket: + port: api + {{- end }} + periodSeconds: 5 + timeoutSeconds: {{ ternary 5 3 .Values.webui.ingress.enabled }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /config + - name: bootstrap + mountPath: /bootstrap + readOnly: true + - name: tmp + mountPath: /tmp + volumes: + - name: config + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (include "magpie.fullname" .) }} + - name: bootstrap + configMap: + name: {{ include "magpie.fullname" . }}-bootstrap + - name: tmp + emptyDir: {} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/magpie/templates/ingress.yaml b/charts/magpie/templates/ingress.yaml new file mode 100644 index 0000000..515a506 --- /dev/null +++ b/charts/magpie/templates/ingress.yaml @@ -0,0 +1,30 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "magpie.fullname" . }} + labels: + {{- include "magpie.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className | quote }} + {{- end }} + tls: + - hosts: [{{ .Values.ingress.host | quote }}] + secretName: {{ .Values.ingress.tlsSecretName | quote }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "magpie.fullname" . }} + port: + name: api +{{- end }} diff --git a/charts/magpie/templates/pvc.yaml b/charts/magpie/templates/pvc.yaml new file mode 100644 index 0000000..2a21723 --- /dev/null +++ b/charts/magpie/templates/pvc.yaml @@ -0,0 +1,22 @@ +{{- if not .Values.persistence.existingClaim }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "magpie.fullname" . }} + labels: + {{- include "magpie.labels" . | nindent 4 }} + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: ["ReadWriteOnce"] + {{- if eq .Values.persistence.storageClass "-" }} + storageClassName: "" + {{- else if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- end }} diff --git a/charts/magpie/templates/secret.yaml b/charts/magpie/templates/secret.yaml new file mode 100644 index 0000000..0eef353 --- /dev/null +++ b/charts/magpie/templates/secret.yaml @@ -0,0 +1,40 @@ +{{- if not .Values.auth.existingSecret }} +{{- $name := include "magpie.webuiSecretName" . }} +{{- $secret := lookup "v1" "Secret" .Release.Namespace $name }} +{{- $data := dict }} +{{- $owned := false }} +{{- if $secret }} + {{- $data = default (dict) $secret.data }} + {{- $key := required (printf "Secret %s must contain auth.webKeySecretKey %s; refusing to replace an existing credential" $name .Values.auth.webKeySecretKey) (index $data .Values.auth.webKeySecretKey) }} + {{- if not (regexMatch "^[A-Za-z0-9._~-]{16,}$" ($key | b64dec)) }} + {{- fail (printf "Secret %s field %s must contain at least 16 URL-safe characters (letters, digits, -._~); refusing to replace it" $name .Values.auth.webKeySecretKey) }} + {{- end }} + {{- $labels := default (dict) $secret.metadata.labels }} + {{- $annotations := default (dict) $secret.metadata.annotations }} + {{- $owned = and (eq (index $labels "app.kubernetes.io/managed-by") .Release.Service) (eq (index $annotations "meta.helm.sh/release-name") .Release.Name) (eq (index $annotations "meta.helm.sh/release-namespace") .Release.Namespace) }} + {{- if and (not $owned) (or (eq (index $labels "app.kubernetes.io/managed-by") "Helm") (index $annotations "meta.helm.sh/release-name") (index $annotations "meta.helm.sh/release-namespace")) }} + {{- fail (printf "Secret %s has different Helm ownership; set auth.existingSecret explicitly to reuse it without managing it" $name) }} + {{- end }} +{{- else }} + {{- $_ := set $data .Values.auth.webKeySecretKey (randAlphaNum 48 | b64enc) }} +{{- end }} +{{- /* Do not adopt or modify an existing externally managed Secret. */}} +{{- if or (not $secret) $owned }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "magpie.labels" . | nindent 4 }} + annotations: + helm.sh/resource-policy: keep + meta.helm.sh/release-name: {{ .Release.Name | quote }} + meta.helm.sh/release-namespace: {{ .Release.Namespace | quote }} +type: Opaque +{{- if and $secret (hasKey $secret "immutable") }} +immutable: {{ $secret.immutable }} +{{- end }} +data: + {{- toYaml $data | nindent 2 }} +{{- end }} +{{- end }} diff --git a/charts/magpie/templates/service.yaml b/charts/magpie/templates/service.yaml new file mode 100644 index 0000000..88f08eb --- /dev/null +++ b/charts/magpie/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "magpie.fullname" . }} + labels: + {{- include "magpie.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "magpie.selectorLabels" . | nindent 4 }} + ports: + - name: api + port: {{ include "magpie.apiPort" . }} + targetPort: api + protocol: TCP diff --git a/charts/magpie/templates/webui.yaml b/charts/magpie/templates/webui.yaml new file mode 100644 index 0000000..68e106f --- /dev/null +++ b/charts/magpie/templates/webui.yaml @@ -0,0 +1,50 @@ +{{- if .Values.webui.ingress.enabled }} +{{- if and .Values.ingress.enabled (eq .Values.ingress.host .Values.webui.ingress.host) }} +{{- fail "The API and Web UI Ingress hosts must differ when both are enabled." }} +{{- end }} +{{- $webuiName := include "magpie.webuiName" . }} +apiVersion: v1 +kind: Service +metadata: + name: {{ $webuiName }} + labels: + {{- include "magpie.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "magpie.selectorLabels" . | nindent 4 }} + ports: + - name: webui + port: {{ include "magpie.webuiPort" . }} + targetPort: webui + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $webuiName }} + labels: + {{- include "magpie.labels" . | nindent 4 }} + {{- with .Values.webui.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.webui.ingress.className }} + ingressClassName: {{ .Values.webui.ingress.className | quote }} + {{- end }} + tls: + - hosts: [{{ .Values.webui.ingress.host | quote }}] + secretName: {{ .Values.webui.ingress.tlsSecretName | quote }} + rules: + - host: {{ .Values.webui.ingress.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ $webuiName }} + port: + name: webui +{{- end }} diff --git a/charts/magpie/values.schema.json b/charts/magpie/values.schema.json new file mode 100644 index 0000000..dafd8d2 --- /dev/null +++ b/charts/magpie/values.schema.json @@ -0,0 +1,219 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "image", + "auth", + "persistence", + "ingress", + "webui" + ], + "properties": { + "image": { + "type": "object", + "required": [ + "repository", + "tag", + "digest", + "pullPolicy" + ], + "properties": { + "repository": { + "type": "string", + "minLength": 1 + }, + "tag": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^$|^sha256:[a-f0-9]{64}$" + }, + "pullPolicy": { + "enum": [ + "Always", + "IfNotPresent", + "Never" + ] + } + }, + "if": { + "properties": { + "digest": { + "const": "" + } + } + }, + "then": { + "properties": { + "tag": { + "minLength": 1 + } + } + } + }, + "auth": { + "type": "object", + "required": [ + "existingSecret", + "webKeySecretKey" + ], + "properties": { + "existingSecret": { + "type": "string" + }, + "webKeySecretKey": { + "type": "string", + "minLength": 1, + "maxLength": 253, + "pattern": "^[a-zA-Z0-9._-]+$" + } + } + }, + "persistence": { + "type": "object", + "required": [ + "existingClaim", + "storageClass", + "size", + "annotations" + ], + "properties": { + "existingClaim": { + "type": "string" + }, + "storageClass": { + "type": "string" + }, + "size": { + "type": "string", + "minLength": 1 + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "ingress": { + "type": "object", + "required": [ + "enabled", + "host", + "className", + "annotations", + "tlsSecretName" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "host": { + "type": "string", + "pattern": "^$|^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$" + }, + "className": { + "type": "string" + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tlsSecretName": { + "type": "string" + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "host": { + "minLength": 1 + }, + "tlsSecretName": { + "minLength": 1 + } + } + } + }, + "webui": { + "type": "object", + "required": [ + "ingress" + ], + "properties": { + "ingress": { + "type": "object", + "required": [ + "enabled", + "host", + "className", + "annotations", + "tlsSecretName" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "host": { + "type": "string", + "pattern": "^$|^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$" + }, + "className": { + "type": "string" + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tlsSecretName": { + "type": "string" + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "host": { + "minLength": 1 + }, + "tlsSecretName": { + "minLength": 1 + }, + "className": { + "const": "traefik" + }, + "annotations": { + "required": [ + "traefik.ingress.kubernetes.io/router.middlewares" + ], + "properties": { + "traefik.ingress.kubernetes.io/router.middlewares": { + "type": "string", + "minLength": 1 + } + } + } + } + } + } + } + } + } +} diff --git a/charts/magpie/values.yaml b/charts/magpie/values.yaml new file mode 100644 index 0000000..9061b08 --- /dev/null +++ b/charts/magpie/values.yaml @@ -0,0 +1,59 @@ +image: + repository: ghcr.io/yetone/magpie + # Magpie updates frequently. Set the application version here, independently of the chart version. + # Use a release tag to pin the version, for example: tag: "0.1.1275". + tag: "latest" + # A digest takes precedence over the tag. + digest: "" + # Always checks for a new image on Pod startup; it does not restart running Pods. + pullPolicy: Always +imagePullSecrets: [] + +nameOverride: "" +fullnameOverride: "" + +# This key authenticates the Web UI, independently of API gateway keys. +auth: + # Empty: create -webui with a random key, or reuse its existing key. + # Set a name to use an externally managed Secret without creating or changing it. + existingSecret: "" + webKeySecretKey: web-key + +# Keep a single replica with Recreate updates; Magpie owns files on this volume. +persistence: + existingClaim: "" + storageClass: "" + size: 5Gi + annotations: + helm.sh/resource-policy: keep + +# This Ingress connects only to the model API. +# Enabling the Ingress requires a host and an existing TLS Secret. +ingress: + enabled: false + className: "" + host: "" + annotations: {} + tlsSecretName: "" + +# Optional LAN administration on a separate HTTPS host using an existing Traefik middleware. +# Disabled keeps the Web UI on Pod loopback without a Service or Ingress. +webui: + ingress: + enabled: false + className: "" + host: "" + annotations: {} + tlsSecretName: "" + +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + +nodeSelector: {} +tolerations: [] +affinity: {}