Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/pkg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
# name: docs
# path: docs
- name: Setup Go environment
uses: actions/setup-go@v6
uses: actions/setup-go@v7
- name: Generate index.html
run: |
go install github.com/halkeye/helm-repo-html@v0.2.1
Expand Down
1 change: 1 addition & 0 deletions .markdownlint-cli2.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
gitignore: true
ignores:
- "**/node_modules/**"
- "CLAUDE.md"
config:
default: true
MD013:
Expand Down
83 changes: 83 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# AGENTS.md

Guidance for coding agents working in this repository.

## What this is

A collection of Helm charts published as a chart repository at `https://devpro.github.io/helm-charts`, alongside a VitePress documentation site.
Charts cover both custom applications maintained by devpro and third-party applications packaged for convenience.

## Repository layout

- **`charts/`**: the source of every chart, one directory per chart.
[Helm Chart Releaser](https://github.com/helm/chart-releaser) supports neither multiple chart directories nor nested levels, so every chart lives directly under `charts/` and nowhere else.
- **`docs/`**: the VitePress site, with application guides in `docs/application-guides` and custom chart documentation in `docs/custom-charts`.
- **`samples/`**: example manifests and values.
- **`scripts/`**: helper scripts, including `add_helm_repo.sh` which adds the dependency repositories the charts need.

## Common commands

```bash
helm lint charts/<chart> # lint one chart
helm template myapp charts/<chart> -f values.yaml --namespace myns # render the manifests
helm upgrade --install myapp charts/<chart> -f values.yaml --namespace myns --create-namespace
kube-linter lint charts --config .kube-linter.yaml # the check CI runs
npm run docs:dev # serve the documentation site
```

## Releasing a chart

A chart is published when its `version` in `Chart.yaml` changes on `main`, so a change to any chart file needs a version bump in the same commit or nothing is released.
`version` is the chart's own version and `appVersion` is the version of the packaged application: they move independently.
Where a chart pins an image tag in `values.yaml`, that tag and `appVersion` must be updated together.

## Conventions

- Chart values are documented with `# --` comments above the key, which is the helm-docs annotation form.
- A chart that packages a devpro application keeps its values structure close to the application's configuration keys, so that a setting can be traced from `values.yaml` to the environment variable it becomes.
- CI runs `kube-linter` over `charts/` with `.kube-linter.yaml`, and `ct lint` over the charts changed against `main`.
Exclusions belong in `.kube-linter.yaml` with a comment saying why.
- Markdown and YAML are linted in CI through `.markdownlint-cli2.yaml` and `.yamllint.yaml`.
- Do not run markdownlint: linting is run manually by the maintainer and in CI.
- Files carrying real values for a local deployment are named `values.mine.yaml` and are gitignored, so they are never edited or committed.

## Writing style

These rules apply to Markdown, YAML comments, chart templates, code comments, commit messages, and any prose in scripts.

**One sentence per line.**
A line break only ever happens at the end of a sentence, and a sentence is never wrapped across two lines.
There is no maximum line length: screens are wide, and the 80 character convention is not used here.
This applies to comments as much as to prose, so a long comment sentence stays on a single line rather than continuing onto a second comment line.
Wrapping is handled by the editor, not by hard newlines.

**Never use the em dash (`—`) or the en dash (`–`).**
Use a colon when introducing an explanation, a comma when joining clauses, or a full stop and a new sentence.
This applies to prose, code comments, table cells, and error message strings.

**Never use the second person.**
No "you", no "your", not even in placeholders such as `<your-token>`, which should read `<token>`.
The documentation describes the repository, it does not address a reader.
Write "the working tree", not "your working tree".

**Other conventions.**
Use `ini` as the fence language for `.properties` blocks, never `properties`.
Prefer `>` over `→` when describing UI navigation, for example **Project Settings > Quality Gate**.

Existing files predate these rules and break them in places.
That is not a reason to add more, and not a reason to reformat prose that a change does not otherwise touch.

### Scripts

Shell scripts are named in `snake_case`, which is the standard for bash: `add_helm_repo.sh`, not `add-helm-repo.sh`.

Scripts must be committed with the executable bit set.
A script committed as `100644` fails on a fresh clone even though it works locally:

```bash
git update-index --chmod=+x path/to/script.sh
```

### Target platform

Target platform is Linux with Docker, including WSL2, and `bash`.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
2 changes: 1 addition & 1 deletion charts/keeptrack/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: keeptrack
description: Helm chart for Keeptrack
type: application
version: 0.2.5
version: 0.2.6
appVersion: "2.4.1"
home: https://github.com/devpro/keeptrack
sources:
Expand Down
6 changes: 4 additions & 2 deletions charts/keeptrack/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ spec:
{{- if eq .role "frontend" }}
- name: WebApi__BaseUrl
value: "http://{{ $.Values.webapi.name }}"
- name: Features__IsWebSocketsOnlyEnabled
value: {{ .webSocketsOnly | quote }}
{{- if .dataProtection.enabled }}
{{- if .dataProtection.connectionStringSecretKeyRef }}
- name: DataProtection__MongoDb__ConnectionString
Expand Down Expand Up @@ -215,6 +213,10 @@ spec:
- name: ReferenceData__BookProvider
value: {{ .referenceData.bookProvider | quote }}
{{- end }}
{{- if .referenceData.videoGameProvider }}
- name: ReferenceData__VideoGameProvider
value: {{ .referenceData.videoGameProvider | quote }}
{{- end }}
{{- if .referenceData.tmdb.apiKeySecretKeyRef }}
- name: Tmdb__ApiKey
valueFrom:
Expand Down
2 changes: 2 additions & 0 deletions charts/keeptrack/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ webapi:
referenceData:
# overrides the app's own default ("OpenLibrary") - see WebApi's AppConfiguration.BookReferenceProvider
bookProvider: ""
# overrides the app's own default ("IGDB") - see WebApi's AppConfiguration.VideoGameProvider
videoGameProvider: ""
# periodic background re-sync of stale reference data (every 24h); matches the app's own default
isSyncEnabled: true
tmdb:
Expand Down
63 changes: 50 additions & 13 deletions charts/sidelab/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contribution guide

## Update chart dependencies
## Updating chart dependencies

Add the Bitnami Helm repository:

Expand All @@ -23,18 +23,48 @@ Update `Chart.lock` and fetch the archive into `charts/` (gitignored, rebuilt fr
helm dependency update .
```

## Review the generated manifest
## Reviewing the generated manifest

```bash
helm template sidelab . -f values.yaml -f values.mine.yaml --namespace sidelab --debug > temp.yaml
```

Useful sanity checks on the output:

- the same value should never appear twice from two different sources (that's what the top-level `domain` value and reusing `ingress.className`/`ingress.tls`/the cert-manager annotation for lab sessions are for),
- and `--set database.backend=mongo` with nothing else set, or `--set ingress.enabled=true` with no `domain`/`ingress.host`, should fail fast with a clear `fail` message rather than render something broken.
- the same value should never appear twice from two different sources
(that's what the top-level `domain` value and reusing `ingress.className`/`ingress.tls`/the cert-manager annotation for lab sessions are for)
- having `--set database.backend=mongo` with nothing else set, or `--set ingress.enabled=true` with no `domain`/`ingress.host`,
should fail fast with a clear `fail` message rather than render something broken

## Validate on MicroK8s
## Running several launcher replicas

The launcher is stateless: sessions, lab-token replay protection and expiry claims are coordinated through the database, and running lab Pods are reconciled from it on startup.
So it scales horizontally once nothing is stored on the Pod itself:

```yaml
replicaCount: 3

database:
backend: mongo
mongo:
url: mongodb://<user>:<pass>@mongo:27017/sidelab

persistence:
enabled: false # nothing left to persist locally; a ReadWriteOnce PVC would pin the launcher to one Pod

launcher:
labAccess: ingress # nodeport lab URLs are built from the answering node's IP

extraEnv:
- name: TRUST_PROXY
value: "1" # the login throttle is per-replica; without this, per-IP throttling sees only the proxy
```

That combination also switches the Deployment from `Recreate` to `RollingUpdate`, so upgrades no longer drop the dashboard, and makes `autoscaling.enabled` usable.
Asking for more than one replica while `database.backend=sqlite` or a non-`ReadWriteMany` PVC is enabled fails at render time with the fix in the message,
rather than silently corrupting a database.

## Validating on MicroK8s

This is the fastest local loop: no cloud cluster, no DNS, no cert-manager required for the base cases below.

Expand Down Expand Up @@ -63,7 +93,7 @@ From the `sidelab` repo:

```bash
docker compose --profile build-only build
docker tag sidelab-app:latest sidelab-app:v1 # avoid :latest (see docs/wsl-microk8s.md)
docker tag sidelab-app:latest sidelab-app:v1
docker save sidelab-launcher:latest | microk8s images import -
docker save sidelab-app:v1 | microk8s images import -
```
Expand All @@ -84,7 +114,8 @@ A `microk8s stop`/`start` cycle (or one triggered by a snap refresh) can clear t

### 2. Known-good `values.mine.yaml` combinations

Point `image.repository`/`launcher.labImage` at the locally-imported tags in every scenario below (MicroK8s doesn't need the registry path prefix, plain `sidelab-launcher`/`sidelab-app` resolve to what was imported).
Point `image.repository`/`launcher.labImage` at the locally-imported tags in every scenario below
(MicroK8s doesn't need the registry path prefix, plain `sidelab-launcher`/`sidelab-app` resolve to what was imported).

#### a. Zero-config default: SQLite + NodePort

Expand Down Expand Up @@ -229,8 +260,10 @@ Two settings in there are easy to get wrong, and both fail quietly:

- **`ingress.tls.enabled: true` despite no cert-manager and no certificate.**
The launcher picks the scheme of the URL it hands learners by reading back whether the lab `Ingress` it just created has a `tls:` block.
Leave this false and every session opens at `http://<id>.labs.example.com`: the single-use `labToken` crosses the public internet in a plaintext query string, and the lab session cookie isn't marked `Secure`.
The per-session secret it names never materializes, and that's harmless here: the controller falls back to its default certificate, which nothing ever sees, because the upstream hop reaches it over HTTP.
Leave this false and every session opens at `http://<id>.labs.example.com`:
the single-use `labToken` crosses the public internet in a plaintext query string, and the lab session cookie isn't marked `Secure`.
The per-session secret it names never materializes, and that's harmless here: the controller falls back to its default certificate, which nothing ever sees,
because the upstream hop reaches it over HTTP.
- **`ingress.enabled: false` does not disable `ingress.className`/`ingress.tls`.** Both are deliberately reused for lab sessions, so you configure the ingress controller once rather than twice.

One thing to confirm on the certificate side, since `launcher.labDomain` decides it:
Expand Down Expand Up @@ -264,8 +297,10 @@ extraEnv:
```

The two required lines are `database.backend: mongo` and `persistence.enabled: false`.
Both are enforced: asking for `replicaCount > 1` (or `autoscaling.enabled` with a `maxReplicas > 1`) on SQLite, or with a PVC that isn't `ReadWriteMany`, fails the render with the fix in the message.
SQLite is a single-writer local file, and a `ReadWriteOnce` PVC can't be mounted by two Pods to begin with — a chart that let either through would hand you a silently split or corrupted database.
Both are enforced: asking for `replicaCount > 1` (or `autoscaling.enabled` with a `maxReplicas > 1`) on SQLite, or with a PVC that isn't `ReadWriteMany`,
fails the render with the fix in the message.
SQLite is a single-writer local file, and a `ReadWriteOnce` PVC can't be mounted by two Pods to begin with.
A chart that let either through would hand you a silently split or corrupted database.
Meeting both conditions is also what flips the `Deployment` strategy from `Recreate` to `RollingUpdate`, so upgrades stop dropping the dashboard.
Verify with:

Expand All @@ -275,8 +310,10 @@ helm template sidelab . -f values.mine.yaml | grep -A2 'strategy:'

Two more, not enforced because neither is wrong enough to block on:

- **`launcher.labAccess: ingress`.** NodePort lab URLs are built from `K8S_NODE_IP`, the IP of the node running the replica that served the request, so learners get URLs pointing at different nodes depending on which replica answered.
- **`TRUST_PROXY`.** The failed-login throttle is in-process, so `replicaCount: 3` already gives an attacker three times the budget (per-username throttling still applies on each replica). Without `TRUST_PROXY` the per-IP half stops working entirely, every request appearing to come from the ingress controller.
- **`launcher.labAccess: ingress`.** NodePort lab URLs are built from `K8S_NODE_IP`, the IP of the node running the replica that served the request,
so learners get URLs pointing at different nodes depending on which replica answered.
- **`TRUST_PROXY`.** The failed-login throttle is in-process, so `replicaCount: 3` already gives an attacker three times the budget (per-username throttling still applies on each replica).
Without `TRUST_PROXY` the per-IP half stops working entirely, every request appearing to come from the ingress controller.

### 3. Check everything came up

Expand Down
2 changes: 1 addition & 1 deletion charts/sidelab/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: >
Self-hosted interactive lab platform.
Deploys the Sidelab launcher, which manages lab sessions as ephemeral Kubernetes Pods inside tenant-scoped namespaces.
type: application
version: 0.1.5
version: 0.1.6
appVersion: "0.1.0"
keywords:
- sidelab
Expand Down
75 changes: 27 additions & 48 deletions charts/sidelab/README.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,37 @@
# Sidelab Helm Chart

Helm chart for [Sidelab](https://github.com/devpro-training/sidelab) — a self-hosted interactive lab platform.
Deploys the Sidelab launcher, which manages lab sessions as ephemeral Kubernetes Pods inside tenant-scoped namespaces.
Helm chart for Sidelab, a self-hosted interactive lab platform.

Supports SQLite (zero-dependency, quick look/demo) or MongoDB as the database backend, and NodePort or Ingress for exposing lab sessions.
`values.yaml` is the source of truth for every option.
It's fully commented, including Traefik/cert-manager/Let's Encrypt examples.
In a nutshell:

## Quick Start

Add the chart repository:

```bash
helm repo add devpro https://devpro.github.io/helm-charts
helm repo update
```

Create the `values.yaml` file to override [default values](values.yaml).

Install the application:
- Deploys the launcher, which manages lab sessions as ephemeral Kubernetes Pods inside tenant-scoped namespaces
- Supports SQLite (zero-dependency, quick look/demo) or MongoDB as the database backend, and NodePort or Ingress for exposing lab sessions

```bash
helm upgrade --install sidelab devpro/sidelab -f values.yaml --namespace sidelab --create-namespace
```

One value is always required: `image.repository`.
The chart is public but the sidelab images are not published to a public registry.
Everything else has working defaults: SQLite + NodePort + auto-generated secrets, no external dependencies.
See [CONTRIBUTING.md](CONTRIBUTING.md) for ready-to-use `values.mine.yaml` snippets covering the other use cases (MongoDB, Ingress + cert-manager, the bundled MongoDB demo chart).

## Running several launcher replicas

The launcher is stateless — sessions, lab-token replay protection and expiry claims are coordinated through the database, and running lab Pods are reconciled from it on startup — so it scales horizontally once nothing is stored on the Pod itself:
## Quick Start

```yaml
replicaCount: 3
1. Add the chart repository

database:
backend: mongo
mongo:
url: mongodb://user:pass@mongo:27017/sidelab
```bash
helm repo add devpro https://devpro.github.io/helm-charts
helm repo update
```

persistence:
enabled: false # nothing left to persist locally; a ReadWriteOnce PVC would pin the launcher to one Pod
2. Create the `values.yaml` file to override [default values](values.yaml)

launcher:
labAccess: ingress # nodeport lab URLs are built from the answering node's IP
> [!IMPORTANT]
> One value is always required: `image.repository`.
>
> `values.yaml` is the source of truth for every option.
> It's fully commented, including Traefik/cert-manager/Let's Encrypt examples.
>
> `CONTRIBUTING` contains known-good `values.yaml` combinations for every use case.

extraEnv:
- name: TRUST_PROXY
value: "1" # the login throttle is per-replica; without this, per-IP throttling sees only the proxy
```
3. Install the application:

That combination also switches the Deployment from `Recreate` to `RollingUpdate`, so upgrades no longer drop the dashboard, and makes `autoscaling.enabled` usable.
Asking for more than one replica while `database.backend=sqlite` or a non-`ReadWriteMany` PVC is enabled fails at render time with the fix in the message, rather than silently corrupting a database.
```bash
helm upgrade --install sidelab devpro/sidelab -f values.yaml \
--namespace sidelab --create-namespace
```

## Uninstall

Expand All @@ -62,9 +40,10 @@ helm uninstall sidelab -n sidelab
kubectl delete namespace sidelab
```

The auto-generated `<release>-auth` Secret (admin password) and the data PVC are deleted along with the namespace.
Back up `database.mongo.url`'s target or the PVC first if data must be kept.
> [!NOTE]
> The auto-generated `<release>-auth` Secret (admin password) and the data PVC are deleted along with the namespace.
> Back up `database.mongo.url`'s target or the PVC first if data must be kept.

## Going further

Check the [contribution guide](CONTRIBUTING.md) for MicroK8s validation steps and known-good `values.yaml` combinations for every use case.
Check the [contribution guide](CONTRIBUTING.md) for additional information.
Loading
Loading