Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,34 @@ jobs:

- name: Run pre-commit
run: pip install pre-commit && pre-commit run --all-files

# Lint + render the Helm chart so template/value regressions are caught in CI.
helm:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4

- uses: azure/setup-helm@v4
with:
version: "v3.16.3"

- name: Helm lint
run: helm lint deploy/helm/emry

- name: Helm template (default values)
run: helm template emry deploy/helm/emry

- name: Helm template (auth token + ingress + PVC)
run: |
helm template emry deploy/helm/emry \
--set auth.token=ci-secret \
--set ingress.enabled=true \
--set ingress.host=emry.example.com \
--set logs.existingClaim=emry-logs

- name: Helm template (auth + ingress + PVC)
run: |
helm template emry deploy/helm/emry \
--set auth.token=ci-token \
--set ingress.enabled=true \
--set logs.existingClaim=emry-logs
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
# Helm templates are Go templates, not valid YAML; check values/Chart only.
exclude: ^deploy/helm/.*/templates/
- id: check-toml
- id: check-merge-conflict
- id: check-added-large-files
Expand Down
65 changes: 65 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# syntax=docker/dockerfile:1

# ---- Builder ---------------------------------------------------------------
# Pin to the workspace MSRV (see rust-toolchain.toml). The `emry` binary is
# produced by the `emry-cli` crate.
FROM rust:1.88-slim AS builder

WORKDIR /src

# Cache-friendly layering: copy only the manifests first so `cargo fetch`
# (dependency resolution + download) is cached until Cargo.toml/Cargo.lock
# actually change, not on every source edit.
COPY Cargo.toml Cargo.lock ./
COPY crates ./crates
# The Python SDK is part of the workspace's file tree but not needed to build
# the CLI; the manifests above reference only the Rust crates.

RUN --mount=type=cache,target=/usr/local/cargo/registry \
cargo fetch

# Now build the release binary. A cache mount on the target dir keeps
# incremental artifacts across builds; we copy the binary out afterwards.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
cargo build --release -p emry-cli \
&& cp /src/target/release/emry /usr/local/bin/emry

# ---- Runtime ---------------------------------------------------------------
# Debian slim (NOT distroless): the GPU poller and other helpers may shell out
# (e.g. `nvidia-smi`), so we keep a real shell and libc. Still small.
FROM debian:bookworm-slim AS runtime

# ca-certificates is handy for any outbound TLS (webhook/Slack alerts); tini as
# a tiny init so signals (SIGTERM on pod stop) are forwarded cleanly.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tini \
&& rm -rf /var/lib/apt/lists/*

# Non-root user. Fixed UID/GID so a mounted /logs volume can be chowned to it.
RUN groupadd --gid 10001 emry \
&& useradd --uid 10001 --gid 10001 --create-home --shell /usr/sbin/nologin emry

COPY --from=builder /usr/local/bin/emry /usr/local/bin/emry

# The dashboard reads a directory of run logs from /logs. Mount a volume here
# (a PVC or hostPath in Kubernetes; see deploy/helm/emry). Owned by the emry
# user so file-mode writers running as the same user can populate it.
RUN mkdir -p /logs && chown emry:emry /logs
VOLUME ["/logs"]

USER emry
WORKDIR /home/emry

# The live web dashboard binds this port (default 8787 in `emry web`).
EXPOSE 8787

# tini reaps zombies and forwards signals to `emry`.
ENTRYPOINT ["/usr/bin/tini", "--", "emry"]

# Serve the multi-run project dashboard over /logs. NOTE: `emry web` takes the
# log directory via `--project <PATH>` (there is no separate --log-dir flag for
# the web subcommand); `--project` is the directory it scans for runs.
# --host 0.0.0.0 so the dashboard is reachable from outside the container; set
# EMRY_AUTH_TOKEN (and TLS) when exposing it beyond localhost.
CMD ["web", "--project", "/logs", "--port", "8787", "--host", "0.0.0.0"]
17 changes: 17 additions & 0 deletions deploy/helm/emry/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Patterns to ignore when packaging Helm charts.
.DS_Store
.git/
.gitignore
.bzr/
.hg/
.hgignore
.svn/
*.swp
*.bak
*.tmp
*.orig
*~
.project
.idea/
*.tmproj
.vscode/
18 changes: 18 additions & 0 deletions deploy/helm/emry/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: v2
name: emry
description: Emry live training-metrics dashboard — serves the multi-run web UI over a directory of run logs.
type: application
# Chart version — bump on chart changes.
version: 0.1.0
# The Emry release the default image tracks.
appVersion: "0.1.0"
home: https://github.com/femboyisp/emry
sources:
- https://github.com/femboyisp/emry
keywords:
- emry
- machine-learning
- metrics
- dashboard
maintainers:
- name: femboyisp contributors
46 changes: 46 additions & 0 deletions deploy/helm/emry/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Emry dashboard {{ .Chart.AppVersion }} has been deployed as release {{ .Release.Name }}.

The web server is running:

emry web --project {{ .Values.logsMountPath }} --port {{ .Values.service.port }}

It serves the multi-run project dashboard over the logs volume mounted at
{{ .Values.logsMountPath }} and exposes a liveness/readiness probe at GET /healthz.

Reach the dashboard:
{{- if .Values.ingress.enabled }}

http{{ if .Values.ingress.tls }}s{{ end }}://{{ .Values.ingress.host }}{{ .Values.ingress.path }}
{{- else if eq .Values.service.type "LoadBalancer" }}

Watch the external IP be assigned:
kubectl get svc -w {{ include "emry.fullname" . }} --namespace {{ .Release.Namespace }}
{{- else if eq .Values.service.type "NodePort" }}

export NODE_PORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" svc {{ include "emry.fullname" . }} --namespace {{ .Release.Namespace }})
export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}" --namespace {{ .Release.Namespace }})
echo "http://$NODE_IP:$NODE_PORT"
{{- else }}

Port-forward to your machine:
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "emry.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
# then open http://127.0.0.1:{{ .Values.service.port }}
{{- end }}

{{- if or .Values.auth.token .Values.auth.existingSecret }}

Dashboard auth is ENABLED: requests must present the token from Secret
"{{ include "emry.authSecretName" . }}" (key "{{ include "emry.authSecretKey" . }}"),
wired into the container as EMRY_AUTH_TOKEN.
{{- else }}

Dashboard auth is DISABLED. Set `auth.token` (or `auth.existingSecret`) to gate
access via EMRY_AUTH_TOKEN.
{{- end }}

{{- if and (not .Values.logs.existingClaim) (not .Values.logs.hostPath) }}

WARNING: no persistent logs volume configured (using an ephemeral emptyDir).
The dashboard will show no runs unless something populates {{ .Values.logsMountPath }}.
Set `logs.existingClaim` (a PVC) or `logs.hostPath` to mount your run logs.
{{- end }}
85 changes: 85 additions & 0 deletions deploy/helm/emry/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "emry.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this
(by the DNS naming spec).
*/}}
{{- define "emry.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "emry.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "emry.labels" -}}
helm.sh/chart: {{ include "emry.chart" . }}
{{ include "emry.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "emry.selectorLabels" -}}
app.kubernetes.io/name: {{ include "emry.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Create the name of the service account to use.
*/}}
{{- define "emry.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "emry.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

{{/*
Name of the Secret holding the dashboard auth token.
Uses the user-provided existingSecret when set, otherwise the chart-managed one.
*/}}
{{- define "emry.authSecretName" -}}
{{- if .Values.auth.existingSecret }}
{{- .Values.auth.existingSecret }}
{{- else }}
{{- printf "%s-auth" (include "emry.fullname" .) }}
{{- end }}
{{- end }}

{{/*
Key within the auth Secret that holds the token.
*/}}
{{- define "emry.authSecretKey" -}}
{{- if .Values.auth.existingSecret }}
{{- default "token" .Values.auth.existingSecretKey }}
{{- else }}
{{- "token" }}
{{- end }}
{{- end }}
Loading
Loading