Skip to content

Commit 5735b94

Browse files
authored
Feature/Make keeptrack scalable (#57)
* Update keeptrack and sidelab charts * Add dummy ci file * Bump version
1 parent d7b222a commit 5735b94

14 files changed

Lines changed: 163 additions & 61 deletions

File tree

charts/keeptrack/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
name: keeptrack
33
description: Helm chart for Keeptrack
44
type: application
5-
version: 0.2.1
6-
appVersion: "2.0"
5+
version: 0.2.2
6+
appVersion: "2.1"
77
home: https://github.com/devpro/keeptrack
88
sources:
99
- https://github.com/devpro/keeptrack

charts/keeptrack/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ Notable ones:
4444
for platforms that mandate the Kubernetes Pod Security Standards "restricted" profile.
4545
Disabled by default; validate in a non-production environment first.
4646

47+
## Scaling & high availability
48+
49+
Both deployments support `replicaCount > 1`, by design of the app itself rather than of any particular ingress (no sticky sessions / cookie affinity required anywhere -
50+
it works identically behind a Cloudflare tunnel, Traefik, ingress-nginx, or a plain `Service`).
51+
52+
**`webapi`** (requires app image >= the release that introduced the MongoDB-backed job store): fully stateless per request.
53+
Background-job progress (TV Time import, reference-data "sync now") is stored in MongoDB so any replica can answer a poll,
54+
and the daily reference-data sync elects a single runner through a MongoDB lease, so scaling out never multiplies TMDB/RAWG/Discogs traffic.
55+
56+
Just raise `webapi.replicaCount`.
57+
58+
**`blazorapp`** (Blazor Server): two settings make multiple replicas work, both on by the time you scale:
59+
60+
1. `blazorapp.dataProtection.enabled: true` (plus its MongoDB connection, typically the same secret as `webapi.db`) - shares the cookie-encryption key ring across replicas.
61+
Without it, a login cookie issued by one pod is unreadable by the others and users bounce between logged-in and logged-out.
62+
Worth enabling even at 1 replica: sessions then survive pod restarts.
63+
2. `blazorapp.webSocketsOnly: true` (the default) -
64+
each Blazor circuit runs on one long-lived WebSocket and therefore naturally sticks to the pod that owns its in-memory state, with no load-balancer affinity needed.
65+
Whatever sits in front only has to pass WebSockets through (Cloudflare tunnels and every mainstream ingress do).
66+
67+
Known limitation (inherent to Blazor Server, not fixable by this chart): a circuit lives in the memory of exactly one pod.
68+
When that pod dies or is replaced during a rollout, browsers connected to it reconnect and reload the page they were on
69+
(no user data is lost - the app saves edits as they are made).
70+
Extra replicas therefore add capacity and availability for new connections; they don't make an individual session survive the loss of its pod.
71+
After a transient network blip (pod still alive), the client's reconnect attempts re-roll across replicas until one lands on the right pod,
72+
which at small replica counts succeeds within the default retry budget.
73+
4774
## Development
4875

4976
Look at the [Contributing guide](CONTRIBUTING.md).

charts/keeptrack/templates/deployment.yaml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ spec:
9595
value: {{ $.Values.dotnet.logLevels.default | quote }}
9696
- name: Logging__LogLevel__Microsoft.AspNetCore
9797
value: {{ $.Values.dotnet.logLevels.framework | quote }}
98-
- name: Logging__LogLevel__Devpro
98+
- name: Logging__LogLevel__Keeptrack
9999
value: {{ $.Values.dotnet.logLevels.application | quote }}
100100
- name: Features__IsHttpsRedirectionEnabled
101101
value: {{ $.Values.dotnet.httpsRedirectionEnabled | quote }}
@@ -104,6 +104,22 @@ spec:
104104
{{- if eq .role "frontend" }}
105105
- name: WebApi__BaseUrl
106106
value: "http://{{ $.Values.webapi.name }}"
107+
- name: Features__IsWebSocketsOnlyEnabled
108+
value: {{ .webSocketsOnly | quote }}
109+
{{- if .dataProtection.enabled }}
110+
{{- if .dataProtection.connectionStringSecretKeyRef }}
111+
- name: DataProtection__MongoDb__ConnectionString
112+
valueFrom:
113+
secretKeyRef:
114+
name: {{ .dataProtection.connectionStringSecretKeyRef.name }}
115+
key: {{ .dataProtection.connectionStringSecretKeyRef.key }}
116+
{{- else }}
117+
- name: DataProtection__MongoDb__ConnectionString
118+
value: {{ .dataProtection.connectionString | quote }}
119+
{{- end }}
120+
- name: DataProtection__MongoDb__DatabaseName
121+
value: {{ .dataProtection.databaseName | quote }}
122+
{{- end }}
107123
{{- if $.Values.firebase.webApp.apiKeySecretKeyRef }}
108124
- name: Firebase__WebAppConfiguration__ApiKey
109125
valueFrom:
@@ -193,6 +209,8 @@ spec:
193209
value: {{ .db.databaseName | quote }}
194210
- name: Features__IsReferenceSyncEnabled
195211
value: {{ .referenceData.isSyncEnabled | quote }}
212+
- name: Features__FreeTierItemLimit
213+
value: {{ .freeTierItemLimit | quote }}
196214
{{- if .referenceData.bookProvider }}
197215
- name: ReferenceData__BookProvider
198216
value: {{ .referenceData.bookProvider | quote }}

charts/keeptrack/values.yaml

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
healthEndpoint: /health
99
tls:
1010
secretName: keeptrack-blazorapp-tls
11-
tag: 2.0.29367732118 # ref. https://hub.docker.com/r/devprofr/keeptrack-blazorapp/tags
11+
tag: 2.1.29541631973 # ref. https://hub.docker.com/r/devprofr/keeptrack-blazorapp/tags
1212
replicaCount: 1
1313
resources: {}
1414
# limits:
@@ -22,6 +22,25 @@
2222
# value: "yyyy"
2323
additionalPodLabels: {}
2424
host: ""
25+
# Blazor Server scale-out support - see README "Scaling & high availability".
26+
# Persists the app's Data Protection key ring (which encrypts the auth cookie and antiforgery tokens)
27+
# in MongoDB, so every replica can decrypt what any other replica issued. REQUIRED before setting replicaCount > 1;
28+
# recommended even at 1 replica (sessions then survive pod restarts).
29+
dataProtection:
30+
enabled: false
31+
# important when enabled: connectionString or connectionStringSecretKeyRef must be provided
32+
# (typically the same secret as webapi.db)
33+
# connectionStringSecretKeyRef:
34+
# name: ""
35+
# key: ""
36+
# connectionString: ""
37+
databaseName: "keeptrack"
38+
# Forces the Blazor Server circuit onto a single WebSocket with no negotiate round-trip, so a circuit
39+
# naturally sticks to the replica that owns its state -
40+
# no load-balancer cookie affinity needed, which makes replicaCount > 1 work behind anything that passes WebSockets through (cloudflared, Traefik, plain Services).
41+
# Only set to false if a proxy in front can't pass WebSockets;
42+
# such a deployment must then stay at replicaCount 1.
43+
webSocketsOnly: true
2544

2645
webapi:
2746
enabled: true
@@ -35,20 +54,23 @@ webapi:
3554
secretName: keeptrack-webapi-tls
3655
host: ""
3756
replicaCount: 1
38-
tag: 2.0.29367732118 # ref. https://hub.docker.com/r/devprofr/keeptrack-webapi/tags
57+
tag: 2.1.29541631973 # ref. https://hub.docker.com/r/devprofr/keeptrack-webapi/tags
3958
resources: {}
4059
extraEnv: []
4160
additionalPodLabels: {}
61+
# how many items a signed-in account WITHOUT the Firebase "member"/"admin" role may create per free-tier collection (movies, TV shows);
62+
# such accounts have no access to the other collections at all -
63+
# grant `role: member` via the Firebase Admin SDK to unlock everything (see the app's CONTRIBUTING.md)
64+
freeTierItemLimit: 20
4265
db:
4366
# important: connectionString or connectionStringSecretKeyRef must be provided
4467
# connectionStringSecretKeyRef:
4568
# name: ""
4669
# key: ""
4770
# connectionString: "someconnstring"
4871
databaseName: "keeptrack"
49-
# optional: third-party providers used to enrich movies/TV shows/video games/books/albums with
50-
# posters, synopsis, cast and episode data. Leaving a key empty just means that provider's
51-
# enrichment silently fails (401/403 from the provider) - the rest of the app is unaffected.
72+
# optional: third-party providers used to enrich movies/TV shows/video games/books/albums with posters, synopsis, cast and episode data.
73+
# Leaving a key empty just means that provider's enrichment silently fails (401/403 from the provider) - the rest of the app is unaffected.
5274
referenceData:
5375
# overrides the app's own default ("OpenLibrary") - see WebApi's AppConfiguration.BookReferenceProvider
5476
bookProvider: ""

charts/sidelab/.helmignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
# Patterns to ignore when building packages.
2-
.DS_Store
1+
.DS_Store
32
.git/
43
.gitignore
54
.helmignore
65
*.orig
76
*.swp
87
*.bak
8+
values.mine.yaml
9+
temp*.yaml
10+
kubeconfig

charts/sidelab/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: >
44
Self-hosted interactive lab platform.
55
Deploys the Sidelab launcher, which manages lab sessions as ephemeral Kubernetes Pods inside tenant-scoped namespaces.
66
type: application
7-
version: 0.1.1
7+
version: 0.1.2
88
appVersion: "0.1.0"
99
keywords:
1010
- sidelab

charts/sidelab/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
Helm chart for [Sidelab](https://github.com/devpro-training/sidelab) — a self-hosted interactive lab platform.
44
Deploys the Sidelab launcher, which manages lab sessions as ephemeral Kubernetes Pods inside tenant-scoped namespaces.
55

6-
Supports SQLite (zero-dependency, quick look/demo) or MongoDB (recommended for anything you keep — external, or a bundled disposable instance for a demo) as the database backend, and NodePort or Ingress for exposing lab sessions.
6+
Supports SQLite (zero-dependency, quick look/demo) or MongoDB (recommended for anything you keep — external, or a bundled disposable instance for a demo)
7+
as the database backend, and NodePort or Ingress for exposing lab sessions.
78
`values.yaml` is the source of truth for every option — it's fully commented, including Traefik/cert-manager/Let's Encrypt examples.
89

910
## Quick Start
@@ -23,7 +24,8 @@ Install the application:
2324
helm upgrade --install sidelab devpro/sidelab -f values.yaml --namespace sidelab --create-namespace
2425
```
2526

26-
Nothing above is strictly required — `helm upgrade --install sidelab devpro/sidelab --namespace sidelab --create-namespace` alone works too: SQLite + NodePort + auto-generated secrets, no external dependencies.
27+
One value is always required: `image.repository` — the chart is public but the sidelab images are not published to a public registry.
28+
Everything else has working defaults: SQLite + NodePort + auto-generated secrets, no external dependencies.
2729
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).
2830

2931
## Uninstall
@@ -33,7 +35,8 @@ helm uninstall sidelab -n sidelab
3335
kubectl delete namespace sidelab
3436
```
3537

36-
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 you need to keep the data.
38+
The auto-generated `<release>-auth` Secret (admin password) and the data PVC are deleted along with the namespace —
39+
back up `database.mongo.url`'s target or the PVC first if you need to keep the data.
3740

3841
## Going further
3942

charts/sidelab/ci/ci-values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
image:
2+
repository: ci-dummy-registry/app

charts/sidelab/templates/NOTES.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2626
First login
2727
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
28-
Username: {{ .Values.auth.adminUsername }}
2928
{{- if .Values.auth.existingSecret }}
29+
Username: (value of the "ADMIN_USERNAME" key in Secret/{{ .Values.auth.existingSecret }})
3030
Password: (value of the "ADMIN_PASSWORD" key in Secret/{{ .Values.auth.existingSecret }})
3131
{{- else if .Values.auth.adminPassword }}
32+
Username: {{ .Values.auth.adminUsername }}
3233
Password: (value of auth.adminPassword you supplied)
3334
{{- else }}
35+
Username: {{ .Values.auth.adminUsername }}
3436
Password: auto-generated on first install — retrieve it with:
3537

3638
kubectl get secret -n {{ .Release.Namespace }} {{ include "sidelab.authSecretName" . }} \

charts/sidelab/templates/_helpers.tpl

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{{/*
1+
{{/*
22
Expand the name of the chart.
33
*/}}
44
{{- define "sidelab.name" -}}
@@ -101,8 +101,8 @@ Image tag — falls back to .Chart.AppVersion.
101101
{{- end }}
102102

103103
{{/*
104-
Dashboard hostname. ingress.host wins; otherwise derived from the shared
105-
top-level `domain` value as "sidelab.<domain>". Empty if neither is set.
104+
Dashboard hostname. ingress.host wins; otherwise derived from the shared top-level `domain` value as "sidelab.<domain>".
105+
Empty if neither is set.
106106
*/}}
107107
{{- define "sidelab.ingressHost" -}}
108108
{{- if .Values.ingress.host -}}
@@ -113,8 +113,8 @@ top-level `domain` value as "sidelab.<domain>". Empty if neither is set.
113113
{{- end }}
114114

115115
{{/*
116-
Lab session wildcard domain. launcher.labDomain wins; otherwise derived from
117-
the shared top-level `domain` value as "labs.<domain>". Empty if neither is set.
116+
Lab session wildcard domain. launcher.labDomain wins; otherwise derived from the shared top-level `domain` value as "labs.<domain>".
117+
Empty if neither is set.
118118
*/}}
119119
{{- define "sidelab.labDomain" -}}
120120
{{- if .Values.launcher.labDomain -}}
@@ -125,11 +125,11 @@ the shared top-level `domain` value as "labs.<domain>". Empty if neither is set.
125125
{{- end }}
126126

127127
{{/*
128-
MongoDB connection string. database.mongo.url wins; otherwise, if the bundled
129-
mongodb subchart is enabled, derived from its default standalone Service name
130-
("<release-name>-mongodb", per the alias in Chart.yaml and the Bitnami chart's
131-
own naming convention) and mongodb.auth.rootPassword. Only called from
132-
secret.yaml when database.mongo.existingSecret is not set.
128+
MongoDB connection string.
129+
database.mongo.url wins;
130+
otherwise, if the bundled mongodb subchart is enabled, derived from its default standalone Service name
131+
("<release-name>-mongodb", per the alias in Chart.yaml and the Bitnami chart's own naming convention) and mongodb.auth.rootPassword.
132+
Only called from secret.yaml when database.mongo.existingSecret is not set.
133133
*/}}
134134
{{- define "sidelab.mongoUrl" -}}
135135
{{- if .Values.database.mongo.url -}}
@@ -140,10 +140,9 @@ secret.yaml when database.mongo.existingSecret is not set.
140140
{{- end }}
141141

142142
{{/*
143-
JWT secret — auth.jwtSecret wins; otherwise reused from the existing Secret on
144-
upgrade (via lookup), or freshly generated on first install. `lookup` returns
145-
nothing outside a real cluster (e.g. `helm template`), so offline rendering
146-
always generates a fresh value — expected, and harmless for a dry render.
143+
JWT secret — auth.jwtSecret wins; otherwise reused from the existing Secret on upgrade (via lookup), or freshly generated on first install.
144+
`lookup` returns nothing outside a real cluster (e.g. `helm template`), so offline rendering always generates a fresh value —
145+
expected, and harmless for a dry render.
147146
Only called from secret.yaml when auth.existingSecret is not set.
148147
*/}}
149148
{{- define "sidelab.jwtSecret" -}}
@@ -180,6 +179,9 @@ Only called from secret.yaml when auth.existingSecret is not set.
180179
Validate required values and emit a clear error message.
181180
*/}}
182181
{{- define "sidelab.validateValues" -}}
182+
{{- if not .Values.image.repository }}
183+
{{- fail "image.repository is required." }}
184+
{{- end }}
183185
{{- if and (eq .Values.database.backend "mongo") (not .Values.database.mongo.url) (not .Values.database.mongo.existingSecret) (not .Values.mongodb.enabled) }}
184186
{{- fail "database.backend=mongo needs one of: database.mongo.url, database.mongo.existingSecret, or mongodb.enabled=true." }}
185187
{{- end }}

0 commit comments

Comments
 (0)